summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test')
-rw-r--r--browser/base/content/test/about/POSTSearchEngine.xml6
-rw-r--r--browser/base/content/test/about/browser.ini59
-rw-r--r--browser/base/content/test/about/browser_aboutCertError.js548
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_clockSkew.js153
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_exception.js221
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_mitm.js158
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_noSubjectAltName.js67
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_offlineSupport.js51
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_telemetry.js164
-rw-r--r--browser/base/content/test/about/browser_aboutDialog_distribution.js66
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_POST.js104
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_composing.js110
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_searchbar.js44
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_suggestion.js78
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_telemetry.js101
-rw-r--r--browser/base/content/test/about/browser_aboutNetError.js245
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_csp_iframe.js153
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_native_fallback.js174
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_trr.js189
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js139
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbar.js311
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js158
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js82
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarPrefs.js74
-rw-r--r--browser/base/content/test/about/browser_aboutStopReload.js169
-rw-r--r--browser/base/content/test/about/browser_aboutSupport.js146
-rw-r--r--browser/base/content/test/about/browser_aboutSupport_newtab_security_state.js19
-rw-r--r--browser/base/content/test/about/browser_aboutSupport_places.js45
-rw-r--r--browser/base/content/test/about/browser_bug435325.js58
-rw-r--r--browser/base/content/test/about/browser_bug633691.js32
-rw-r--r--browser/base/content/test/about/csp_iframe.sjs33
-rw-r--r--browser/base/content/test/about/dummy_page.html9
-rw-r--r--browser/base/content/test/about/head.js220
-rw-r--r--browser/base/content/test/about/iframe_page_csp.html16
-rw-r--r--browser/base/content/test/about/iframe_page_xfo.html16
-rw-r--r--browser/base/content/test/about/print_postdata.sjs25
-rw-r--r--browser/base/content/test/about/searchSuggestionEngine.sjs9
-rw-r--r--browser/base/content/test/about/searchSuggestionEngine.xml11
-rw-r--r--browser/base/content/test/about/slow_loading_page.sjs29
-rw-r--r--browser/base/content/test/about/xfo_iframe.sjs34
-rw-r--r--browser/base/content/test/alerts/browser.ini22
-rw-r--r--browser/base/content/test/alerts/browser_notification_close.js107
-rw-r--r--browser/base/content/test/alerts/browser_notification_do_not_disturb.js160
-rw-r--r--browser/base/content/test/alerts/browser_notification_open_settings.js80
-rw-r--r--browser/base/content/test/alerts/browser_notification_remove_permission.js86
-rw-r--r--browser/base/content/test/alerts/browser_notification_replace.js66
-rw-r--r--browser/base/content/test/alerts/browser_notification_tab_switching.js117
-rw-r--r--browser/base/content/test/alerts/file_dom_notifications.html39
-rw-r--r--browser/base/content/test/alerts/head.js73
-rw-r--r--browser/base/content/test/backforward/browser.ini2
-rw-r--r--browser/base/content/test/backforward/browser_history_menu.js175
-rw-r--r--browser/base/content/test/caps/browser.ini6
-rw-r--r--browser/base/content/test/caps/browser_principalSerialization_csp.js106
-rw-r--r--browser/base/content/test/caps/browser_principalSerialization_json.js161
-rw-r--r--browser/base/content/test/caps/browser_principalSerialization_version1.js159
-rw-r--r--browser/base/content/test/captivePortal/browser.ini12
-rw-r--r--browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js125
-rw-r--r--browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js108
-rw-r--r--browser/base/content/test/captivePortal/browser_captivePortalTabReference.js65
-rw-r--r--browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js221
-rw-r--r--browser/base/content/test/captivePortal/browser_captivePortal_https_only.js73
-rw-r--r--browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js152
-rw-r--r--browser/base/content/test/captivePortal/head.js260
-rw-r--r--browser/base/content/test/chrome/chrome.ini4
-rw-r--r--browser/base/content/test/chrome/test_aboutCrashed.xhtml77
-rw-r--r--browser/base/content/test/chrome/test_aboutRestartRequired.xhtml76
-rw-r--r--browser/base/content/test/contentTheme/browser.ini3
-rw-r--r--browser/base/content/test/contentTheme/browser_contentTheme_in_process_tab.js80
-rw-r--r--browser/base/content/test/contextMenu/browser.ini91
-rw-r--r--browser/base/content/test/contextMenu/browser_bug1798178.js89
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu.js1943
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js182
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_contenteditable.js118
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_iframe.js73
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_input.js387
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_inspect.js61
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_keyword.js198
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_linkopen.js109
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.html56
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.js186
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js78
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js144
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_share_win.js77
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_shareurl.html2
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_spellcheck.js334
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_touch.js94
-rw-r--r--browser/base/content/test/contextMenu/browser_copy_image_link.js40
-rw-r--r--browser/base/content/test/contextMenu/browser_strip_on_share_link.js151
-rw-r--r--browser/base/content/test/contextMenu/browser_utilityOverlay.js78
-rw-r--r--browser/base/content/test/contextMenu/browser_utilityOverlayPrincipal.js72
-rw-r--r--browser/base/content/test/contextMenu/browser_view_image.js197
-rw-r--r--browser/base/content/test/contextMenu/bug1798178.sjs9
-rw-r--r--browser/base/content/test/contextMenu/contextmenu_common.js437
-rw-r--r--browser/base/content/test/contextMenu/ctxmenu-image.pngbin0 -> 5401 bytes
-rw-r--r--browser/base/content/test/contextMenu/doggy.pngbin0 -> 46876 bytes
-rw-r--r--browser/base/content/test/contextMenu/file_bug1798178.html5
-rw-r--r--browser/base/content/test/contextMenu/firebird.pngbin0 -> 16179 bytes
-rw-r--r--browser/base/content/test/contextMenu/firebird.png^headers^2
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu.html61
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu_input.html30
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu_keyword.html17
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu_webext.html12
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu_xul.xhtml9
-rw-r--r--browser/base/content/test/contextMenu/test_contextmenu_iframe.html11
-rw-r--r--browser/base/content/test/contextMenu/test_contextmenu_links.html14
-rw-r--r--browser/base/content/test/contextMenu/test_view_image_inline_svg.html15
-rw-r--r--browser/base/content/test/contextMenu/test_view_image_revoked_cached_blob.html40
-rw-r--r--browser/base/content/test/favicons/accept.html9
-rw-r--r--browser/base/content/test/favicons/accept.sjs15
-rw-r--r--browser/base/content/test/favicons/auth_test.html11
-rw-r--r--browser/base/content/test/favicons/auth_test.png0
-rw-r--r--browser/base/content/test/favicons/auth_test.png^headers^2
-rw-r--r--browser/base/content/test/favicons/blank.html6
-rw-r--r--browser/base/content/test/favicons/browser.ini113
-rw-r--r--browser/base/content/test/favicons/browser_bug408415.js34
-rw-r--r--browser/base/content/test/favicons/browser_bug550565.js35
-rw-r--r--browser/base/content/test/favicons/browser_favicon_accept.js30
-rw-r--r--browser/base/content/test/favicons/browser_favicon_auth.js27
-rw-r--r--browser/base/content/test/favicons/browser_favicon_cache.js50
-rw-r--r--browser/base/content/test/favicons/browser_favicon_change.js33
-rw-r--r--browser/base/content/test/favicons/browser_favicon_change_not_in_document.js55
-rw-r--r--browser/base/content/test/favicons/browser_favicon_credentials.js89
-rw-r--r--browser/base/content/test/favicons/browser_favicon_crossorigin.js61
-rw-r--r--browser/base/content/test/favicons/browser_favicon_load.js168
-rw-r--r--browser/base/content/test/favicons/browser_favicon_nostore.js169
-rw-r--r--browser/base/content/test/favicons/browser_favicon_referer.js62
-rw-r--r--browser/base/content/test/favicons/browser_favicon_store.js56
-rw-r--r--browser/base/content/test/favicons/browser_icon_discovery.js136
-rw-r--r--browser/base/content/test/favicons/browser_invalid_href_fallback.js29
-rw-r--r--browser/base/content/test/favicons/browser_missing_favicon.js36
-rw-r--r--browser/base/content/test/favicons/browser_mixed_content.js26
-rw-r--r--browser/base/content/test/favicons/browser_multiple_icons_in_short_timeframe.js37
-rw-r--r--browser/base/content/test/favicons/browser_oversized.js25
-rw-r--r--browser/base/content/test/favicons/browser_preferred_icons.js140
-rw-r--r--browser/base/content/test/favicons/browser_redirect.js20
-rw-r--r--browser/base/content/test/favicons/browser_rich_icons.js50
-rw-r--r--browser/base/content/test/favicons/browser_rooticon.js24
-rw-r--r--browser/base/content/test/favicons/browser_subframe_favicons_not_used.js22
-rw-r--r--browser/base/content/test/favicons/browser_title_flicker.js185
-rw-r--r--browser/base/content/test/favicons/cookie_favicon.html11
-rw-r--r--browser/base/content/test/favicons/cookie_favicon.sjs26
-rw-r--r--browser/base/content/test/favicons/credentials.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/favicons/credentials.png^headers^3
-rw-r--r--browser/base/content/test/favicons/credentials1.html10
-rw-r--r--browser/base/content/test/favicons/credentials2.html10
-rw-r--r--browser/base/content/test/favicons/crossorigin.html10
-rw-r--r--browser/base/content/test/favicons/crossorigin.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/favicons/crossorigin.png^headers^1
-rw-r--r--browser/base/content/test/favicons/datauri-favicon.html8
-rw-r--r--browser/base/content/test/favicons/discovery.html8
-rw-r--r--browser/base/content/test/favicons/file_bug970276_favicon1.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/favicons/file_bug970276_favicon2.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/favicons/file_bug970276_popup1.html14
-rw-r--r--browser/base/content/test/favicons/file_bug970276_popup2.html12
-rw-r--r--browser/base/content/test/favicons/file_favicon.html11
-rw-r--r--browser/base/content/test/favicons/file_favicon.pngbin0 -> 344 bytes
-rw-r--r--browser/base/content/test/favicons/file_favicon.png^headers^1
-rw-r--r--browser/base/content/test/favicons/file_favicon_change.html13
-rw-r--r--browser/base/content/test/favicons/file_favicon_change_not_in_document.html20
-rw-r--r--browser/base/content/test/favicons/file_favicon_no_referrer.html11
-rw-r--r--browser/base/content/test/favicons/file_favicon_redirect.html12
-rw-r--r--browser/base/content/test/favicons/file_favicon_redirect.ico0
-rw-r--r--browser/base/content/test/favicons/file_favicon_redirect.ico^headers^2
-rw-r--r--browser/base/content/test/favicons/file_favicon_thirdParty.html11
-rw-r--r--browser/base/content/test/favicons/file_generic_favicon.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/favicons/file_insecure_favicon.html11
-rw-r--r--browser/base/content/test/favicons/file_invalid_href.html12
-rw-r--r--browser/base/content/test/favicons/file_mask_icon.html11
-rw-r--r--browser/base/content/test/favicons/file_rich_icon.html12
-rw-r--r--browser/base/content/test/favicons/file_with_favicon.html12
-rw-r--r--browser/base/content/test/favicons/file_with_slow_favicon.html10
-rw-r--r--browser/base/content/test/favicons/head.js98
-rw-r--r--browser/base/content/test/favicons/icon.svg11
-rw-r--r--browser/base/content/test/favicons/large.pngbin0 -> 21237 bytes
-rw-r--r--browser/base/content/test/favicons/large_favicon.html12
-rw-r--r--browser/base/content/test/favicons/moz.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/favicons/no-store.html11
-rw-r--r--browser/base/content/test/favicons/no-store.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/favicons/no-store.png^headers^1
-rw-r--r--browser/base/content/test/favicons/rich_moz_1.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/favicons/rich_moz_2.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/forms/browser.ini21
-rw-r--r--browser/base/content/test/forms/browser_selectpopup.js913
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_colors.js867
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_dir.js21
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_large.js338
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_searchfocus.js36
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_text_transform.js40
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_toplevel.js19
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_user_input.js90
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_width.js49
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_xhtml.js36
-rw-r--r--browser/base/content/test/forms/head.js51
-rw-r--r--browser/base/content/test/fullscreen/FullscreenFrame.sys.mjs103
-rw-r--r--browser/base/content/test/fullscreen/browser.ini31
-rw-r--r--browser/base/content/test/fullscreen/browser_bug1557041.js47
-rw-r--r--browser/base/content/test/fullscreen/browser_bug1620341.js108
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js252
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js142
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_cross_origin.js64
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_enterInUrlbar.js56
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_from_minimize.js82
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_keydown_reservation.js112
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_menus.js72
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_newtab.js55
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_newwindow.js83
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_permissions_prompt.js160
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_warning.js280
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js110
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_window_open.js102
-rw-r--r--browser/base/content/test/fullscreen/fullscreen.html12
-rw-r--r--browser/base/content/test/fullscreen/fullscreen_frame.html9
-rw-r--r--browser/base/content/test/fullscreen/head.js164
-rw-r--r--browser/base/content/test/fullscreen/open_and_focus_helper.html56
-rw-r--r--browser/base/content/test/general/alltabslistener.html8
-rw-r--r--browser/base/content/test/general/app_bug575561.html18
-rw-r--r--browser/base/content/test/general/app_subframe_bug575561.html12
-rw-r--r--browser/base/content/test/general/audio.oggbin0 -> 14293 bytes
-rw-r--r--browser/base/content/test/general/browser.ini416
-rw-r--r--browser/base/content/test/general/browser_accesskeys.js202
-rw-r--r--browser/base/content/test/general/browser_addCertException.js77
-rw-r--r--browser/base/content/test/general/browser_alltabslistener.js331
-rw-r--r--browser/base/content/test/general/browser_backButtonFitts.js40
-rw-r--r--browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js114
-rw-r--r--browser/base/content/test/general/browser_bug1261299.js112
-rw-r--r--browser/base/content/test/general/browser_bug1297539.js122
-rw-r--r--browser/base/content/test/general/browser_bug1299667.js70
-rw-r--r--browser/base/content/test/general/browser_bug321000.js91
-rw-r--r--browser/base/content/test/general/browser_bug356571.js100
-rw-r--r--browser/base/content/test/general/browser_bug380960.js18
-rw-r--r--browser/base/content/test/general/browser_bug406216.js64
-rw-r--r--browser/base/content/test/general/browser_bug417483.js50
-rw-r--r--browser/base/content/test/general/browser_bug424101.js72
-rw-r--r--browser/base/content/test/general/browser_bug427559.js41
-rw-r--r--browser/base/content/test/general/browser_bug431826.js56
-rw-r--r--browser/base/content/test/general/browser_bug432599.js109
-rw-r--r--browser/base/content/test/general/browser_bug455852.js27
-rw-r--r--browser/base/content/test/general/browser_bug462289.js144
-rw-r--r--browser/base/content/test/general/browser_bug462673.js66
-rw-r--r--browser/base/content/test/general/browser_bug479408.js23
-rw-r--r--browser/base/content/test/general/browser_bug479408_sample.html4
-rw-r--r--browser/base/content/test/general/browser_bug481560.js16
-rw-r--r--browser/base/content/test/general/browser_bug484315.js14
-rw-r--r--browser/base/content/test/general/browser_bug491431.js42
-rw-r--r--browser/base/content/test/general/browser_bug495058.js53
-rw-r--r--browser/base/content/test/general/browser_bug519216.js48
-rw-r--r--browser/base/content/test/general/browser_bug520538.js27
-rw-r--r--browser/base/content/test/general/browser_bug521216.js68
-rw-r--r--browser/base/content/test/general/browser_bug533232.js56
-rw-r--r--browser/base/content/test/general/browser_bug537013.js168
-rw-r--r--browser/base/content/test/general/browser_bug537474.js20
-rw-r--r--browser/base/content/test/general/browser_bug563588.js42
-rw-r--r--browser/base/content/test/general/browser_bug565575.js21
-rw-r--r--browser/base/content/test/general/browser_bug567306.js65
-rw-r--r--browser/base/content/test/general/browser_bug575561.js118
-rw-r--r--browser/base/content/test/general/browser_bug577121.js27
-rw-r--r--browser/base/content/test/general/browser_bug578534.js31
-rw-r--r--browser/base/content/test/general/browser_bug579872.js26
-rw-r--r--browser/base/content/test/general/browser_bug581253.js74
-rw-r--r--browser/base/content/test/general/browser_bug585785.js48
-rw-r--r--browser/base/content/test/general/browser_bug585830.js27
-rw-r--r--browser/base/content/test/general/browser_bug594131.js25
-rw-r--r--browser/base/content/test/general/browser_bug596687.js28
-rw-r--r--browser/base/content/test/general/browser_bug597218.js40
-rw-r--r--browser/base/content/test/general/browser_bug609700.js28
-rw-r--r--browser/base/content/test/general/browser_bug623893.js50
-rw-r--r--browser/base/content/test/general/browser_bug624734.js49
-rw-r--r--browser/base/content/test/general/browser_bug664672.js27
-rw-r--r--browser/base/content/test/general/browser_bug676619.js225
-rw-r--r--browser/base/content/test/general/browser_bug710878.js49
-rw-r--r--browser/base/content/test/general/browser_bug724239.js56
-rw-r--r--browser/base/content/test/general/browser_bug734076.js195
-rw-r--r--browser/base/content/test/general/browser_bug749738.js32
-rw-r--r--browser/base/content/test/general/browser_bug763468_perwindowpb.js57
-rw-r--r--browser/base/content/test/general/browser_bug767836_perwindowpb.js72
-rw-r--r--browser/base/content/test/general/browser_bug817947.js51
-rw-r--r--browser/base/content/test/general/browser_bug832435.js26
-rw-r--r--browser/base/content/test/general/browser_bug882977.js33
-rw-r--r--browser/base/content/test/general/browser_bug963945.js26
-rw-r--r--browser/base/content/test/general/browser_clipboard.js290
-rw-r--r--browser/base/content/test/general/browser_clipboard_pastefile.js133
-rw-r--r--browser/base/content/test/general/browser_contentAltClick.js205
-rw-r--r--browser/base/content/test/general/browser_ctrlTab.js464
-rw-r--r--browser/base/content/test/general/browser_datachoices_notification.js287
-rw-r--r--browser/base/content/test/general/browser_documentnavigation.js493
-rw-r--r--browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js237
-rw-r--r--browser/base/content/test/general/browser_double_close_tab.js120
-rw-r--r--browser/base/content/test/general/browser_drag.js64
-rw-r--r--browser/base/content/test/general/browser_duplicateIDs.js11
-rw-r--r--browser/base/content/test/general/browser_findbarClose.js47
-rw-r--r--browser/base/content/test/general/browser_focusonkeydown.js34
-rw-r--r--browser/base/content/test/general/browser_fullscreen-window-open.js366
-rw-r--r--browser/base/content/test/general/browser_gestureSupport.js1132
-rw-r--r--browser/base/content/test/general/browser_hide_removing.js27
-rw-r--r--browser/base/content/test/general/browser_homeDrop.js117
-rw-r--r--browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js48
-rw-r--r--browser/base/content/test/general/browser_lastAccessedTab.js62
-rw-r--r--browser/base/content/test/general/browser_menuButtonFitts.js69
-rw-r--r--browser/base/content/test/general/browser_middleMouse_noJSPaste.js49
-rw-r--r--browser/base/content/test/general/browser_minimize.js49
-rw-r--r--browser/base/content/test/general/browser_modifiedclick_inherit_principal.js42
-rw-r--r--browser/base/content/test/general/browser_newTabDrop.js221
-rw-r--r--browser/base/content/test/general/browser_newWindowDrop.js230
-rw-r--r--browser/base/content/test/general/browser_new_http_window_opened_from_file_tab.js63
-rw-r--r--browser/base/content/test/general/browser_newwindow_focus.js93
-rw-r--r--browser/base/content/test/general/browser_plainTextLinks.js237
-rw-r--r--browser/base/content/test/general/browser_printpreview.js43
-rw-r--r--browser/base/content/test/general/browser_private_browsing_window.js133
-rw-r--r--browser/base/content/test/general/browser_private_no_prompt.js12
-rw-r--r--browser/base/content/test/general/browser_refreshBlocker.js209
-rw-r--r--browser/base/content/test/general/browser_relatedTabs.js74
-rw-r--r--browser/base/content/test/general/browser_remoteTroubleshoot.js130
-rw-r--r--browser/base/content/test/general/browser_remoteWebNavigation_postdata.js53
-rw-r--r--browser/base/content/test/general/browser_restore_isAppTab.js87
-rw-r--r--browser/base/content/test/general/browser_save_link-perwindowpb.js214
-rw-r--r--browser/base/content/test/general/browser_save_link_when_window_navigates.js197
-rw-r--r--browser/base/content/test/general/browser_save_private_link_perwindowpb.js127
-rw-r--r--browser/base/content/test/general/browser_save_video.js99
-rw-r--r--browser/base/content/test/general/browser_save_video_frame.js103
-rw-r--r--browser/base/content/test/general/browser_selectTabAtIndex.js89
-rw-r--r--browser/base/content/test/general/browser_star_hsts.js87
-rw-r--r--browser/base/content/test/general/browser_star_hsts.sjs12
-rw-r--r--browser/base/content/test/general/browser_storagePressure_notification.js182
-rw-r--r--browser/base/content/test/general/browser_tabDrop.js207
-rw-r--r--browser/base/content/test/general/browser_tab_close_dependent_window.js35
-rw-r--r--browser/base/content/test/general/browser_tab_detach_restore.js54
-rw-r--r--browser/base/content/test/general/browser_tab_drag_drop_perwindow.js423
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop.js257
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop2.js65
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop2_frame1.xhtml158
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop_embed.html2
-rw-r--r--browser/base/content/test/general/browser_tabfocus.js811
-rw-r--r--browser/base/content/test/general/browser_tabs_close_beforeunload.js69
-rw-r--r--browser/base/content/test/general/browser_tabs_isActive.js235
-rw-r--r--browser/base/content/test/general/browser_tabs_owner.js40
-rw-r--r--browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js144
-rw-r--r--browser/base/content/test/general/browser_typeAheadFind.js31
-rw-r--r--browser/base/content/test/general/browser_unknownContentType_title.js88
-rw-r--r--browser/base/content/test/general/browser_unloaddialogs.js40
-rw-r--r--browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js60
-rw-r--r--browser/base/content/test/general/browser_visibleFindSelection.js62
-rw-r--r--browser/base/content/test/general/browser_visibleTabs.js125
-rw-r--r--browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js35
-rw-r--r--browser/base/content/test/general/browser_visibleTabs_tabPreview.js52
-rw-r--r--browser/base/content/test/general/browser_windowactivation.js112
-rw-r--r--browser/base/content/test/general/browser_zbug569342.js77
-rw-r--r--browser/base/content/test/general/bug792517-2.html5
-rw-r--r--browser/base/content/test/general/bug792517.html5
-rw-r--r--browser/base/content/test/general/bug792517.sjs13
-rw-r--r--browser/base/content/test/general/clipboard_pastefile.html52
-rw-r--r--browser/base/content/test/general/close_beforeunload.html8
-rw-r--r--browser/base/content/test/general/close_beforeunload_opens_second_tab.html3
-rw-r--r--browser/base/content/test/general/download_page.html72
-rw-r--r--browser/base/content/test/general/download_page_1.txt1
-rw-r--r--browser/base/content/test/general/download_page_2.txt1
-rw-r--r--browser/base/content/test/general/download_with_content_disposition_header.sjs19
-rw-r--r--browser/base/content/test/general/dummy.ics13
-rw-r--r--browser/base/content/test/general/dummy.ics^headers^1
-rw-r--r--browser/base/content/test/general/dummy_page.html9
-rw-r--r--browser/base/content/test/general/file_documentnavigation_frameset.html12
-rw-r--r--browser/base/content/test/general/file_double_close_tab.html15
-rw-r--r--browser/base/content/test/general/file_fullscreen-window-open.html22
-rw-r--r--browser/base/content/test/general/file_window_activation.html4
-rw-r--r--browser/base/content/test/general/file_window_activation2.html1
-rw-r--r--browser/base/content/test/general/file_with_link_to_http.html9
-rw-r--r--browser/base/content/test/general/head.js347
-rw-r--r--browser/base/content/test/general/moz.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/general/navigating_window_with_download.html7
-rw-r--r--browser/base/content/test/general/print_postdata.sjs25
-rw-r--r--browser/base/content/test/general/redirect_download.sjs11
-rw-r--r--browser/base/content/test/general/refresh_header.sjs24
-rw-r--r--browser/base/content/test/general/refresh_meta.sjs36
-rw-r--r--browser/base/content/test/general/test_bug462673.html18
-rw-r--r--browser/base/content/test/general/test_bug628179.html9
-rw-r--r--browser/base/content/test/general/test_remoteTroubleshoot.html50
-rw-r--r--browser/base/content/test/general/title_test.svg59
-rw-r--r--browser/base/content/test/general/unknownContentType_file.pif1
-rw-r--r--browser/base/content/test/general/unknownContentType_file.pif^headers^1
-rw-r--r--browser/base/content/test/general/video.oggbin0 -> 285310 bytes
-rw-r--r--browser/base/content/test/general/web_video.html10
-rw-r--r--browser/base/content/test/general/web_video1.ogvbin0 -> 28942 bytes
-rw-r--r--browser/base/content/test/general/web_video1.ogv^headers^3
-rw-r--r--browser/base/content/test/gesture/browser.ini1
-rw-r--r--browser/base/content/test/gesture/browser_gesture_navigation.js233
-rw-r--r--browser/base/content/test/historySwipeAnimation/browser.ini1
-rw-r--r--browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js49
-rw-r--r--browser/base/content/test/keyboard/browser.ini19
-rw-r--r--browser/base/content/test/keyboard/browser_bookmarks_shortcut.js140
-rw-r--r--browser/base/content/test/keyboard/browser_cancel_caret_browsing_in_content.js91
-rw-r--r--browser/base/content/test/keyboard/browser_popup_keyNav.js50
-rw-r--r--browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js336
-rw-r--r--browser/base/content/test/keyboard/browser_toolbarKeyNav.js641
-rw-r--r--browser/base/content/test/keyboard/file_empty.html8
-rw-r--r--browser/base/content/test/keyboard/focusableContent.html1
-rw-r--r--browser/base/content/test/keyboard/head.js55
-rw-r--r--browser/base/content/test/menubar/browser.ini9
-rw-r--r--browser/base/content/test/menubar/browser_file_close_tabs.js60
-rw-r--r--browser/base/content/test/menubar/browser_file_menu_import_wizard.js27
-rw-r--r--browser/base/content/test/menubar/browser_file_share.js136
-rw-r--r--browser/base/content/test/menubar/file_shareurl.html2
-rw-r--r--browser/base/content/test/metaTags/bad_meta_tags.html14
-rw-r--r--browser/base/content/test/metaTags/browser.ini9
-rw-r--r--browser/base/content/test/metaTags/browser_bad_meta_tags.js37
-rw-r--r--browser/base/content/test/metaTags/browser_meta_tags.js57
-rw-r--r--browser/base/content/test/metaTags/head.js19
-rw-r--r--browser/base/content/test/metaTags/meta_tags.html29
-rw-r--r--browser/base/content/test/notificationbox/browser.ini3
-rw-r--r--browser/base/content/test/notificationbox/browser_notification_stacking.js78
-rw-r--r--browser/base/content/test/notificationbox/browser_notificationbar_telemetry.js219
-rw-r--r--browser/base/content/test/notificationbox/browser_tabnotificationbox_switch_tabs.js142
-rw-r--r--browser/base/content/test/outOfProcess/browser.ini15
-rw-r--r--browser/base/content/test/outOfProcess/browser_basic_outofprocess.js149
-rw-r--r--browser/base/content/test/outOfProcess/browser_controller.js127
-rw-r--r--browser/base/content/test/outOfProcess/browser_promisefocus.js262
-rw-r--r--browser/base/content/test/outOfProcess/file_base.html5
-rw-r--r--browser/base/content/test/outOfProcess/file_frame1.html5
-rw-r--r--browser/base/content/test/outOfProcess/file_frame2.html11
-rw-r--r--browser/base/content/test/outOfProcess/file_innerframe.html3
-rw-r--r--browser/base/content/test/outOfProcess/head.js85
-rw-r--r--browser/base/content/test/pageActions/browser.ini7
-rw-r--r--browser/base/content/test/pageActions/browser_PageActions_bookmark.js130
-rw-r--r--browser/base/content/test/pageActions/browser_PageActions_overflow.js257
-rw-r--r--browser/base/content/test/pageActions/browser_PageActions_removeExtension.js338
-rw-r--r--browser/base/content/test/pageActions/head.js163
-rw-r--r--browser/base/content/test/pageStyle/browser.ini16
-rw-r--r--browser/base/content/test/pageStyle/browser_disable_author_style_oop.js100
-rw-r--r--browser/base/content/test/pageStyle/browser_page_style_menu.js174
-rw-r--r--browser/base/content/test/pageStyle/browser_page_style_menu_update.js49
-rw-r--r--browser/base/content/test/pageStyle/head.js30
-rw-r--r--browser/base/content/test/pageStyle/page_style.html8
-rw-r--r--browser/base/content/test/pageStyle/page_style_only_alternates.html5
-rw-r--r--browser/base/content/test/pageStyle/page_style_sample.html45
-rw-r--r--browser/base/content/test/pageStyle/style.css1
-rw-r--r--browser/base/content/test/pageinfo/all_images.html15
-rw-r--r--browser/base/content/test/pageinfo/browser.ini27
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js89
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js31
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_image_info.js57
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_images.js93
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_permissions.js258
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_rtl.js28
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_security.js354
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js49
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js34
-rw-r--r--browser/base/content/test/pageinfo/iframes.html8
-rw-r--r--browser/base/content/test/pageinfo/image.html5
-rw-r--r--browser/base/content/test/pageinfo/svg_image.html11
-rw-r--r--browser/base/content/test/performance/PerfTestHelpers.sys.mjs79
-rw-r--r--browser/base/content/test/performance/StartupContentSubframe.sys.mjs55
-rw-r--r--browser/base/content/test/performance/browser.ini90
-rw-r--r--browser/base/content/test/performance/browser_appmenu.js129
-rw-r--r--browser/base/content/test/performance/browser_panel_vsync.js69
-rw-r--r--browser/base/content/test/performance/browser_preferences_usage.js282
-rw-r--r--browser/base/content/test/performance/browser_startup.js245
-rw-r--r--browser/base/content/test/performance/browser_startup_content.js196
-rw-r--r--browser/base/content/test/performance/browser_startup_content_mainthreadio.js438
-rw-r--r--browser/base/content/test/performance/browser_startup_content_subframe.js150
-rw-r--r--browser/base/content/test/performance/browser_startup_flicker.js85
-rw-r--r--browser/base/content/test/performance/browser_startup_hiddenwindow.js50
-rw-r--r--browser/base/content/test/performance/browser_startup_images.js136
-rw-r--r--browser/base/content/test/performance/browser_startup_mainthreadio.js881
-rw-r--r--browser/base/content/test/performance/browser_startup_syncIPC.js449
-rw-r--r--browser/base/content/test/performance/browser_tabclose.js108
-rw-r--r--browser/base/content/test/performance/browser_tabclose_grow.js91
-rw-r--r--browser/base/content/test/performance/browser_tabdetach.js118
-rw-r--r--browser/base/content/test/performance/browser_tabopen.js201
-rw-r--r--browser/base/content/test/performance/browser_tabopen_squeeze.js100
-rw-r--r--browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js200
-rw-r--r--browser/base/content/test/performance/browser_tabswitch.js123
-rw-r--r--browser/base/content/test/performance/browser_toolbariconcolor_restyles.js65
-rw-r--r--browser/base/content/test/performance/browser_urlbar_keyed_search.js27
-rw-r--r--browser/base/content/test/performance/browser_urlbar_search.js27
-rw-r--r--browser/base/content/test/performance/browser_vsync_accessibility.js20
-rw-r--r--browser/base/content/test/performance/browser_window_resize.js132
-rw-r--r--browser/base/content/test/performance/browser_windowclose.js58
-rw-r--r--browser/base/content/test/performance/browser_windowopen.js182
-rw-r--r--browser/base/content/test/performance/file_empty.html1
-rw-r--r--browser/base/content/test/performance/head.js971
-rw-r--r--browser/base/content/test/performance/hidpi/browser.ini7
-rw-r--r--browser/base/content/test/performance/io/browser.ini33
-rw-r--r--browser/base/content/test/performance/lowdpi/browser.ini8
-rw-r--r--browser/base/content/test/performance/moz.build17
-rw-r--r--browser/base/content/test/performance/triage.json62
-rw-r--r--browser/base/content/test/perftest.ini1
-rw-r--r--browser/base/content/test/perftest_browser_xhtml_dom.js85
-rw-r--r--browser/base/content/test/permissions/browser.ini41
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_blocked.html14
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_blocked.js357
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs36
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_js.html16
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_muted.html14
-rw-r--r--browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js383
-rw-r--r--browser/base/content/test/permissions/browser_canvas_rfp_exclusion.js194
-rw-r--r--browser/base/content/test/permissions/browser_permission_delegate_geo.js279
-rw-r--r--browser/base/content/test/permissions/browser_permissions.js569
-rw-r--r--browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js46
-rw-r--r--browser/base/content/test/permissions/browser_permissions_handling_user_input.js99
-rw-r--r--browser/base/content/test/permissions/browser_permissions_postPrompt.js104
-rw-r--r--browser/base/content/test/permissions/browser_reservedkey.js312
-rw-r--r--browser/base/content/test/permissions/browser_site_scoped_permissions.js106
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions.js118
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions_expiry.js208
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions_navigation.js239
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions_tabs.js148
-rw-r--r--browser/base/content/test/permissions/dummy.js1
-rw-r--r--browser/base/content/test/permissions/empty.html8
-rw-r--r--browser/base/content/test/permissions/head.js28
-rw-r--r--browser/base/content/test/permissions/permissions.html49
-rw-r--r--browser/base/content/test/permissions/temporary_permissions_frame.html12
-rw-r--r--browser/base/content/test/permissions/temporary_permissions_subframe.html11
-rw-r--r--browser/base/content/test/plugins/browser.ini14
-rw-r--r--browser/base/content/test/plugins/browser_bug797677.js45
-rw-r--r--browser/base/content/test/plugins/browser_enable_DRM_prompt.js232
-rw-r--r--browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js63
-rw-r--r--browser/base/content/test/plugins/browser_private_browsing_eme_persistent_state.js59
-rw-r--r--browser/base/content/test/plugins/empty_file.html9
-rw-r--r--browser/base/content/test/plugins/head.js205
-rw-r--r--browser/base/content/test/plugins/plugin_bug797677.html5
-rw-r--r--browser/base/content/test/plugins/plugin_test.html9
-rw-r--r--browser/base/content/test/popupNotifications/browser.ini38
-rw-r--r--browser/base/content/test/popupNotifications/browser_displayURI.js159
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification.js394
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_2.js315
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_3.js377
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_4.js290
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_5.js501
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js44
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js248
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js36
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js44
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js273
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js64
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js288
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js296
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js57
-rw-r--r--browser/base/content/test/popupNotifications/browser_reshow_in_background.js72
-rw-r--r--browser/base/content/test/popupNotifications/head.js367
-rw-r--r--browser/base/content/test/popups/browser.ini69
-rw-r--r--browser/base/content/test/popups/browser_popupUI.js192
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker.js155
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker_frames.js100
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker_identity_block.js242
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker_iframes.js186
-rw-r--r--browser/base/content/test/popups/browser_popup_close_main_window.js84
-rw-r--r--browser/base/content/test/popups/browser_popup_frames.js128
-rw-r--r--browser/base/content/test/popups/browser_popup_inner_outer_size.js120
-rw-r--r--browser/base/content/test/popups/browser_popup_linux_move.js56
-rw-r--r--browser/base/content/test/popups/browser_popup_linux_resize.js53
-rw-r--r--browser/base/content/test/popups/browser_popup_move.js6
-rw-r--r--browser/base/content/test/popups/browser_popup_move_instant.js6
-rw-r--r--browser/base/content/test/popups/browser_popup_new_window_resize.js51
-rw-r--r--browser/base/content/test/popups/browser_popup_new_window_size.js90
-rw-r--r--browser/base/content/test/popups/browser_popup_resize.js6
-rw-r--r--browser/base/content/test/popups/browser_popup_resize_instant.js6
-rw-r--r--browser/base/content/test/popups/browser_popup_resize_repeat.js6
-rw-r--r--browser/base/content/test/popups/browser_popup_resize_repeat_instant.js6
-rw-r--r--browser/base/content/test/popups/browser_popup_resize_revert.js6
-rw-r--r--browser/base/content/test/popups/browser_popup_resize_revert_instant.js6
-rw-r--r--browser/base/content/test/popups/head.js574
-rw-r--r--browser/base/content/test/popups/popup_blocker.html13
-rw-r--r--browser/base/content/test/popups/popup_blocker2.html10
-rw-r--r--browser/base/content/test/popups/popup_blocker_10_popups.html14
-rw-r--r--browser/base/content/test/popups/popup_blocker_a.html1
-rw-r--r--browser/base/content/test/popups/popup_blocker_b.html1
-rw-r--r--browser/base/content/test/popups/popup_blocker_frame.html27
-rw-r--r--browser/base/content/test/popups/popup_size.html16
-rw-r--r--browser/base/content/test/protectionsUI/benignPage.html18
-rw-r--r--browser/base/content/test/protectionsUI/browser.ini63
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI.js713
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_3.js224
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_background_tabs.js74
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_categories.js300
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cookie_banner.js475
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js537
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cryptominers.js306
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_email_trackers_subview.js179
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js39
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js303
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_icon_state.js223
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js95
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js155
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js175
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js404
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js124
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js321
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_state.js405
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_state_reset.js129
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_subview_shim.js403
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js89
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js134
-rw-r--r--browser/base/content/test/protectionsUI/containerPage.html6
-rw-r--r--browser/base/content/test/protectionsUI/cookiePage.html13
-rw-r--r--browser/base/content/test/protectionsUI/cookieServer.sjs24
-rw-r--r--browser/base/content/test/protectionsUI/cookieSetterPage.html6
-rw-r--r--browser/base/content/test/protectionsUI/emailTrackingPage.html12
-rw-r--r--browser/base/content/test/protectionsUI/embeddedPage.html6
-rw-r--r--browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html17
-rw-r--r--browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js2
-rw-r--r--browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^1
-rw-r--r--browser/base/content/test/protectionsUI/head.js221
-rw-r--r--browser/base/content/test/protectionsUI/sandboxed.html12
-rw-r--r--browser/base/content/test/protectionsUI/sandboxed.html^headers^1
-rw-r--r--browser/base/content/test/protectionsUI/trackingAPI.js77
-rw-r--r--browser/base/content/test/protectionsUI/trackingPage.html13
-rw-r--r--browser/base/content/test/referrer/browser.ini35
-rw-r--r--browser/base/content/test/referrer/browser_referrer_click_pinned_tab.js82
-rw-r--r--browser/base/content/test/referrer/browser_referrer_middle_click.js25
-rw-r--r--browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js33
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js80
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js43
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js81
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_private.js33
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js27
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_window.js28
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js39
-rw-r--r--browser/base/content/test/referrer/browser_referrer_simple_click.js27
-rw-r--r--browser/base/content/test/referrer/file_referrer_policyserver.sjs41
-rw-r--r--browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs41
-rw-r--r--browser/base/content/test/referrer/file_referrer_testserver.sjs30
-rw-r--r--browser/base/content/test/referrer/head.js311
-rw-r--r--browser/base/content/test/sanitize/browser.ini19
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission.js1
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js101
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_containers.js1
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js290
-rw-r--r--browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js71
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-cookie-exceptions.js274
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-formhistory.js28
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-history.js132
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-offlineData.js255
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js28
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js37
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-timespans.js1194
-rw-r--r--browser/base/content/test/sanitize/browser_sanitizeDialog.js833
-rw-r--r--browser/base/content/test/sanitize/dummy.js0
-rw-r--r--browser/base/content/test/sanitize/dummy_page.html9
-rw-r--r--browser/base/content/test/sanitize/head.js329
-rw-r--r--browser/base/content/test/sidebar/browser.ini8
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_adopt.js74
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js111
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_keys.js108
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_move.js72
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_persist.js37
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_switcher.js64
-rw-r--r--browser/base/content/test/siteIdentity/browser.ini152
-rw-r--r--browser/base/content/test/siteIdentity/browser_about_blank_same_document_tabswitch.js79
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug1045809.js105
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug822367.js254
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug902156.js171
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug906190.js340
-rw-r--r--browser/base/content/test/siteIdentity/browser_check_identity_state.js882
-rw-r--r--browser/base/content/test/siteIdentity/browser_check_identity_state_pdf.js77
-rw-r--r--browser/base/content/test/siteIdentity/browser_csp_block_all_mixedcontent.js60
-rw-r--r--browser/base/content/test/siteIdentity/browser_deprecatedTLSVersions.js94
-rw-r--r--browser/base/content/test/siteIdentity/browser_geolocation_indicator.js381
-rw-r--r--browser/base/content/test/siteIdentity/browser_getSecurityInfo.js35
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js52
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityBlock_focus.js126
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityIcon_img_url.js148
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_HttpsOnlyMode.js191
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js245
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData_extensions.js80
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_custom_roots.js82
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_focus.js120
-rw-r--r--browser/base/content/test/siteIdentity/browser_identity_UI.js192
-rw-r--r--browser/base/content/test/siteIdentity/browser_iframe_navigation.js108
-rw-r--r--browser/base/content/test/siteIdentity/browser_ignore_same_page_navigation.js50
-rw-r--r--browser/base/content/test/siteIdentity/browser_mcb_redirect.js360
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js37
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixedContentFromOnunload.js68
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixed_content_cert_override.js69
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixed_content_with_navigation.js131
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixed_passive_content_indicator.js18
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixedcontent_securityflags.js71
-rw-r--r--browser/base/content/test/siteIdentity/browser_navigation_failures.js166
-rw-r--r--browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js88
-rw-r--r--browser/base/content/test/siteIdentity/browser_no_mcb_for_onions.js41
-rw-r--r--browser/base/content/test/siteIdentity/browser_no_mcb_on_http_site.js133
-rw-r--r--browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js185
-rw-r--r--browser/base/content/test/siteIdentity/browser_session_store_pageproxystate.js92
-rw-r--r--browser/base/content/test/siteIdentity/browser_tab_sharing_state.js96
-rw-r--r--browser/base/content/test/siteIdentity/dummy_iframe_page.html10
-rw-r--r--browser/base/content/test/siteIdentity/dummy_page.html10
-rw-r--r--browser/base/content/test/siteIdentity/file_bug1045809_1.html7
-rw-r--r--browser/base/content/test/siteIdentity/file_bug1045809_2.html7
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_1.html18
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_1.js1
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_2.html16
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_3.html27
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_4.html18
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_4.js2
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_4B.html18
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_5.html23
-rw-r--r--browser/base/content/test/siteIdentity/file_bug822367_6.html16
-rw-r--r--browser/base/content/test/siteIdentity/file_bug902156.js6
-rw-r--r--browser/base/content/test/siteIdentity/file_bug902156_1.html15
-rw-r--r--browser/base/content/test/siteIdentity/file_bug902156_2.html17
-rw-r--r--browser/base/content/test/siteIdentity/file_bug902156_3.html15
-rw-r--r--browser/base/content/test/siteIdentity/file_bug906190.js6
-rw-r--r--browser/base/content/test/siteIdentity/file_bug906190.sjs18
-rw-r--r--browser/base/content/test/siteIdentity/file_bug906190_1.html15
-rw-r--r--browser/base/content/test/siteIdentity/file_bug906190_2.html15
-rw-r--r--browser/base/content/test/siteIdentity/file_bug906190_3_4.html14
-rw-r--r--browser/base/content/test/siteIdentity/file_bug906190_redirected.html15
-rw-r--r--browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.html11
-rw-r--r--browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.js3
-rw-r--r--browser/base/content/test/siteIdentity/file_mixedContentFramesOnHttp.html14
-rw-r--r--browser/base/content/test/siteIdentity/file_mixedContentFromOnunload.html18
-rw-r--r--browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test1.html14
-rw-r--r--browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test2.html15
-rw-r--r--browser/base/content/test/siteIdentity/file_mixedPassiveContent.html13
-rw-r--r--browser/base/content/test/siteIdentity/file_pdf.pdf12
-rw-r--r--browser/base/content/test/siteIdentity/file_pdf_blob.html18
-rw-r--r--browser/base/content/test/siteIdentity/head.js435
-rw-r--r--browser/base/content/test/siteIdentity/iframe_navigation.html44
-rw-r--r--browser/base/content/test/siteIdentity/insecure_opener.html9
-rw-r--r--browser/base/content/test/siteIdentity/open-self-from-frame.html6
-rw-r--r--browser/base/content/test/siteIdentity/simple_mixed_passive.html1
-rw-r--r--browser/base/content/test/siteIdentity/test-mixedcontent-securityerrors.html21
-rw-r--r--browser/base/content/test/siteIdentity/test_mcb_double_redirect_image.html23
-rw-r--r--browser/base/content/test/siteIdentity/test_mcb_redirect.html15
-rw-r--r--browser/base/content/test/siteIdentity/test_mcb_redirect.js5
-rw-r--r--browser/base/content/test/siteIdentity/test_mcb_redirect.sjs29
-rw-r--r--browser/base/content/test/siteIdentity/test_mcb_redirect_image.html23
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_for_loopback.html56
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_for_onions.html29
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css11
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html44
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css1
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html45
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css3
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html44
-rw-r--r--browser/base/content/test/startup/browser.ini2
-rw-r--r--browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js136
-rw-r--r--browser/base/content/test/static/browser.ini22
-rw-r--r--browser/base/content/test/static/browser_all_files_referenced.js1093
-rw-r--r--browser/base/content/test/static/browser_misused_characters_in_strings.js276
-rw-r--r--browser/base/content/test/static/browser_parsable_css.js590
-rw-r--r--browser/base/content/test/static/browser_parsable_script.js167
-rw-r--r--browser/base/content/test/static/browser_sentence_case_strings.js279
-rw-r--r--browser/base/content/test/static/browser_title_case_menus.js158
-rw-r--r--browser/base/content/test/static/bug1262648_string_with_newlines.dtd3
-rw-r--r--browser/base/content/test/static/dummy_page.html9
-rw-r--r--browser/base/content/test/static/head.js177
-rw-r--r--browser/base/content/test/statuspanel/browser.ini7
-rw-r--r--browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js28
-rw-r--r--browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js29
-rw-r--r--browser/base/content/test/statuspanel/head.js58
-rw-r--r--browser/base/content/test/sync/browser.ini13
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendpage.js465
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendtab.js362
-rw-r--r--browser/base/content/test/sync/browser_fxa_badge.js70
-rw-r--r--browser/base/content/test/sync/browser_fxa_web_channel.html158
-rw-r--r--browser/base/content/test/sync/browser_fxa_web_channel.js282
-rw-r--r--browser/base/content/test/sync/browser_sync.js751
-rw-r--r--browser/base/content/test/sync/browser_synced_tabs_view.js76
-rw-r--r--browser/base/content/test/sync/head.js34
-rw-r--r--browser/base/content/test/tabMediaIndicator/almostSilentAudioTrack.webmbin0 -> 1699661 bytes
-rw-r--r--browser/base/content/test/tabMediaIndicator/audio.oggbin0 -> 14293 bytes
-rw-r--r--browser/base/content/test/tabMediaIndicator/audioEndedDuringPlaying.webmbin0 -> 109366 bytes
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser.ini33
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_destroy_iframe.js50
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_mediaPlayback.js42
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_mediaPlayback_mute.js118
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_mediaplayback_audibility_change.js258
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_mute.js19
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_mute2.js32
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_mute_webAudio.js75
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_sound_indicator_silent_video.js88
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_webAudio_hideSoundPlayingIcon.js60
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_webAudio_silentData.js57
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_webaudio_audibility_change.js172
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_almostSilentAudioTrack.html18
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_autoplay_media.html9
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_empty.html8
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_mediaPlayback.html9
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_mediaPlayback2.html14
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame.html2
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame2.html2
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_silentAudioTrack.html18
-rw-r--r--browser/base/content/test/tabMediaIndicator/file_webAudio.html29
-rw-r--r--browser/base/content/test/tabMediaIndicator/gizmo.mp4bin0 -> 455255 bytes
-rw-r--r--browser/base/content/test/tabMediaIndicator/head.js158
-rw-r--r--browser/base/content/test/tabMediaIndicator/noaudio.webmbin0 -> 105755 bytes
-rw-r--r--browser/base/content/test/tabMediaIndicator/silentAudioTrack.webmbin0 -> 224800 bytes
-rw-r--r--browser/base/content/test/tabPrompts/auth-route.sjs28
-rw-r--r--browser/base/content/test/tabPrompts/browser.ini30
-rw-r--r--browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js60
-rw-r--r--browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js232
-rw-r--r--browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js95
-rw-r--r--browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js93
-rw-r--r--browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js75
-rw-r--r--browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js53
-rw-r--r--browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js141
-rw-r--r--browser/base/content/test/tabPrompts/browser_contentOrigins.js217
-rw-r--r--browser/base/content/test/tabPrompts/browser_multiplePrompts.js171
-rw-r--r--browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js262
-rw-r--r--browser/base/content/test/tabPrompts/browser_promptFocus.js170
-rw-r--r--browser/base/content/test/tabPrompts/browser_prompt_closed_window.js40
-rw-r--r--browser/base/content/test/tabPrompts/browser_switchTabPermissionPrompt.js41
-rw-r--r--browser/base/content/test/tabPrompts/browser_windowPrompt.js259
-rw-r--r--browser/base/content/test/tabPrompts/file_beforeunload_stop.html8
-rw-r--r--browser/base/content/test/tabPrompts/openPromptOffTimeout.html10
-rw-r--r--browser/base/content/test/tabPrompts/redirect-crossDomain-tabTitle-update.html15
-rw-r--r--browser/base/content/test/tabPrompts/redirect-crossDomain.html13
-rw-r--r--browser/base/content/test/tabPrompts/redirect-sameDomain.html13
-rw-r--r--browser/base/content/test/tabcrashed/browser.ini21
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired.ini19
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired_basic.js31
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_false-positive.js35
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_mismatch.js56
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_no-platform-ini.js50
-rw-r--r--browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js183
-rw-r--r--browser/base/content/test/tabcrashed/browser_launchFail.js59
-rw-r--r--browser/base/content/test/tabcrashed/browser_multipleCrashedTabs.js136
-rw-r--r--browser/base/content/test/tabcrashed/browser_noPermanentKey.js41
-rw-r--r--browser/base/content/test/tabcrashed/browser_printpreview_crash.js83
-rw-r--r--browser/base/content/test/tabcrashed/browser_showForm.js44
-rw-r--r--browser/base/content/test/tabcrashed/browser_shown.js150
-rw-r--r--browser/base/content/test/tabcrashed/browser_shownRestartRequired.js121
-rw-r--r--browser/base/content/test/tabcrashed/browser_withoutDump.js42
-rw-r--r--browser/base/content/test/tabcrashed/file_contains_emptyiframe.html9
-rw-r--r--browser/base/content/test/tabcrashed/file_iframe.html9
-rw-r--r--browser/base/content/test/tabcrashed/head.js238
-rw-r--r--browser/base/content/test/tabdialogs/browser.ini19
-rw-r--r--browser/base/content/test/tabdialogs/browser_multiple_dialog_navigation.js61
-rw-r--r--browser/base/content/test/tabdialogs/browser_subdialog_esc.js122
-rw-r--r--browser/base/content/test/tabdialogs/browser_tabdialogbox_content_prompts.js179
-rw-r--r--browser/base/content/test/tabdialogs/browser_tabdialogbox_focus.js212
-rw-r--r--browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js174
-rw-r--r--browser/base/content/test/tabdialogs/loadDelayedReply.sjs22
-rw-r--r--browser/base/content/test/tabdialogs/subdialog.xhtml46
-rw-r--r--browser/base/content/test/tabdialogs/test_page.html10
-rw-r--r--browser/base/content/test/tabs/204.sjs3
-rw-r--r--browser/base/content/test/tabs/blank.html2
-rw-r--r--browser/base/content/test/tabs/browser.ini211
-rw-r--r--browser/base/content/test/tabs/browser_addAdjacentNewTab.js55
-rw-r--r--browser/base/content/test/tabs/browser_addTab_index.js8
-rw-r--r--browser/base/content/test/tabs/browser_adoptTab_failure.js107
-rw-r--r--browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js40
-rw-r--r--browser/base/content/test/tabs/browser_audioTabIcon.js676
-rw-r--r--browser/base/content/test/tabs/browser_bfcache_exemption_about_pages.js176
-rw-r--r--browser/base/content/test/tabs/browser_bug580956.js25
-rw-r--r--browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js56
-rw-r--r--browser/base/content/test/tabs/browser_close_during_beforeunload.js46
-rw-r--r--browser/base/content/test/tabs/browser_close_tab_by_dblclick.js35
-rw-r--r--browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js60
-rw-r--r--browser/base/content/test/tabs/browser_dont_process_switch_204.js56
-rw-r--r--browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js208
-rw-r--r--browser/base/content/test/tabs/browser_e10s_about_process.js174
-rw-r--r--browser/base/content/test/tabs/browser_e10s_chrome_process.js136
-rw-r--r--browser/base/content/test/tabs/browser_e10s_javascript.js19
-rw-r--r--browser/base/content/test/tabs/browser_e10s_mozillaweb_process.js52
-rw-r--r--browser/base/content/test/tabs/browser_e10s_switchbrowser.js490
-rw-r--r--browser/base/content/test/tabs/browser_file_to_http_named_popup.js60
-rw-r--r--browser/base/content/test/tabs/browser_file_to_http_script_closable.js43
-rw-r--r--browser/base/content/test/tabs/browser_hiddentab_contextmenu.js34
-rw-r--r--browser/base/content/test/tabs/browser_lazy_tab_browser_events.js157
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.js139
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_new_window.js54
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js199
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js84
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js86
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js156
-rw-r--r--browser/base/content/test/tabs/browser_long_data_url_label_truncation.js78
-rw-r--r--browser/base/content/test/tabs/browser_middle_click_new_tab_button_loads_clipboard.js255
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_active_tab_selected_by_default.js52
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js81
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_clear_selection_when_tab_switch.js33
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_close.js192
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js122
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_left.js131
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js113
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_close_using_shortcuts.js64
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js51
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js74
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js136
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_event.js220
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_move.js192
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js118
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js129
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js336
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js143
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_pin_unpin.js75
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_play.js254
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_reload.js82
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js133
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js65
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js60
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js159
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift_and_Ctrl.js75
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js147
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js72
-rw-r--r--browser/base/content/test/tabs/browser_navigatePinnedTab.js71
-rw-r--r--browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js21
-rw-r--r--browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js177
-rw-r--r--browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js37
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js230
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_insert_position.js288
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_url.js29
-rw-r--r--browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js41
-rw-r--r--browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js28
-rw-r--r--browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js56
-rw-r--r--browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js106
-rw-r--r--browser/base/content/test/tabs/browser_origin_attrs_rel.js281
-rw-r--r--browser/base/content/test/tabs/browser_originalURI.js181
-rw-r--r--browser/base/content/test/tabs/browser_overflowScroll.js111
-rw-r--r--browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js156
-rw-r--r--browser/base/content/test/tabs/browser_pinnedTabs.js97
-rw-r--r--browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js58
-rw-r--r--browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js72
-rw-r--r--browser/base/content/test/tabs/browser_positional_attributes.js60
-rw-r--r--browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js89
-rw-r--r--browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js212
-rw-r--r--browser/base/content/test/tabs/browser_progress_keyword_search_handling.js91
-rw-r--r--browser/base/content/test/tabs/browser_relatedTabs_reset.js81
-rw-r--r--browser/base/content/test/tabs/browser_reload_deleted_file.js36
-rw-r--r--browser/base/content/test/tabs/browser_removeTabsToTheEnd.js30
-rw-r--r--browser/base/content/test/tabs/browser_removeTabsToTheStart.js35
-rw-r--r--browser/base/content/test/tabs/browser_removeTabs_order.js40
-rw-r--r--browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js115
-rw-r--r--browser/base/content/test/tabs/browser_replacewithwindow_commands.js42
-rw-r--r--browser/base/content/test/tabs/browser_switch_by_scrolling.js51
-rw-r--r--browser/base/content/test/tabs/browser_tabCloseProbes.js112
-rw-r--r--browser/base/content/test/tabs/browser_tabCloseSpacer.js91
-rw-r--r--browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js64
-rw-r--r--browser/base/content/test/tabs/browser_tabReorder.js64
-rw-r--r--browser/base/content/test/tabs/browser_tabReorder_overflow.js62
-rw-r--r--browser/base/content/test/tabs/browser_tabSpinnerProbe.js101
-rw-r--r--browser/base/content/test/tabs/browser_tabSuccessors.js131
-rw-r--r--browser/base/content/test/tabs/browser_tab_a11y_description.js74
-rw-r--r--browser/base/content/test/tabs/browser_tab_label_during_reload.js41
-rw-r--r--browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js30
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_close.js84
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_drag.js259
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js38
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_visibility.js55
-rw-r--r--browser/base/content/test/tabs/browser_tab_move_to_new_window_reload.js51
-rw-r--r--browser/base/content/test/tabs/browser_tab_play.js216
-rw-r--r--browser/base/content/test/tabs/browser_tab_tooltips.js108
-rw-r--r--browser/base/content/test/tabs/browser_tabswitch_contextmenu.js45
-rw-r--r--browser/base/content/test/tabs/browser_tabswitch_select.js63
-rw-r--r--browser/base/content/test/tabs/browser_tabswitch_updatecommands.js28
-rw-r--r--browser/base/content/test/tabs/browser_tabswitch_window_focus.js78
-rw-r--r--browser/base/content/test/tabs/browser_undo_close_tabs.js171
-rw-r--r--browser/base/content/test/tabs/browser_undo_close_tabs_at_start.js74
-rw-r--r--browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js53
-rw-r--r--browser/base/content/test/tabs/browser_visibleTabs_bookmarkAllTabs.js64
-rw-r--r--browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js115
-rw-r--r--browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js255
-rw-r--r--browser/base/content/test/tabs/dummy_page.html9
-rw-r--r--browser/base/content/test/tabs/file_about_child.html10
-rw-r--r--browser/base/content/test/tabs/file_about_parent.html10
-rw-r--r--browser/base/content/test/tabs/file_about_srcdoc.html9
-rw-r--r--browser/base/content/test/tabs/file_anchor_elements.html12
-rw-r--r--browser/base/content/test/tabs/file_mediaPlayback.html2
-rw-r--r--browser/base/content/test/tabs/file_new_tab_page.html9
-rw-r--r--browser/base/content/test/tabs/file_rel_opener_noopener.html12
-rw-r--r--browser/base/content/test/tabs/head.js564
-rw-r--r--browser/base/content/test/tabs/helper_origin_attrs_testing.js158
-rw-r--r--browser/base/content/test/tabs/link_in_tab_title_and_url_prefilled.html30
-rw-r--r--browser/base/content/test/tabs/open_window_in_new_tab.html15
-rw-r--r--browser/base/content/test/tabs/page_with_iframe.html12
-rw-r--r--browser/base/content/test/tabs/redirect_via_header.html9
-rw-r--r--browser/base/content/test/tabs/redirect_via_header.html^headers^2
-rw-r--r--browser/base/content/test/tabs/redirect_via_meta_tag.html13
-rw-r--r--browser/base/content/test/tabs/request-timeout.sjs8
-rw-r--r--browser/base/content/test/tabs/tab_that_closes.html15
-rw-r--r--browser/base/content/test/tabs/test_bug1358314.html10
-rw-r--r--browser/base/content/test/tabs/test_process_flags_chrome.html10
-rw-r--r--browser/base/content/test/tabs/wait-a-bit.sjs23
-rw-r--r--browser/base/content/test/touch/browser.ini4
-rw-r--r--browser/base/content/test/touch/browser_menu_touch.js198
-rw-r--r--browser/base/content/test/utilityOverlay/browser.ini2
-rw-r--r--browser/base/content/test/utilityOverlay/browser_openWebLinkIn.js185
-rw-r--r--browser/base/content/test/webextensions/.eslintrc.js7
-rw-r--r--browser/base/content/test/webextensions/browser.ini33
-rw-r--r--browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js26
-rw-r--r--browser/base/content/test/webextensions/browser_extension_sideloading.js404
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background.js282
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js121
-rw-r--r--browser/base/content/test/webextensions/browser_legacy_webext.xpibin0 -> 4243 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_dismiss.js112
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_installTrigger.js26
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_local_file.js43
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js18
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_optional.js52
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_pointerevent.js53
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_unsigned.js63
-rw-r--r--browser/base/content/test/webextensions/browser_update_checkForUpdates.js17
-rw-r--r--browser/base/content/test/webextensions/browser_update_interactive_noprompt.js77
-rw-r--r--browser/base/content/test/webextensions/browser_webext_nopermissions.xpibin0 -> 4273 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_permissions.xpibin0 -> 16602 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_unsigned.xpibin0 -> 12620 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update.json70
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update1.xpibin0 -> 4271 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update2.xpibin0 -> 4291 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_icon1.xpibin0 -> 16545 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_icon2.xpibin0 -> 16564 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_origins1.xpibin0 -> 268 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_origins2.xpibin0 -> 275 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_perms1.xpibin0 -> 4273 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_perms2.xpibin0 -> 4282 bytes
-rw-r--r--browser/base/content/test/webextensions/file_install_extensions.html19
-rw-r--r--browser/base/content/test/webextensions/head.js650
-rw-r--r--browser/base/content/test/webrtc/browser.ini118
-rw-r--r--browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js484
-rw-r--r--browser/base/content/test/webrtc/browser_device_controls_menus.js55
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media.js949
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js106
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js82
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js209
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js388
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js775
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js798
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js251
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js517
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js999
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js383
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js949
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js73
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js100
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js666
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js309
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js47
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js108
-rw-r--r--browser/base/content/test/webrtc/browser_devices_select_audio_output.js233
-rw-r--r--browser/base/content/test/webrtc/browser_global_mute_toggles.js293
-rw-r--r--browser/base/content/test/webrtc/browser_indicator_popuphiding.js50
-rw-r--r--browser/base/content/test/webrtc/browser_notification_silencing.js231
-rw-r--r--browser/base/content/test/webrtc/browser_stop_sharing_button.js175
-rw-r--r--browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js215
-rw-r--r--browser/base/content/test/webrtc/browser_tab_switch_warning.js538
-rw-r--r--browser/base/content/test/webrtc/browser_webrtc_hooks.js371
-rw-r--r--browser/base/content/test/webrtc/get_user_media.html124
-rw-r--r--browser/base/content/test/webrtc/get_user_media2.html107
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_frame.html98
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html71
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html12
-rw-r--r--browser/base/content/test/webrtc/gracePeriod/browser.ini15
-rw-r--r--browser/base/content/test/webrtc/head.js1338
-rw-r--r--browser/base/content/test/webrtc/legacyIndicator/browser.ini63
-rw-r--r--browser/base/content/test/webrtc/peerconnection_connect.html39
-rw-r--r--browser/base/content/test/webrtc/single_peerconnection.html23
-rw-r--r--browser/base/content/test/zoom/browser.ini34
-rw-r--r--browser/base/content/test/zoom/browser_background_link_zoom_reset.js45
-rw-r--r--browser/base/content/test/zoom/browser_background_zoom.js115
-rw-r--r--browser/base/content/test/zoom/browser_default_zoom.js149
-rw-r--r--browser/base/content/test/zoom/browser_default_zoom_fission.js114
-rw-r--r--browser/base/content/test/zoom/browser_default_zoom_multitab.js190
-rw-r--r--browser/base/content/test/zoom/browser_default_zoom_multitab_002.js93
-rw-r--r--browser/base/content/test/zoom/browser_default_zoom_sitespecific.js108
-rw-r--r--browser/base/content/test/zoom/browser_image_zoom_tabswitch.js39
-rw-r--r--browser/base/content/test/zoom/browser_mousewheel_zoom.js72
-rw-r--r--browser/base/content/test/zoom/browser_sitespecific_background_pref.js35
-rw-r--r--browser/base/content/test/zoom/browser_sitespecific_image_zoom.js52
-rw-r--r--browser/base/content/test/zoom/browser_sitespecific_video_zoom.js128
-rw-r--r--browser/base/content/test/zoom/browser_subframe_textzoom.js52
-rw-r--r--browser/base/content/test/zoom/browser_tabswitch_zoom_flicker.js45
-rw-r--r--browser/base/content/test/zoom/browser_tooltip_zoom.js41
-rw-r--r--browser/base/content/test/zoom/browser_zoom_commands.js203
-rw-r--r--browser/base/content/test/zoom/head.js223
-rw-r--r--browser/base/content/test/zoom/zoom_test.html14
1063 files changed, 115388 insertions, 0 deletions
diff --git a/browser/base/content/test/about/POSTSearchEngine.xml b/browser/base/content/test/about/POSTSearchEngine.xml
new file mode 100644
index 0000000000..f2f884cf51
--- /dev/null
+++ b/browser/base/content/test/about/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/base/content/test/about/print_postdata.sjs">
+ <Param name="searchterms" value="{searchTerms}"/>
+ </Url>
+</OpenSearchDescription>
diff --git a/browser/base/content/test/about/browser.ini b/browser/base/content/test/about/browser.ini
new file mode 100644
index 0000000000..ce82ff8006
--- /dev/null
+++ b/browser/base/content/test/about/browser.ini
@@ -0,0 +1,59 @@
+[DEFAULT]
+support-files =
+ head.js
+ print_postdata.sjs
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine.xml
+ slow_loading_page.sjs
+ POSTSearchEngine.xml
+ dummy_page.html
+
+[browser_aboutCertError.js]
+[browser_aboutCertError_clockSkew.js]
+[browser_aboutCertError_exception.js]
+[browser_aboutCertError_mitm.js]
+[browser_aboutCertError_noSubjectAltName.js]
+[browser_aboutCertError_offlineSupport.js]
+[browser_aboutCertError_telemetry.js]
+[browser_aboutDialog_distribution.js]
+[browser_aboutHome_search_POST.js]
+[browser_aboutHome_search_composing.js]
+[browser_aboutHome_search_searchbar.js]
+[browser_aboutHome_search_suggestion.js]
+skip-if =
+ os == "mac"
+ os == "linux" && (!debug || bits == 64)
+ os == 'win' && os_version == '10.0' && bits == 64 && !debug # Bug 1399648, bug 1402502
+[browser_aboutHome_search_telemetry.js]
+[browser_aboutNetError.js]
+[browser_aboutNetError_csp_iframe.js]
+https_first_disabled = true
+support-files =
+ iframe_page_csp.html
+ csp_iframe.sjs
+[browser_aboutNetError_native_fallback.js]
+skip-if =
+ socketprocess_networking
+[browser_aboutNetError_trr.js]
+skip-if =
+ socketprocess_networking
+[browser_aboutNetError_xfo_iframe.js]
+https_first_disabled = true
+support-files =
+ iframe_page_xfo.html
+ xfo_iframe.sjs
+[browser_aboutNewTab_bookmarksToolbar.js]
+[browser_aboutNewTab_bookmarksToolbarEmpty.js]
+skip-if = tsan # Bug 1676326, highly frequent on TSan
+[browser_aboutNewTab_bookmarksToolbarNewWindow.js]
+[browser_aboutNewTab_bookmarksToolbarPrefs.js]
+[browser_aboutStopReload.js]
+[browser_aboutSupport.js]
+skip-if =
+ os == 'linux' && bits == 64 && asan && !debug # Bug 1713368
+[browser_aboutSupport_newtab_security_state.js]
+[browser_aboutSupport_places.js]
+skip-if = os == 'android'
+[browser_bug435325.js]
+skip-if = verify && !debug && os == 'mac'
+[browser_bug633691.js]
diff --git a/browser/base/content/test/about/browser_aboutCertError.js b/browser/base/content/test/about/browser_aboutCertError.js
new file mode 100644
index 0000000000..7f1f8149fa
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError.js
@@ -0,0 +1,548 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This is testing the aboutCertError page (Bug 1207107).
+
+const GOOD_PAGE = "https://example.com/";
+const GOOD_PAGE_2 = "https://example.org/";
+const BAD_CERT = "https://expired.example.com/";
+const UNKNOWN_ISSUER = "https://self-signed.example.com ";
+const BAD_STS_CERT =
+ "https://badchain.include-subdomains.pinning.example.com:443";
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+add_task(async function checkReturnToAboutHome() {
+ info(
+ "Loading a bad cert page directly and making sure 'return to previous page' goes to about:home"
+ );
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ is(browser.webNavigation.canGoBack, false, "!webNavigation.canGoBack");
+ is(
+ browser.webNavigation.canGoForward,
+ false,
+ "!webNavigation.canGoForward"
+ );
+
+ // Populate the shistory entries manually, since it happens asynchronously
+ // and the following tests will be too soon otherwise.
+ await TabStateFlusher.flush(browser);
+ let { entries } = JSON.parse(SessionStore.getTabState(tab));
+ is(entries.length, 1, "there is one shistory entry");
+
+ info("Clicking the go back button on about:certerror");
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ "about:home"
+ );
+ await SpecialPowers.spawn(bc, [useFrame], async function (subFrame) {
+ let returnButton = content.document.getElementById("returnButton");
+ if (!subFrame) {
+ if (!Services.focus.focusedElement == returnButton) {
+ await ContentTaskUtils.waitForEvent(returnButton, "focus");
+ }
+ Assert.ok(true, "returnButton has focus");
+ }
+ // Note that going back to about:newtab might cause a process flip, if
+ // the browser is configured to run about:newtab in its own special
+ // content process.
+ returnButton.click();
+ });
+
+ await locationChangePromise;
+
+ is(browser.webNavigation.canGoBack, true, "webNavigation.canGoBack");
+ is(
+ browser.webNavigation.canGoForward,
+ false,
+ "!webNavigation.canGoForward"
+ );
+ is(gBrowser.currentURI.spec, "about:home", "Went back");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkReturnToPreviousPage() {
+ info(
+ "Loading a bad cert page and making sure 'return to previous page' goes back"
+ );
+ for (let useFrame of [false, true]) {
+ let tab;
+ let browser;
+ if (useFrame) {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, GOOD_PAGE);
+ browser = tab.linkedBrowser;
+
+ BrowserTestUtils.loadURIString(browser, GOOD_PAGE_2);
+ await BrowserTestUtils.browserLoaded(browser, false, GOOD_PAGE_2);
+ await injectErrorPageFrame(tab, BAD_CERT);
+ } else {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, GOOD_PAGE);
+ browser = gBrowser.selectedBrowser;
+
+ info("Loading and waiting for the cert error");
+ let certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ BrowserTestUtils.loadURIString(browser, BAD_CERT);
+ await certErrorLoaded;
+ }
+
+ is(browser.webNavigation.canGoBack, true, "webNavigation.canGoBack");
+ is(
+ browser.webNavigation.canGoForward,
+ false,
+ "!webNavigation.canGoForward"
+ );
+
+ // Populate the shistory entries manually, since it happens asynchronously
+ // and the following tests will be too soon otherwise.
+ await TabStateFlusher.flush(browser);
+ let { entries } = JSON.parse(SessionStore.getTabState(tab));
+ is(entries.length, 2, "there are two shistory entries");
+
+ info("Clicking the go back button on about:certerror");
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+ await SpecialPowers.spawn(bc, [useFrame], async function (subFrame) {
+ let returnButton = content.document.getElementById("returnButton");
+ returnButton.click();
+ });
+ await pageShownPromise;
+
+ is(browser.webNavigation.canGoBack, false, "!webNavigation.canGoBack");
+ is(browser.webNavigation.canGoForward, true, "webNavigation.canGoForward");
+ is(gBrowser.currentURI.spec, GOOD_PAGE, "Went back");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+// This checks that the appinfo.appBuildID starts with a date string,
+// which is required for the misconfigured system time check.
+add_task(async function checkAppBuildIDIsDate() {
+ let appBuildID = Services.appinfo.appBuildID;
+ let year = parseInt(appBuildID.substr(0, 4), 10);
+ let month = parseInt(appBuildID.substr(4, 2), 10);
+ let day = parseInt(appBuildID.substr(6, 2), 10);
+
+ ok(year >= 2016 && year <= 2100, "appBuildID contains a valid year");
+ ok(month >= 1 && month <= 12, "appBuildID contains a valid month");
+ ok(day >= 1 && day <= 31, "appBuildID contains a valid day");
+});
+
+add_task(async function checkAdvancedDetails() {
+ info(
+ "Loading a bad cert page and verifying the main error and advanced details section"
+ );
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ let message = await SpecialPowers.spawn(bc, [], async function () {
+ let doc = content.document;
+
+ const shortDesc = doc.getElementById("errorShortDesc");
+ const sdArgs = JSON.parse(shortDesc.dataset.l10nArgs);
+ is(
+ sdArgs.hostname,
+ "expired.example.com",
+ "Should list hostname in error message."
+ );
+
+ Assert.ok(
+ doc.getElementById("certificateErrorDebugInformation").hidden,
+ "Debug info is initially hidden"
+ );
+
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ Assert.ok(
+ !exceptionButton.disabled,
+ "Exception button is not disabled by default."
+ );
+
+ let advancedButton = doc.getElementById("advancedButton");
+ advancedButton.click();
+
+ // Wait until fluent sets the errorCode inner text.
+ let errorCode;
+ await ContentTaskUtils.waitForCondition(() => {
+ errorCode = doc.getElementById("errorCode");
+ return errorCode && errorCode.textContent != "";
+ }, "error code has been set inside the advanced button panel");
+
+ return { textContent: errorCode.textContent, tagName: errorCode.tagName };
+ });
+ is(
+ message.textContent,
+ "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Correct error message found"
+ );
+ is(message.tagName, "a", "Error message is a link");
+
+ message = await SpecialPowers.spawn(bc, [], async function () {
+ let doc = content.document;
+ let errorCode = doc.getElementById("errorCode");
+ errorCode.click();
+ let div = doc.getElementById("certificateErrorDebugInformation");
+ let text = doc.getElementById("certificateErrorText");
+ Assert.ok(
+ content.getComputedStyle(div).display !== "none",
+ "Debug information is visible"
+ );
+ let failedCertChain =
+ content.docShell.failedChannel.securityInfo.failedCertChain.map(cert =>
+ cert.getBase64DERString()
+ );
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: text.textContent,
+ failedCertChain,
+ };
+ });
+ isnot(message.divDisplay, "none", "Debug information is visible");
+ ok(message.text.includes(BAD_CERT), "Correct URL found");
+ ok(
+ message.text.includes("Certificate has expired"),
+ "Correct error message found"
+ );
+ ok(
+ message.text.includes("HTTP Strict Transport Security: false"),
+ "Correct HSTS value found"
+ );
+ ok(
+ message.text.includes("HTTP Public Key Pinning: false"),
+ "Correct HPKP value found"
+ );
+ let certChain = getCertChainAsString(message.failedCertChain);
+ ok(message.text.includes(certChain), "Found certificate chain");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkAdvancedDetailsForHSTS() {
+ info(
+ "Loading a bad STS cert page and verifying the advanced details section"
+ );
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_STS_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ let message = await SpecialPowers.spawn(bc, [], async function () {
+ let doc = content.document;
+ let advancedButton = doc.getElementById("advancedButton");
+ advancedButton.click();
+
+ // Wait until fluent sets the errorCode inner text.
+ let ec;
+ await ContentTaskUtils.waitForCondition(() => {
+ ec = doc.getElementById("errorCode");
+ return ec.textContent != "";
+ }, "error code has been set inside the advanced button panel");
+
+ let cdl = doc.getElementById("cert_domain_link");
+ return {
+ ecTextContent: ec.textContent,
+ ecTagName: ec.tagName,
+ cdlTextContent: cdl.textContent,
+ cdlTagName: cdl.tagName,
+ };
+ });
+
+ const badStsUri = Services.io.newURI(BAD_STS_CERT);
+ is(
+ message.ecTextContent,
+ "SSL_ERROR_BAD_CERT_DOMAIN",
+ "Correct error message found"
+ );
+ is(message.ecTagName, "a", "Error message is a link");
+ const url = badStsUri.prePath.slice(badStsUri.prePath.indexOf(".") + 1);
+ is(message.cdlTextContent, url, "Correct cert_domain_link contents found");
+ is(message.cdlTagName, "a", "cert_domain_link is a link");
+
+ message = await SpecialPowers.spawn(bc, [], async function () {
+ let doc = content.document;
+ let errorCode = doc.getElementById("errorCode");
+ errorCode.click();
+ let div = doc.getElementById("certificateErrorDebugInformation");
+ let text = doc.getElementById("certificateErrorText");
+ let failedCertChain =
+ content.docShell.failedChannel.securityInfo.failedCertChain.map(cert =>
+ cert.getBase64DERString()
+ );
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: text.textContent,
+ failedCertChain,
+ };
+ });
+ isnot(message.divDisplay, "none", "Debug information is visible");
+ ok(message.text.includes(badStsUri.spec), "Correct URL found");
+ ok(
+ message.text.includes(
+ "requested domain name does not match the server\u2019s certificate"
+ ),
+ "Correct error message found"
+ );
+ ok(
+ message.text.includes("HTTP Strict Transport Security: false"),
+ "Correct HSTS value found"
+ );
+ ok(
+ message.text.includes("HTTP Public Key Pinning: true"),
+ "Correct HPKP value found"
+ );
+ let certChain = getCertChainAsString(message.failedCertChain);
+ ok(message.text.includes(certChain), "Found certificate chain");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkUnknownIssuerLearnMoreLink() {
+ info(
+ "Loading a cert error for self-signed pages and checking the correct link is shown"
+ );
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(UNKNOWN_ISSUER, useFrame);
+ let browser = tab.linkedBrowser;
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ let href = await SpecialPowers.spawn(bc, [], async function () {
+ let learnMoreLink = content.document.getElementById("learnMoreLink");
+ return learnMoreLink.href;
+ });
+ ok(href.endsWith("security-error"), "security-error in the Learn More URL");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkViewCertificate() {
+ info("Loading a cert error and checking that the certificate can be shown.");
+ for (let useFrame of [true, false]) {
+ if (useFrame) {
+ // Bug #1573502
+ continue;
+ }
+ let tab = await openErrorPage(UNKNOWN_ISSUER, useFrame);
+ let browser = tab.linkedBrowser;
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ let loaded = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+ await SpecialPowers.spawn(bc, [], async function () {
+ let viewCertificate = content.document.getElementById("viewCertificate");
+ viewCertificate.click();
+ });
+ await loaded;
+
+ let spec = gBrowser.selectedTab.linkedBrowser.documentURI.spec;
+ Assert.ok(
+ spec.startsWith("about:certificate"),
+ "about:certificate is the new opened tab"
+ );
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedTab.linkedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let certificateSection = await ContentTaskUtils.waitForCondition(() => {
+ return doc.querySelector("certificate-section");
+ }, "Certificate section found");
+
+ let infoGroup =
+ certificateSection.shadowRoot.querySelector("info-group");
+ Assert.ok(infoGroup, "infoGroup found");
+
+ let items = infoGroup.shadowRoot.querySelectorAll("info-item");
+ let commonnameID = items[items.length - 1].shadowRoot
+ .querySelector("label")
+ .getAttribute("data-l10n-id");
+ Assert.equal(
+ commonnameID,
+ "certificate-viewer-common-name",
+ "The correct item was selected"
+ );
+
+ let commonnameValue =
+ items[items.length - 1].shadowRoot.querySelector(".info").textContent;
+ Assert.equal(
+ commonnameValue,
+ "self-signed.example.com",
+ "Shows the correct certificate in the page"
+ );
+ }
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab); // closes about:certificate
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkBadStsCertHeadline() {
+ info(
+ "Loading a bad sts cert error page and checking that the correct headline is shown"
+ );
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ await SpecialPowers.spawn(bc, [useFrame], async _useFrame => {
+ const titleText = content.document.querySelector(".title-text");
+ is(
+ titleText.dataset.l10nId,
+ _useFrame ? "nssBadCert-sts-title" : "nssBadCert-title",
+ "Error page title is set"
+ );
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkSandboxedIframe() {
+ info(
+ "Loading a bad sts cert error in a sandboxed iframe and check that the correct headline is shown"
+ );
+ let useFrame = true;
+ let sandboxed = true;
+ let tab = await openErrorPage(BAD_CERT, useFrame, sandboxed);
+ let browser = tab.linkedBrowser;
+
+ let bc = browser.browsingContext.children[0];
+ await SpecialPowers.spawn(bc, [], async function () {
+ let doc = content.document;
+
+ const titleText = doc.querySelector(".title-text");
+ is(
+ titleText.dataset.l10nId,
+ "nssBadCert-sts-title",
+ "Title shows Did Not Connect: Potential Security Issue"
+ );
+
+ const errorLabel = doc.querySelector(
+ '[data-l10n-id="cert-error-code-prefix-link"]'
+ );
+ const elArgs = JSON.parse(errorLabel.dataset.l10nArgs);
+ is(
+ elArgs.error,
+ "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Correct error message found"
+ );
+ is(
+ doc.getElementById("errorCode").tagName,
+ "a",
+ "Error message contains a link"
+ );
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function checkViewSource() {
+ info(
+ "Loading a bad sts cert error in a sandboxed iframe and check that the correct headline is shown"
+ );
+ let uri = "view-source:" + BAD_CERT;
+ let tab = await openErrorPage(uri);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+
+ const errorLabel = doc.querySelector(
+ '[data-l10n-id="cert-error-code-prefix-link"]'
+ );
+ const elArgs = JSON.parse(errorLabel.dataset.l10nArgs);
+ is(
+ elArgs.error,
+ "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Correct error message found"
+ );
+ is(
+ doc.getElementById("errorCode").tagName,
+ "a",
+ "Error message contains a link"
+ );
+
+ const titleText = doc.querySelector(".title-text");
+ is(titleText.dataset.l10nId, "nssBadCert-title", "Error page title is set");
+
+ const shortDesc = doc.getElementById("errorShortDesc");
+ const sdArgs = JSON.parse(shortDesc.dataset.l10nArgs);
+ is(
+ sdArgs.hostname,
+ "expired.example.com",
+ "Should list hostname in error message."
+ );
+ });
+
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, uri);
+ info("Clicking the exceptionDialogButton in advanced panel");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ exceptionButton.click();
+ });
+
+ info("Loading the url after adding exception");
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ ok(
+ !doc.documentURI.startsWith("about:certerror"),
+ "Exception has been added"
+ );
+ });
+
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("expired.example.com", -1, {});
+
+ loaded = BrowserTestUtils.waitForErrorPage(browser);
+ BrowserReloadSkipCache();
+ await loaded;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/about/browser_aboutCertError_clockSkew.js b/browser/base/content/test/about/browser_aboutCertError_clockSkew.js
new file mode 100644
index 0000000000..e3b77bd636
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_clockSkew.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PREF_SERVICES_SETTINGS_CLOCK_SKEW_SECONDS =
+ "services.settings.clock_skew_seconds";
+const PREF_SERVICES_SETTINGS_LAST_FETCHED =
+ "services.settings.last_update_seconds";
+
+add_task(async function checkWrongSystemTimeWarning() {
+ async function setUpPage() {
+ let browser;
+ let certErrorLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://expired.example.com/"
+ );
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the cert error");
+ await certErrorLoaded;
+
+ return SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let div = doc.getElementById("errorShortDesc");
+ let learnMoreLink = doc.getElementById("learnMoreLink");
+
+ await ContentTaskUtils.waitForCondition(
+ () => div.textContent.includes("update your computer clock"),
+ "Correct error message found"
+ );
+
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: div.textContent,
+ learnMoreLink: learnMoreLink.href,
+ };
+ });
+ }
+
+ // Pretend that we recently updated our kinto clock skew pref
+ Services.prefs.setIntPref(
+ PREF_SERVICES_SETTINGS_LAST_FETCHED,
+ Math.floor(Date.now() / 1000)
+ );
+
+ // For this test, we want to trick Firefox into believing that
+ // the local system time (as returned by Date.now()) is wrong.
+ // Because we don't want to actually change the local system time,
+ // we will do the following:
+
+ // Take the validity date of our test page (expired.example.com).
+ let expiredDate = new Date("2010/01/05 12:00");
+ let localDate = Date.now();
+
+ // Compute the difference between the server date and the correct
+ // local system date.
+ let skew = Math.floor((localDate - expiredDate) / 1000);
+
+ // Make it seem like our reference server agrees that the certificate
+ // date is correct by recording the difference as clock skew.
+ Services.prefs.setIntPref(PREF_SERVICES_SETTINGS_CLOCK_SKEW_SECONDS, skew);
+
+ let localDateFmt = new Intl.DateTimeFormat("en-US", {
+ dateStyle: "medium",
+ }).format(localDate);
+
+ info("Loading a bad cert page with a skewed clock");
+ let message = await setUpPage();
+
+ isnot(
+ message.divDisplay,
+ "none",
+ "Wrong time message information is visible"
+ );
+ ok(
+ message.text.includes("update your computer clock"),
+ "Correct error message found"
+ );
+ ok(
+ message.text.includes("expired.example.com"),
+ "URL found in error message"
+ );
+ ok(message.text.includes(localDateFmt), "Correct local date displayed");
+ ok(
+ message.learnMoreLink.includes("time-errors"),
+ "time-errors in the Learn More URL"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ Services.prefs.clearUserPref(PREF_SERVICES_SETTINGS_LAST_FETCHED);
+ Services.prefs.clearUserPref(PREF_SERVICES_SETTINGS_CLOCK_SKEW_SECONDS);
+});
+
+add_task(async function checkCertError() {
+ async function setUpPage() {
+ let browser;
+ let certErrorLoaded;
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://expired.example.com/"
+ );
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+
+ info("Loading and waiting for the cert error");
+ await certErrorLoaded;
+
+ return SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let el = doc.getElementById("errorWhatToDoText");
+ await ContentTaskUtils.waitForCondition(() => el.textContent);
+ return el.textContent;
+ });
+ }
+
+ // The particular error message will be displayed only when clock_skew_seconds is
+ // less or equal to a day and the difference between date.now() and last_fetched is less than
+ // or equal to 5 days. Setting the prefs accordingly.
+
+ Services.prefs.setIntPref(
+ PREF_SERVICES_SETTINGS_LAST_FETCHED,
+ Math.floor(Date.now() / 1000)
+ );
+
+ let skew = 60 * 60 * 24;
+ Services.prefs.setIntPref(PREF_SERVICES_SETTINGS_CLOCK_SKEW_SECONDS, skew);
+
+ info("Loading a bad cert page");
+ let message = await setUpPage();
+
+ ok(
+ message.includes(
+ "The issue is most likely with the website, and there is nothing you can do" +
+ " to resolve it. You can notify the website’s administrator about the problem."
+ ),
+ "Correct error message found"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ Services.prefs.clearUserPref(PREF_SERVICES_SETTINGS_LAST_FETCHED);
+ Services.prefs.clearUserPref(PREF_SERVICES_SETTINGS_CLOCK_SKEW_SECONDS);
+});
diff --git a/browser/base/content/test/about/browser_aboutCertError_exception.js b/browser/base/content/test/about/browser_aboutCertError_exception.js
new file mode 100644
index 0000000000..7ee1bdde45
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_exception.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BAD_CERT = "https://expired.example.com/";
+const BAD_STS_CERT =
+ "https://badchain.include-subdomains.pinning.example.com:443";
+const PREF_PERMANENT_OVERRIDE = "security.certerrors.permanentOverride";
+
+add_task(async function checkExceptionDialogButton() {
+ info(
+ "Loading a bad cert page and making sure the exceptionDialogButton directly adds an exception"
+ );
+ let tab = await openErrorPage(BAD_CERT);
+ let browser = tab.linkedBrowser;
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, BAD_CERT);
+ info("Clicking the exceptionDialogButton in advanced panel");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ exceptionButton.click();
+ });
+
+ info("Loading the url after adding exception");
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ ok(
+ !doc.documentURI.startsWith("about:certerror"),
+ "Exception has been added"
+ );
+ });
+
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("expired.example.com", -1, {});
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function checkPermanentExceptionPref() {
+ info(
+ "Loading a bad cert page and making sure the permanent state of exceptions can be controlled via pref"
+ );
+
+ for (let permanentOverride of [false, true]) {
+ Services.prefs.setBoolPref(PREF_PERMANENT_OVERRIDE, permanentOverride);
+
+ let tab = await openErrorPage(BAD_CERT);
+ let browser = tab.linkedBrowser;
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, BAD_CERT);
+ info("Clicking the exceptionDialogButton in advanced panel");
+ let serverCertBytes = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let doc = content.document;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ exceptionButton.click();
+ return content.docShell.failedChannel.securityInfo.serverCert.getRawDER();
+ }
+ );
+
+ info("Loading the url after adding exception");
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ ok(
+ !doc.documentURI.startsWith("about:certerror"),
+ "Exception has been added"
+ );
+ });
+
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+
+ let isTemporary = {};
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ let cert = certdb.constructX509(serverCertBytes);
+ let hasException = certOverrideService.hasMatchingOverride(
+ "expired.example.com",
+ -1,
+ {},
+ cert,
+ isTemporary
+ );
+ ok(hasException, "Has stored an exception for the page.");
+ is(
+ isTemporary.value,
+ !permanentOverride,
+ `Has stored a ${
+ permanentOverride ? "permanent" : "temporary"
+ } exception for the page.`
+ );
+
+ certOverrideService.clearValidityOverride("expired.example.com", -1, {});
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ Services.prefs.clearUserPref(PREF_PERMANENT_OVERRIDE);
+});
+
+add_task(async function checkBadStsCert() {
+ info("Loading a badStsCert and making sure exception button doesn't show up");
+
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_STS_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ frame: useFrame }],
+ async function ({ frame }) {
+ let doc = frame
+ ? content.document.querySelector("iframe").contentDocument
+ : content.document;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ ok(
+ ContentTaskUtils.is_hidden(exceptionButton),
+ "Exception button is hidden."
+ );
+ }
+ );
+
+ let message = await SpecialPowers.spawn(
+ browser,
+ [{ frame: useFrame }],
+ async function ({ frame }) {
+ let doc = frame
+ ? content.document.querySelector("iframe").contentDocument
+ : content.document;
+ let advancedButton = doc.getElementById("advancedButton");
+ advancedButton.click();
+
+ // aboutNetError.mjs is using async localization to format several
+ // messages and in result the translation may be applied later.
+ // We want to return the textContent of the element only after
+ // the translation completes, so let's wait for it here.
+ let elements = [doc.getElementById("badCertTechnicalInfo")];
+ await ContentTaskUtils.waitForCondition(() => {
+ return elements.every(elem => !!elem.textContent.trim().length);
+ });
+
+ return doc.getElementById("badCertTechnicalInfo").textContent;
+ }
+ );
+ ok(
+ message.includes("SSL_ERROR_BAD_CERT_DOMAIN"),
+ "Didn't find SSL_ERROR_BAD_CERT_DOMAIN."
+ );
+ ok(
+ message.includes("The certificate is only valid for"),
+ "Didn't find error message."
+ );
+ ok(
+ message.includes("a certificate that is not valid for"),
+ "Didn't find error message."
+ );
+ ok(
+ message.includes("badchain.include-subdomains.pinning.example.com"),
+ "Didn't find domain in error message."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkhideAddExceptionButtonViaPref() {
+ info(
+ "Loading a bad cert page and verifying the pref security.certerror.hideAddException"
+ );
+ Services.prefs.setBoolPref("security.certerror.hideAddException", true);
+
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ frame: useFrame }],
+ async function ({ frame }) {
+ let doc = frame
+ ? content.document.querySelector("iframe").contentDocument
+ : content.document;
+
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ ok(
+ ContentTaskUtils.is_hidden(exceptionButton),
+ "Exception button is hidden."
+ );
+ }
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ Services.prefs.clearUserPref("security.certerror.hideAddException");
+});
+
+add_task(async function checkhideAddExceptionButtonInFrames() {
+ info("Loading a bad cert page in a frame and verifying it's hidden.");
+ let tab = await openErrorPage(BAD_CERT, true);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document.querySelector("iframe").contentDocument;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ ok(
+ ContentTaskUtils.is_hidden(exceptionButton),
+ "Exception button is hidden."
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/about/browser_aboutCertError_mitm.js b/browser/base/content/test/about/browser_aboutCertError_mitm.js
new file mode 100644
index 0000000000..5c9b5e8144
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_mitm.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PREF_MITM_PRIMING = "security.certerrors.mitm.priming.enabled";
+const PREF_MITM_PRIMING_ENDPOINT = "security.certerrors.mitm.priming.endpoint";
+const PREF_MITM_CANARY_ISSUER = "security.pki.mitm_canary_issuer";
+const PREF_MITM_AUTO_ENABLE_ENTERPRISE_ROOTS =
+ "security.certerrors.mitm.auto_enable_enterprise_roots";
+const PREF_ENTERPRISE_ROOTS = "security.enterprise_roots.enabled";
+
+const UNKNOWN_ISSUER = "https://untrusted.example.com";
+
+// Check that basic MitM priming works and the MitM error page is displayed successfully.
+add_task(async function checkMitmPriming() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_MITM_PRIMING, true],
+ [PREF_MITM_PRIMING_ENDPOINT, UNKNOWN_ISSUER],
+ ],
+ });
+
+ let browser;
+ let certErrorLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, UNKNOWN_ISSUER);
+ browser = gBrowser.selectedBrowser;
+ // The page will reload by itself after the initial canary request, so we wait
+ // until the AboutNetErrorLoad event has happened twice.
+ certErrorLoaded = new Promise(resolve => {
+ let loaded = 0;
+ let removeEventListener = BrowserTestUtils.addContentEventListener(
+ browser,
+ "AboutNetErrorLoad",
+ () => {
+ if (++loaded == 2) {
+ removeEventListener();
+ resolve();
+ }
+ },
+ { capture: false, wantUntrusted: true }
+ );
+ });
+ },
+ false
+ );
+
+ await certErrorLoaded;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ is(
+ content.document.body.getAttribute("code"),
+ "MOZILLA_PKIX_ERROR_MITM_DETECTED",
+ "MitM error page has loaded."
+ );
+ });
+
+ ok(true, "Successfully loaded the MitM error page.");
+
+ is(
+ Services.prefs.getStringPref(PREF_MITM_CANARY_ISSUER),
+ "CN=Unknown CA",
+ "Stored the correct issuer"
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ const shortDesc = content.document.querySelector("#errorShortDesc");
+ const whatToDo = content.document.querySelector("#errorWhatToDoText");
+
+ await ContentTaskUtils.waitForCondition(
+ () => shortDesc.textContent != "" && whatToDo.textContent != "",
+ "DOM localization has been updated"
+ );
+
+ ok(
+ shortDesc.textContent.includes("Unknown CA"),
+ "Shows the name of the issuer."
+ );
+
+ ok(
+ whatToDo.textContent.includes("Unknown CA"),
+ "Shows the name of the issuer."
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ Services.prefs.clearUserPref(PREF_MITM_CANARY_ISSUER);
+});
+
+// Check that we set the enterprise roots pref correctly on MitM
+add_task(async function checkMitmAutoEnableEnterpriseRoots() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_MITM_PRIMING, true],
+ [PREF_MITM_PRIMING_ENDPOINT, UNKNOWN_ISSUER],
+ [PREF_MITM_AUTO_ENABLE_ENTERPRISE_ROOTS, true],
+ [PREF_ENTERPRISE_ROOTS, false],
+ ],
+ });
+
+ let browser;
+ let certErrorLoaded;
+
+ let prefChanged = TestUtils.waitForPrefChange(
+ PREF_ENTERPRISE_ROOTS,
+ value => value === true
+ );
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, UNKNOWN_ISSUER);
+ browser = gBrowser.selectedBrowser;
+ // The page will reload by itself after the initial canary request, so we wait
+ // until the AboutNetErrorLoad event has happened twice.
+ certErrorLoaded = new Promise(resolve => {
+ let loaded = 0;
+ let removeEventListener = BrowserTestUtils.addContentEventListener(
+ browser,
+ "AboutNetErrorLoad",
+ () => {
+ if (++loaded == 2) {
+ removeEventListener();
+ resolve();
+ }
+ },
+ { capture: false, wantUntrusted: true }
+ );
+ });
+ },
+ false
+ );
+
+ await certErrorLoaded;
+ await prefChanged;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ is(
+ content.document.body.getAttribute("code"),
+ "MOZILLA_PKIX_ERROR_MITM_DETECTED",
+ "MitM error page has loaded."
+ );
+ });
+
+ ok(true, "Successfully loaded the MitM error page.");
+
+ ok(
+ !Services.prefs.prefHasUserValue(PREF_ENTERPRISE_ROOTS),
+ "Flipped the enterprise roots pref back"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ Services.prefs.clearUserPref(PREF_MITM_CANARY_ISSUER);
+});
diff --git a/browser/base/content/test/about/browser_aboutCertError_noSubjectAltName.js b/browser/base/content/test/about/browser_aboutCertError_noSubjectAltName.js
new file mode 100644
index 0000000000..1a2add1c96
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_noSubjectAltName.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BROWSER_NAME = document
+ .getElementById("bundle_brand")
+ .getString("brandShortName");
+const UNKNOWN_ISSUER = "https://no-subject-alt-name.example.com:443";
+
+const checkAdvancedAndGetTechnicalInfoText = async () => {
+ let doc = content.document;
+
+ let advancedButton = doc.getElementById("advancedButton");
+ ok(advancedButton, "advancedButton found");
+ is(
+ advancedButton.hasAttribute("disabled"),
+ false,
+ "advancedButton should be clickable"
+ );
+ advancedButton.click();
+
+ let badCertAdvancedPanel = doc.getElementById("badCertAdvancedPanel");
+ ok(badCertAdvancedPanel, "badCertAdvancedPanel found");
+
+ let badCertTechnicalInfo = doc.getElementById("badCertTechnicalInfo");
+ ok(badCertTechnicalInfo, "badCertTechnicalInfo found");
+
+ // Wait until fluent sets the errorCode inner text.
+ await ContentTaskUtils.waitForCondition(() => {
+ let errorCode = doc.getElementById("errorCode");
+ return errorCode.textContent == "SSL_ERROR_BAD_CERT_DOMAIN";
+ }, "correct error code has been set inside the advanced button panel");
+
+ let viewCertificate = doc.getElementById("viewCertificate");
+ ok(viewCertificate, "viewCertificate found");
+
+ return badCertTechnicalInfo.innerHTML;
+};
+
+const checkCorrectMessages = message => {
+ let isCorrectMessage = message.includes(
+ "Websites prove their identity via certificates. " +
+ BROWSER_NAME +
+ " does not trust this site because it uses a certificate that is" +
+ " not valid for no-subject-alt-name.example.com"
+ );
+ is(isCorrectMessage, true, "That message should appear");
+ let isWrongMessage = message.includes("The certificate is only valid for ");
+ is(isWrongMessage, false, "That message shouldn't appear");
+};
+
+add_task(async function checkUntrustedCertError() {
+ info(
+ `Loading ${UNKNOWN_ISSUER} which does not have a subject specified in the certificate`
+ );
+ let tab = await openErrorPage(UNKNOWN_ISSUER);
+ let browser = tab.linkedBrowser;
+ info("Clicking the exceptionDialogButton in advanced panel");
+ let badCertTechnicalInfoText = await SpecialPowers.spawn(
+ browser,
+ [],
+ checkAdvancedAndGetTechnicalInfoText
+ );
+ checkCorrectMessages(badCertTechnicalInfoText, browser);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/about/browser_aboutCertError_offlineSupport.js b/browser/base/content/test/about/browser_aboutCertError_offlineSupport.js
new file mode 100644
index 0000000000..5b717a683a
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_offlineSupport.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BAD_CERT_PAGE = "https://expired.example.com";
+const DUMMY_SUPPORT_BASE_PATH = "/1/firefox/fxVersion/OSVersion/language/";
+const DUMMY_SUPPORT_URL = BAD_CERT_PAGE + DUMMY_SUPPORT_BASE_PATH;
+const OFFLINE_SUPPORT_PAGE =
+ "chrome://global/content/neterror/supportpages/time-errors.html";
+
+add_task(async function testOfflineSupportPage() {
+ // Cache the original value of app.support.baseURL pref to reset later
+ let originalBaseURL = Services.prefs.getCharPref("app.support.baseURL");
+
+ Services.prefs.setCharPref("app.support.baseURL", DUMMY_SUPPORT_URL);
+ let errorTab = await openErrorPage(BAD_CERT_PAGE);
+
+ let offlineSupportPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ DUMMY_SUPPORT_URL + "time-errors"
+ );
+ await SpecialPowers.spawn(
+ errorTab.linkedBrowser,
+ [DUMMY_SUPPORT_URL],
+ async expectedURL => {
+ let doc = content.document;
+
+ let learnMoreLink = doc.getElementById("learnMoreLink");
+ let supportPageURL = learnMoreLink.getAttribute("href");
+ ok(
+ supportPageURL == expectedURL + "time-errors",
+ "Correct support page URL has been set"
+ );
+ await EventUtils.synthesizeMouseAtCenter(learnMoreLink, {}, content);
+ }
+ );
+ let offlineSupportTab = await offlineSupportPromise;
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ OFFLINE_SUPPORT_PAGE
+ );
+
+ // Reset this pref instead of clearing it to maintain globally set
+ // custom value for testing purposes.
+ Services.prefs.setCharPref("app.support.baseURL", originalBaseURL);
+
+ await BrowserTestUtils.removeTab(offlineSupportTab);
+ await BrowserTestUtils.removeTab(errorTab);
+});
diff --git a/browser/base/content/test/about/browser_aboutCertError_telemetry.js b/browser/base/content/test/about/browser_aboutCertError_telemetry.js
new file mode 100644
index 0000000000..61ec8afcbf
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_telemetry.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const BAD_CERT = "https://expired.example.com/";
+const BAD_STS_CERT =
+ "https://badchain.include-subdomains.pinning.example.com:443";
+
+add_task(async function checkTelemetryClickEvents() {
+ info("Loading a bad cert page and verifying telemetry click events arrive.");
+
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+
+ // For obvious reasons event telemetry in the content processes updates with
+ // the main processs asynchronously, so we need to wait for the main process
+ // to catch up through the entire test.
+
+ // There's an arbitrary interval of 2 seconds in which the content
+ // processes sync their event data with the parent process, we wait
+ // this out to ensure that we clear everything that is left over from
+ // previous tests and don't receive random events in the middle of our tests.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 2000));
+
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+
+ // Now enable recording our telemetry. Even if this is disabled, content
+ // processes will send event telemetry to the parent, thus we needed to ensure
+ // we waited and cleared first. Sigh.
+ Services.telemetry.setEventRecordingEnabled("security.ui.certerror", true);
+
+ for (let useFrame of [false, true]) {
+ let recordedObjects = [
+ "advanced_button",
+ "learn_more_link",
+ "error_code_link",
+ "clipboard_button_top",
+ "clipboard_button_bot",
+ "return_button_top",
+ ];
+
+ recordedObjects.push("return_button_adv");
+ if (!useFrame) {
+ recordedObjects.push("exception_button");
+ }
+
+ for (let object of recordedObjects) {
+ let tab = await openErrorPage(BAD_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ let loadEvents = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ if (events && events.length) {
+ events = events.filter(
+ e => e[1] == "security.ui.certerror" && e[2] == "load"
+ );
+ if (
+ events.length == 1 &&
+ events[0][5].is_frame == useFrame.toString()
+ ) {
+ return events;
+ }
+ }
+ return null;
+ }, "recorded telemetry for the load");
+
+ is(
+ loadEvents.length,
+ 1,
+ `recorded telemetry for the load testing ${object}, useFrame: ${useFrame}`
+ );
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ await SpecialPowers.spawn(bc, [object], async function (objectId) {
+ let doc = content.document;
+
+ await ContentTaskUtils.waitForCondition(
+ () => doc.body.classList.contains("certerror"),
+ "Wait for certerror to be loaded"
+ );
+
+ let domElement = doc.querySelector(`[data-telemetry-id='${objectId}']`);
+ domElement.click();
+ });
+
+ let clickEvents = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ if (events && events.length) {
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.certerror" &&
+ e[2] == "click" &&
+ e[3] == object
+ );
+ if (
+ events.length == 1 &&
+ events[0][5].is_frame == useFrame.toString()
+ ) {
+ return events;
+ }
+ }
+ return null;
+ }, "Has captured telemetry events.");
+
+ is(
+ clickEvents.length,
+ 1,
+ `recorded telemetry for the click on ${object}, useFrame: ${useFrame}`
+ );
+
+ // We opened an extra tab for the SUMO page, need to close it.
+ if (object == "learn_more_link") {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ if (object == "exception_button") {
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride(
+ "expired.example.com",
+ -1,
+ {}
+ );
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ }
+
+ let enableCertErrorUITelemetry = Services.prefs.getBoolPref(
+ "security.certerrors.recordEventTelemetry"
+ );
+ Services.telemetry.setEventRecordingEnabled(
+ "security.ui.certerror",
+ enableCertErrorUITelemetry
+ );
+});
diff --git a/browser/base/content/test/about/browser_aboutDialog_distribution.js b/browser/base/content/test/about/browser_aboutDialog_distribution.js
new file mode 100644
index 0000000000..8f52533bbc
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutDialog_distribution.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js",
+ this
+);
+
+add_task(async function verify_distribution_info_hides() {
+ let defaultBranch = Services.prefs.getDefaultBranch(null);
+
+ defaultBranch.setCharPref("distribution.id", "mozilla-test-distribution-id");
+ defaultBranch.setCharPref("distribution.version", "1.0");
+
+ let aboutDialog = await waitForAboutDialog();
+
+ let distroIdField = aboutDialog.document.getElementById("distributionId");
+ let distroField = aboutDialog.document.getElementById("distribution");
+
+ if (
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId")
+ ) {
+ is(distroIdField.value, "mozilla-test-distribution-id - 1.0");
+ is(distroIdField.style.display, "block");
+ is(distroField.style.display, "block");
+ } else {
+ is(distroIdField.value, "");
+ isnot(distroIdField.style.display, "block");
+ isnot(distroField.style.display, "block");
+ }
+
+ aboutDialog.close();
+});
+
+add_task(async function verify_distribution_info_displays() {
+ let defaultBranch = Services.prefs.getDefaultBranch(null);
+
+ defaultBranch.setCharPref("distribution.id", "test-distribution-id");
+ defaultBranch.setCharPref("distribution.version", "1.0");
+ defaultBranch.setCharPref("distribution.about", "About Text");
+
+ let aboutDialog = await waitForAboutDialog();
+
+ let distroIdField = aboutDialog.document.getElementById("distributionId");
+
+ is(distroIdField.value, "test-distribution-id - 1.0");
+ is(distroIdField.style.display, "block");
+
+ let distroField = aboutDialog.document.getElementById("distribution");
+ is(distroField.value, "About Text");
+ is(distroField.style.display, "block");
+
+ aboutDialog.close();
+});
+
+add_task(async function cleanup() {
+ let defaultBranch = Services.prefs.getDefaultBranch(null);
+
+ // This is the best we can do since we can't remove default prefs
+ defaultBranch.setCharPref("distribution.id", "");
+ defaultBranch.setCharPref("distribution.version", "");
+ defaultBranch.setCharPref("distribution.about", "");
+});
diff --git a/browser/base/content/test/about/browser_aboutHome_search_POST.js b/browser/base/content/test/about/browser_aboutHome_search_POST.js
new file mode 100644
index 0000000000..c892198207
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_POST.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ignoreAllUncaughtExceptions();
+
+add_task(async function () {
+ info("Check POST search engine support");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar",
+ false,
+ ],
+ ],
+ });
+
+ let currEngine = await Services.search.getDefault();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async browser => {
+ let observerPromise = new Promise(resolve => {
+ let searchObserver = async function search_observer(
+ subject,
+ topic,
+ data
+ ) {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ info("Observer: " + data + " for " + engine.name);
+
+ if (data != "engine-added") {
+ return;
+ }
+
+ if (engine.name != "POST Search") {
+ return;
+ }
+
+ Services.obs.removeObserver(
+ searchObserver,
+ "browser-search-engine-modified"
+ );
+
+ resolve(engine);
+ };
+
+ Services.obs.addObserver(
+ searchObserver,
+ "browser-search-engine-modified"
+ );
+ });
+
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ Services.search.addOpenSearchEngine(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test:80/browser/browser/base/content/test/about/POSTSearchEngine.xml",
+ null
+ );
+
+ engine = await observerPromise;
+ Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ return engine.name;
+ });
+
+ // Ready to execute the tests!
+ let needle = "Search for something awesome.";
+
+ let promise = BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(browser, [{ needle }], async function (args) {
+ let doc = content.document;
+ let el = doc.querySelector(["#searchText", "#newtab-search-text"]);
+ el.value = args.needle;
+ doc.getElementById("searchSubmit").click();
+ });
+
+ await promise;
+
+ // When the search results load, check them for correctness.
+ await SpecialPowers.spawn(browser, [{ needle }], async function (args) {
+ let loadedText = content.document.body.textContent;
+ ok(loadedText, "search page loaded");
+ is(
+ loadedText,
+ "searchterms=" + escape(args.needle.replace(/\s/g, "+")),
+ "Search text should arrive correctly"
+ );
+ });
+
+ await Services.search.setDefault(
+ currEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {}
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/about/browser_aboutHome_search_composing.js b/browser/base/content/test/about/browser_aboutHome_search_composing.js
new file mode 100644
index 0000000000..309f1f674a
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_composing.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ignoreAllUncaughtExceptions();
+
+add_task(async function () {
+ info("Clicking suggestion list while composing");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar",
+ false,
+ ],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async function (browser) {
+ // Add a test engine that provides suggestions and switch to it.
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ return engine.name;
+ });
+
+ // Clear any search history results
+ await FormHistory.update({ op: "remove" });
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Start composition and type "x"
+ let input = content.document.querySelector([
+ "#searchText",
+ "#newtab-search-text",
+ ]);
+ input.focus();
+ });
+
+ info("Setting up the mutation observer before synthesizing composition");
+ let mutationPromise = SpecialPowers.spawn(browser, [], async function () {
+ let searchController = content.wrappedJSObject.gContentSearchController;
+
+ // Wait for the search suggestions to become visible.
+ let table = searchController._suggestionsList;
+ let input = content.document.querySelector([
+ "#searchText",
+ "#newtab-search-text",
+ ]);
+
+ await ContentTaskUtils.waitForMutationCondition(
+ input,
+ { attributeFilter: ["aria-expanded"] },
+ () => input.getAttribute("aria-expanded") == "true"
+ );
+ ok(!table.hidden, "Search suggestion table unhidden");
+
+ let row = table.children[1];
+ row.setAttribute("id", "TEMPID");
+
+ // ContentSearchUIController looks at the current selectedIndex when
+ // performing a search. Synthesizing the mouse event on the suggestion
+ // doesn't actually mouseover the suggestion and trigger it to be flagged
+ // as selected, so we manually select it first.
+ searchController.selectedIndex = 1;
+ });
+
+ // FYI: "compositionstart" will be dispatched automatically.
+ await BrowserTestUtils.synthesizeCompositionChange(
+ {
+ composition: {
+ string: "x",
+ clauses: [
+ { length: 1, attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE },
+ ],
+ },
+ caret: { start: 1, length: 0 },
+ },
+ browser
+ );
+
+ info("Waiting for search suggestion table unhidden");
+ await mutationPromise;
+
+ // Click the second suggestion.
+ let expectedURL = (await Services.search.getDefault()).getSubmission(
+ "xbar",
+ null,
+ "homepage"
+ ).uri.spec;
+ let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#TEMPID",
+ {
+ button: 0,
+ },
+ browser
+ );
+ await loadPromise;
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/about/browser_aboutHome_search_searchbar.js b/browser/base/content/test/about/browser_aboutHome_search_searchbar.js
new file mode 100644
index 0000000000..7b08d2ae34
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_searchbar.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+ignoreAllUncaughtExceptions();
+
+add_task(async function test_setup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function () {
+ info("Cmd+k should focus the search box in the toolbar when it's present");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async function (browser) {
+ await BrowserTestUtils.synthesizeMouseAtCenter("#brandLogo", {}, browser);
+
+ let doc = window.document;
+ let searchInput = BrowserSearch.searchBar.textbox;
+ isnot(
+ searchInput,
+ doc.activeElement,
+ "Search bar should not be the active element."
+ );
+
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await TestUtils.waitForCondition(() => doc.activeElement === searchInput);
+ is(
+ searchInput,
+ doc.activeElement,
+ "Search bar should be the active element."
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/about/browser_aboutHome_search_suggestion.js b/browser/base/content/test/about/browser_aboutHome_search_suggestion.js
new file mode 100644
index 0000000000..4e1da9fe3e
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_suggestion.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ignoreAllUncaughtExceptions();
+
+add_task(async function () {
+ // See browser_contentSearchUI.js for comprehensive content search UI tests.
+ info("Search suggestion smoke test");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar",
+ false,
+ ],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async function (browser) {
+ // Add a test engine that provides suggestions and switch to it.
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ return engine.name;
+ });
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Type an X in the search input.
+ let input = content.document.querySelector([
+ "#searchText",
+ "#newtab-search-text",
+ ]);
+ input.focus();
+ });
+
+ await BrowserTestUtils.synthesizeKey("x", {}, browser);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Wait for the search suggestions to become visible.
+ let table = content.document.getElementById("searchSuggestionTable");
+ let input = content.document.querySelector([
+ "#searchText",
+ "#newtab-search-text",
+ ]);
+
+ await ContentTaskUtils.waitForMutationCondition(
+ input,
+ { attributeFilter: ["aria-expanded"] },
+ () => input.getAttribute("aria-expanded") == "true"
+ );
+ ok(!table.hidden, "Search suggestion table unhidden");
+ });
+
+ // Empty the search input, causing the suggestions to be hidden.
+ await BrowserTestUtils.synthesizeKey("a", { accelKey: true }, browser);
+ await BrowserTestUtils.synthesizeKey("VK_DELETE", {}, browser);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let table = content.document.getElementById("searchSuggestionTable");
+ await ContentTaskUtils.waitForCondition(
+ () => table.hidden,
+ "Search suggestion table hidden"
+ );
+ });
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/about/browser_aboutHome_search_telemetry.js b/browser/base/content/test/about/browser_aboutHome_search_telemetry.js
new file mode 100644
index 0000000000..e23d07aa38
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_telemetry.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ignoreAllUncaughtExceptions();
+
+add_task(async function () {
+ info(
+ "Check that performing a search fires a search event and records to Telemetry."
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar",
+ false,
+ ],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async function (browser) {
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ return engine.name;
+ });
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ expectedName: engine.name }],
+ async function (args) {
+ let engineName =
+ content.wrappedJSObject.gContentSearchController.defaultEngine.name;
+ is(
+ engineName,
+ args.expectedName,
+ "Engine name in DOM should match engine we just added"
+ );
+ }
+ );
+
+ let numSearchesBefore = 0;
+ // Get the current number of recorded searches.
+ let histogramKey = `other-${engine.name}.abouthome`;
+ try {
+ let hs = Services.telemetry
+ .getKeyedHistogramById("SEARCH_COUNTS")
+ .snapshot();
+ if (histogramKey in hs) {
+ numSearchesBefore = hs[histogramKey].sum;
+ }
+ } catch (ex) {
+ // No searches performed yet, not a problem, |numSearchesBefore| is 0.
+ }
+
+ let searchStr = "a search";
+
+ let expectedURL = (await Services.search.getDefault()).getSubmission(
+ searchStr,
+ null,
+ "homepage"
+ ).uri.spec;
+ let promise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ browser
+ );
+
+ // Perform a search to increase the SEARCH_COUNT histogram.
+ await SpecialPowers.spawn(
+ browser,
+ [{ searchStr }],
+ async function (args) {
+ let doc = content.document;
+ info("Perform a search.");
+ let el = doc.querySelector(["#searchText", "#newtab-search-text"]);
+ el.value = args.searchStr;
+ doc.getElementById("searchSubmit").click();
+ }
+ );
+
+ await promise;
+
+ // Make sure the SEARCH_COUNTS histogram has the right key and count.
+ let hs = Services.telemetry
+ .getKeyedHistogramById("SEARCH_COUNTS")
+ .snapshot();
+ Assert.ok(histogramKey in hs, "histogram with key should be recorded");
+ Assert.equal(
+ hs[histogramKey].sum,
+ numSearchesBefore + 1,
+ "histogram sum should be incremented"
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/about/browser_aboutNetError.js b/browser/base/content/test/about/browser_aboutNetError.js
new file mode 100644
index 0000000000..0f98413f33
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SSL3_PAGE = "https://ssl3.example.com/";
+const TLS10_PAGE = "https://tls1.example.com/";
+const TLS12_PAGE = "https://tls12.example.com/";
+const TRIPLEDES_PAGE = "https://3des.example.com/";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gDNSOverride",
+ "@mozilla.org/network/native-dns-override;1",
+ "nsINativeDNSResolverOverride"
+);
+
+// This includes all the cipher suite prefs we have.
+function resetPrefs() {
+ Services.prefs.clearUserPref("security.tls.version.min");
+ Services.prefs.clearUserPref("security.tls.version.max");
+ Services.prefs.clearUserPref("security.tls.version.enable-deprecated");
+ Services.prefs.clearUserPref("browser.fixup.alternate.enabled");
+}
+
+add_task(async function resetToDefaultConfig() {
+ info(
+ "Change TLS config to cause page load to fail, check that reset button is shown and that it works"
+ );
+
+ // Set ourselves up for a TLS error.
+ Services.prefs.setIntPref("security.tls.version.min", 1); // TLS 1.0
+ Services.prefs.setIntPref("security.tls.version.max", 1);
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TLS12_PAGE);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ await pageLoaded;
+
+ // Setup an observer for the target page.
+ const finalLoadComplete = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ TLS12_PAGE
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+
+ const prefResetButton = doc.getElementById("prefResetButton");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.is_visible(prefResetButton),
+ "prefResetButton is visible"
+ );
+
+ if (!Services.focus.focusedElement == prefResetButton) {
+ await ContentTaskUtils.waitForEvent(prefResetButton, "focus");
+ }
+
+ Assert.ok(true, "prefResetButton has focus");
+
+ prefResetButton.click();
+ });
+
+ info("Waiting for the page to load after the click");
+ await finalLoadComplete;
+
+ resetPrefs();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function checkLearnMoreLink() {
+ info("Load an unsupported TLS page and check for a learn more link");
+
+ // Set ourselves up for TLS error
+ Services.prefs.setIntPref("security.tls.version.min", 3);
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TLS10_PAGE);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ await pageLoaded;
+
+ const baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+
+ await SpecialPowers.spawn(browser, [baseURL], function (_baseURL) {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+
+ const tlsVersionNotice = doc.getElementById("tlsVersionNotice");
+ ok(
+ ContentTaskUtils.is_visible(tlsVersionNotice),
+ "TLS version notice is visible"
+ );
+
+ const learnMoreLink = doc.getElementById("learnMoreLink");
+ ok(
+ ContentTaskUtils.is_visible(learnMoreLink),
+ "Learn More link is visible"
+ );
+ is(learnMoreLink.getAttribute("href"), _baseURL + "connection-not-secure");
+
+ const titleEl = doc.querySelector(".title-text");
+ const actualDataL10nID = titleEl.getAttribute("data-l10n-id");
+ is(
+ actualDataL10nID,
+ "nssFailure2-title",
+ "Correct error page title is set"
+ );
+
+ const errorCodeEl = doc.querySelector("#errorShortDesc2");
+ const actualDataL10Args = errorCodeEl.getAttribute("data-l10n-args");
+ ok(
+ actualDataL10Args.includes("SSL_ERROR_PROTOCOL_VERSION_ALERT"),
+ "Correct error code is set"
+ );
+ });
+
+ resetPrefs();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// When a user tries going to a host without a suffix
+// and the term doesn't match a host and we are able to suggest a
+// valid correction, the page should show the correction.
+// e.g. http://example/example2 -> https://www.example.com/example2
+add_task(async function checkDomainCorrection() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.fixup.alternate.enabled", false]],
+ });
+ lazy.gDNSOverride.addIPOverride("www.example.com", "::1");
+
+ info("Try loading a URI that should result in an error page");
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example/example2/",
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ let browser = gBrowser.selectedBrowser;
+ let pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ await pageLoaded;
+
+ const baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+
+ await SpecialPowers.spawn(browser, [baseURL], async function (_baseURL) {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+
+ const errorNotice = doc.getElementById("errorShortDesc");
+ ok(ContentTaskUtils.is_visible(errorNotice), "Error text is visible");
+
+ // Wait for the domain suggestion to be resolved and for the text to update
+ let link;
+ await ContentTaskUtils.waitForCondition(() => {
+ link = errorNotice.querySelector("a");
+ return link && link.textContent != "";
+ }, "Helper link has been set");
+
+ is(
+ link.getAttribute("href"),
+ "https://www.example.com/example2/",
+ "Link was corrected"
+ );
+
+ const actualDataL10nID = link.getAttribute("data-l10n-name");
+ is(actualDataL10nID, "website", "Correct name is set");
+ });
+
+ lazy.gDNSOverride.clearHostOverride("www.example.com");
+ resetPrefs();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test that ciphersuites that use 3DES (namely, TLS_RSA_WITH_3DES_EDE_CBC_SHA)
+// can only be enabled when deprecated TLS is enabled.
+add_task(async function onlyAllow3DESWithDeprecatedTLS() {
+ // By default, connecting to a server that only uses 3DES should fail.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ BrowserTestUtils.loadURIString(browser, TRIPLEDES_PAGE);
+ await BrowserTestUtils.waitForErrorPage(browser);
+ }
+ );
+
+ // Enabling deprecated TLS should also enable 3DES.
+ Services.prefs.setBoolPref("security.tls.version.enable-deprecated", true);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ BrowserTestUtils.loadURIString(browser, TRIPLEDES_PAGE);
+ await BrowserTestUtils.browserLoaded(browser, false, TRIPLEDES_PAGE);
+ }
+ );
+
+ // 3DES can be disabled separately.
+ Services.prefs.setBoolPref(
+ "security.ssl3.deprecated.rsa_des_ede3_sha",
+ false
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ BrowserTestUtils.loadURIString(browser, TRIPLEDES_PAGE);
+ await BrowserTestUtils.waitForErrorPage(browser);
+ }
+ );
+
+ resetPrefs();
+});
diff --git a/browser/base/content/test/about/browser_aboutNetError_csp_iframe.js b/browser/base/content/test/about/browser_aboutNetError_csp_iframe.js
new file mode 100644
index 0000000000..21e2ba7b51
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError_csp_iframe.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BLOCKED_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org:8000/browser/browser/base/content/test/about/csp_iframe.sjs";
+
+add_task(async function test_csp() {
+ let { iframePageTab, blockedPageTab } = await setupPage(
+ "iframe_page_csp.html",
+ BLOCKED_PAGE
+ );
+
+ let cspBrowser = gBrowser.selectedTab.linkedBrowser;
+
+ // The blocked page opened in a new window/tab
+ await SpecialPowers.spawn(
+ cspBrowser,
+ [BLOCKED_PAGE],
+ async function (cspBlockedPage) {
+ let cookieHeader = content.document.getElementById("strictCookie");
+ let location = content.document.location.href;
+
+ Assert.ok(
+ cookieHeader.textContent.includes("No same site strict cookie header"),
+ "Same site strict cookie has not been set"
+ );
+ Assert.equal(
+ location,
+ cspBlockedPage,
+ "Location of new page is correct!"
+ );
+ }
+ );
+
+ Services.cookies.removeAll();
+ BrowserTestUtils.removeTab(iframePageTab);
+ BrowserTestUtils.removeTab(blockedPageTab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+async function setupPage(htmlPageName, blockedPage) {
+ let iFramePage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ ) + htmlPageName;
+
+ // Opening the blocked page once in a new tab
+ let blockedPageTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ blockedPage
+ );
+ let blockedPageBrowser = blockedPageTab.linkedBrowser;
+
+ let cookies = Services.cookies.getCookiesFromHost(
+ "example.org",
+ blockedPageBrowser.contentPrincipal.originAttributes
+ );
+ let strictCookie = cookies[0];
+
+ is(
+ strictCookie.value,
+ "green",
+ "Same site strict cookie has the expected value"
+ );
+
+ is(strictCookie.sameSite, 2, "The cookie is a same site strict cookie");
+
+ // Opening the page that contains the iframe
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browser = tab.linkedBrowser;
+ let browserLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ blockedPage,
+ true
+ );
+
+ BrowserTestUtils.loadURIString(browser, iFramePage);
+ await browserLoaded;
+ info("The error page has loaded!");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let iframe = content.document.getElementById("theIframe");
+
+ await ContentTaskUtils.waitForCondition(() =>
+ SpecialPowers.spawn(iframe, [], () =>
+ content.document.body.classList.contains("neterror")
+ )
+ );
+ });
+
+ let iframe = browser.browsingContext.children[0];
+
+ let newTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ // In the iframe, we see the correct error page and click on the button
+ // to open the blocked page in a new window/tab
+ await SpecialPowers.spawn(iframe, [], async function () {
+ let doc = content.document;
+
+ // aboutNetError.mjs is using async localization to format several
+ // messages and in result the translation may be applied later.
+ // We want to return the textContent of the element only after
+ // the translation completes, so let's wait for it here.
+ let elements = [
+ doc.getElementById("errorLongDesc"),
+ doc.getElementById("openInNewWindowButton"),
+ ];
+ await ContentTaskUtils.waitForCondition(() => {
+ return elements.every(elem => !!elem.textContent.trim().length);
+ });
+
+ let textLongDescription = doc.getElementById("errorLongDesc").textContent;
+ let learnMoreLinkLocation = doc.getElementById("learnMoreLink").href;
+
+ Assert.ok(
+ textLongDescription.includes(
+ "To see this page, you need to open it in a new window."
+ ),
+ "Correct error message found"
+ );
+
+ let button = doc.getElementById("openInNewWindowButton");
+ Assert.ok(
+ button.textContent.includes("Open Site in New Window"),
+ "We see the correct button to open the site in a new window"
+ );
+
+ Assert.ok(
+ learnMoreLinkLocation.includes("xframe-neterror-page"),
+ "Correct Learn More URL for CSP error page"
+ );
+
+ // We click on the button
+ await EventUtils.synthesizeMouseAtCenter(button, {}, content);
+ });
+ info("Button was clicked!");
+
+ // We wait for the new tab to load
+ await newTabLoaded;
+ info("The new tab has loaded!");
+
+ let iframePageTab = tab;
+ return {
+ iframePageTab,
+ blockedPageTab,
+ };
+}
diff --git a/browser/base/content/test/about/browser_aboutNetError_native_fallback.js b/browser/base/content/test/about/browser_aboutNetError_native_fallback.js
new file mode 100644
index 0000000000..4a87ad5cce
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError_native_fallback.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let oldProxyType = Services.prefs.getIntPref("network.proxy.type");
+
+function reset() {
+ Services.prefs.clearUserPref("network.trr.display_fallback_warning");
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+ Services.prefs.clearUserPref("doh-rollout.disable-heuristics");
+ Services.prefs.setIntPref("network.proxy.type", oldProxyType);
+ Services.prefs.clearUserPref("network.trr.uri");
+
+ Services.dns.setHeuristicDetectionResult(Ci.nsITRRSkipReason.TRR_OK);
+}
+
+// This helper verifies that the given url loads correctly
+async function verifyLoad(url, testName) {
+ let browser;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
+ browser = gBrowser.selectedBrowser;
+ },
+ true
+ );
+
+ await SpecialPowers.spawn(browser, [{ url, testName }], function (args) {
+ const doc = content.document;
+ ok(
+ doc.documentURI == args.url,
+ "Should have loaded page: " + args.testName
+ );
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+// This helper verifies that loading the given url will lead to an error -- the fallback warning if the parameter is true
+async function verifyError(url, fallbackWarning, testName) {
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+ Services.telemetry.setEventRecordingEnabled("security.doh.neterror", true);
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ await pageLoaded;
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ url, fallbackWarning, testName }],
+ function (args) {
+ const doc = content.document;
+
+ ok(doc.documentURI.startsWith("about:neterror"));
+ "Should be showing error page: " + args.testName;
+
+ const titleEl = doc.querySelector(".title-text");
+ const actualDataL10nID = titleEl.getAttribute("data-l10n-id");
+ if (args.fallbackWarning) {
+ is(
+ actualDataL10nID,
+ "dns-not-found-native-fallback-title2",
+ "Correct fallback warning error page title is set: " + args.testName
+ );
+ } else {
+ ok(
+ actualDataL10nID != "dns-not-found-native-fallback-title2",
+ "Should not show fallback warning: " + args.testName
+ );
+ }
+ }
+ );
+
+ if (fallbackWarning) {
+ let loadEvent = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return events?.find(
+ e => e[1] == "security.doh.neterror" && e[2] == "load"
+ );
+ }, "recorded telemetry for the load");
+ loadEvent.shift();
+ Assert.deepEqual(loadEvent, [
+ "security.doh.neterror",
+ "load",
+ "dohwarning",
+ "NativeFallbackWarning",
+ {
+ mode: "0",
+ provider_key: "0.0.0.0",
+ skip_reason: "TRR_HEURISTIC_TRIPPED_CANARY",
+ },
+ ]);
+ }
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+// This test verifies that the native fallback warning appears in the desired scenarios, and only in those scenarios
+add_task(async function nativeFallbackWarnings() {
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // Disable heuristics since they will attempt to connect to external servers
+ Services.prefs.setBoolPref("doh-rollout.disable-heuristics", true);
+
+ // Set a local TRR to prevent external connections
+ Services.prefs.setCharPref("network.trr.uri", "https://0.0.0.0/dns-query");
+
+ registerCleanupFunction(reset);
+
+ // Test without DoH
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+
+ Services.dns.clearCache(true);
+ await verifyLoad("https://www.example.com/", "valid url, no error");
+
+ // Should not trigger the native fallback warning
+ await verifyError("https://does-not-exist.test", false, "non existent url");
+
+ // We need to disable proxy, otherwise TRR isn't used for name resolution.
+ Services.prefs.setIntPref("network.proxy.type", 0);
+
+ // Switch to TRR first
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ Services.prefs.setBoolPref("network.trr.display_fallback_warning", true);
+
+ // Simulate a tripped canary network
+ Services.dns.setHeuristicDetectionResult(
+ Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_CANARY
+ );
+
+ // We should see the fallback warning displayed in both of these scenarios
+ Services.dns.clearCache(true);
+ await verifyError(
+ "https://www.example.com",
+ true,
+ "canary heuristic tripped"
+ );
+ await verifyError(
+ "https://does-not-exist.test",
+ true,
+ "canary heuristic tripped - non existent url"
+ );
+
+ reset();
+});
diff --git a/browser/base/content/test/about/browser_aboutNetError_trr.js b/browser/base/content/test/about/browser_aboutNetError_trr.js
new file mode 100644
index 0000000000..bfee686e7c
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError_trr.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// See bug 1831731. This test should not actually try to create a connection to
+// the real DoH endpoint. But that may happen when clearing the proxy type, and
+// sometimes even in the next test.
+// To prevent that we override the IP to a local address.
+Cc["@mozilla.org/network/native-dns-override;1"]
+ .getService(Ci.nsINativeDNSResolverOverride)
+ .addIPOverride("mozilla.cloudflare-dns.com", "127.0.0.1");
+
+let oldProxyType = Services.prefs.getIntPref("network.proxy.type");
+function resetPrefs() {
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+ Services.prefs.setIntPref("network.proxy.type", oldProxyType);
+}
+
+async function loadErrorPage() {
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+ // We need to disable proxy, otherwise TRR isn't used for name resolution.
+ Services.prefs.setIntPref("network.proxy.type", 0);
+ registerCleanupFunction(resetPrefs);
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://does-not-exist.test"
+ );
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ await pageLoaded;
+ return browser;
+}
+
+// This test makes sure that the Add exception button only shows up
+// when the skipReason indicates that the domain could not be resolved.
+// If instead there is a problem with the TRR connection, then we don't
+// show the exception button.
+add_task(async function exceptionButtonTRROnly() {
+ let browser = await loadErrorPage();
+
+ await SpecialPowers.spawn(browser, [], function () {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+
+ const titleEl = doc.querySelector(".title-text");
+ const actualDataL10nID = titleEl.getAttribute("data-l10n-id");
+ is(
+ actualDataL10nID,
+ "dns-not-found-trr-only-title2",
+ "Correct error page title is set"
+ );
+
+ let trrExceptionButton = doc.getElementById("trrExceptionButton");
+ Assert.equal(
+ trrExceptionButton.hidden,
+ true,
+ "Exception button should be hidden for TRR service failures"
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ resetPrefs();
+});
+
+add_task(async function TRROnlyExceptionButtonTelemetry() {
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+ Services.telemetry.setEventRecordingEnabled("security.doh.neterror", true);
+
+ let browser = await loadErrorPage();
+
+ await SpecialPowers.spawn(browser, [], function () {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+ });
+
+ let loadEvent = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return events?.find(e => e[1] == "security.doh.neterror" && e[2] == "load");
+ }, "recorded telemetry for the load");
+
+ loadEvent.shift();
+ Assert.deepEqual(loadEvent, [
+ "security.doh.neterror",
+ "load",
+ "dohwarning",
+ "TRROnlyFailure",
+ {
+ mode: "3",
+ provider_key: "mozilla.cloudflare-dns.com",
+ skip_reason: "TRR_UNKNOWN_CHANNEL_FAILURE",
+ },
+ ]);
+
+ await SpecialPowers.spawn(browser, [], function () {
+ const doc = content.document;
+ let buttons = ["neterrorTryAgainButton", "trrSettingsButton"];
+ for (let buttonId of buttons) {
+ let button = doc.getElementById(buttonId);
+ button.click();
+ }
+ });
+
+ // Since we click TryAgain, make sure the error page is loaded again.
+ await BrowserTestUtils.waitForErrorPage(browser);
+
+ is(
+ gBrowser.tabs.length,
+ 3,
+ "Should open about:preferences#privacy-doh in another tab"
+ );
+
+ let clickEvents = await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return events?.filter(
+ e => e[1] == "security.doh.neterror" && e[2] == "click"
+ );
+ },
+ "recorded telemetry for clicking buttons",
+ 500,
+ 100
+ );
+
+ let firstEvent = clickEvents[0];
+ firstEvent.shift(); // remove timestamp
+ Assert.deepEqual(firstEvent, [
+ "security.doh.neterror",
+ "click",
+ "try_again_button",
+ "TRROnlyFailure",
+ {
+ mode: "3",
+ provider_key: "mozilla.cloudflare-dns.com",
+ skip_reason: "TRR_UNKNOWN_CHANNEL_FAILURE",
+ },
+ ]);
+
+ let secondEvent = clickEvents[1];
+ secondEvent.shift(); // remove timestamp
+ Assert.deepEqual(secondEvent, [
+ "security.doh.neterror",
+ "click",
+ "settings_button",
+ "TRROnlyFailure",
+ {
+ mode: "3",
+ provider_key: "mozilla.cloudflare-dns.com",
+ skip_reason: "TRR_UNKNOWN_CHANNEL_FAILURE",
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(gBrowser.tabs[2]);
+ BrowserTestUtils.removeTab(gBrowser.tabs[1]);
+ resetPrefs();
+});
diff --git a/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js b/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js
new file mode 100644
index 0000000000..ae4d5c22a2
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BLOCKED_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org:8000/browser/browser/base/content/test/about/xfo_iframe.sjs";
+
+add_task(async function test_xfo_iframe() {
+ let { iframePageTab, blockedPageTab } = await setupPage(
+ "iframe_page_xfo.html",
+ BLOCKED_PAGE
+ );
+
+ let xfoBrowser = gBrowser.selectedTab.linkedBrowser;
+
+ // The blocked page opened in a new window/tab
+ await SpecialPowers.spawn(
+ xfoBrowser,
+ [BLOCKED_PAGE],
+ async function (xfoBlockedPage) {
+ let cookieHeader = content.document.getElementById("strictCookie");
+ let location = content.document.location.href;
+
+ Assert.ok(
+ cookieHeader.textContent.includes("No same site strict cookie header"),
+ "Same site strict cookie has not been set"
+ );
+ Assert.equal(
+ location,
+ xfoBlockedPage,
+ "Location of new page is correct!"
+ );
+ }
+ );
+
+ Services.cookies.removeAll();
+ BrowserTestUtils.removeTab(iframePageTab);
+ BrowserTestUtils.removeTab(blockedPageTab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+async function setupPage(htmlPageName, blockedPage) {
+ let iFramePage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ ) + htmlPageName;
+
+ // Opening the blocked page once in a new tab
+ let blockedPageTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ blockedPage
+ );
+ let blockedPageBrowser = blockedPageTab.linkedBrowser;
+
+ let cookies = Services.cookies.getCookiesFromHost(
+ "example.org",
+ blockedPageBrowser.contentPrincipal.originAttributes
+ );
+ let strictCookie = cookies[0];
+
+ is(
+ strictCookie.value,
+ "creamy",
+ "Same site strict cookie has the expected value"
+ );
+
+ is(strictCookie.sameSite, 2, "The cookie is a same site strict cookie");
+
+ // Opening the page that contains the iframe
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browser = tab.linkedBrowser;
+ let browserLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ blockedPage,
+ true
+ );
+
+ BrowserTestUtils.loadURIString(browser, iFramePage);
+ await browserLoaded;
+ info("The error page has loaded!");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let iframe = content.document.getElementById("theIframe");
+
+ await ContentTaskUtils.waitForCondition(() =>
+ SpecialPowers.spawn(iframe, [], () =>
+ content.document.body.classList.contains("neterror")
+ )
+ );
+ });
+
+ let frameContext = browser.browsingContext.children[0];
+ let newTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ // In the iframe, we see the correct error page and click on the button
+ // to open the blocked page in a new window/tab
+ await SpecialPowers.spawn(frameContext, [], async function () {
+ let doc = content.document;
+ let textLongDescription = doc.getElementById("errorLongDesc").textContent;
+ let learnMoreLinkLocation = doc.getElementById("learnMoreLink").href;
+
+ Assert.ok(
+ textLongDescription.includes(
+ "To see this page, you need to open it in a new window."
+ ),
+ "Correct error message found"
+ );
+
+ let button = doc.getElementById("openInNewWindowButton");
+ Assert.ok(
+ button.textContent.includes("Open Site in New Window"),
+ "We see the correct button to open the site in a new window"
+ );
+
+ Assert.ok(
+ learnMoreLinkLocation.includes("xframe-neterror-page"),
+ "Correct Learn More URL for XFO error page"
+ );
+
+ // We click on the button
+ await EventUtils.synthesizeMouseAtCenter(button, {}, content);
+ });
+ info("Button was clicked!");
+
+ // We wait for the new tab to load
+ await newTabLoaded;
+ info("The new tab has loaded!");
+
+ let iframePageTab = tab;
+ return {
+ iframePageTab,
+ blockedPageTab,
+ };
+}
diff --git a/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbar.js b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbar.js
new file mode 100644
index 0000000000..c566276d9f
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbar.js
@@ -0,0 +1,311 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function bookmarks_toolbar_shown_on_newtab() {
+ let newtab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+
+ // 1: Test that the toolbar is shown in a newly opened foreground about:newtab
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible on newtab",
+ });
+ ok(isBookmarksToolbarVisible(), "Toolbar should be visible on newtab");
+
+ // 2: Test that the toolbar is hidden when the browser is navigated away from newtab
+ BrowserTestUtils.loadURIString(newtab.linkedBrowser, "https://example.com");
+ await BrowserTestUtils.browserLoaded(newtab.linkedBrowser);
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message:
+ "Toolbar should not be visible on newtab after example.com is loaded within",
+ });
+ ok(
+ !isBookmarksToolbarVisible(),
+ "Toolbar should not be visible on newtab after example.com is loaded within"
+ );
+
+ // 3: Re-load about:newtab in the browser for the following tests and confirm toolbar reappears
+ BrowserTestUtils.loadURIString(newtab.linkedBrowser, "about:newtab");
+ await BrowserTestUtils.browserLoaded(newtab.linkedBrowser);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible on newtab",
+ });
+ ok(isBookmarksToolbarVisible(), "Toolbar should be visible on newtab");
+
+ // 4: Toolbar should get hidden when opening a new tab to example.com
+ let example = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "https://example.com",
+ });
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar should be hidden on example.com",
+ });
+
+ // 5: Toolbar should become visible when switching tabs to newtab
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible with switch to newtab",
+ });
+ ok(isBookmarksToolbarVisible(), "Toolbar is visible with switch to newtab");
+
+ // 6: Toolbar should become hidden when switching tabs to example.com
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar is hidden with switch to example",
+ });
+
+ // 7: Similar to #3 above, loading about:newtab in example should show toolbar
+ BrowserTestUtils.loadURIString(example.linkedBrowser, "about:newtab");
+ await BrowserTestUtils.browserLoaded(example.linkedBrowser);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible with newtab load",
+ });
+ ok(isBookmarksToolbarVisible(), "Toolbar is visible with newtab load");
+
+ // 8: Switching back and forth between two browsers showing about:newtab will still show the toolbar
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ ok(isBookmarksToolbarVisible(), "Toolbar is visible with switch to newtab");
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ ok(
+ isBookmarksToolbarVisible(),
+ "Toolbar is visible with switch to example(newtab)"
+ );
+
+ // 9: With custom newtab URL, toolbar isn't shown on about:newtab but is shown on custom URL
+ let oldNewTab = AboutNewTab.newTabURL;
+ AboutNewTab.newTabURL = "https://example.com/2";
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({ visible: false });
+ ok(!isBookmarksToolbarVisible(), "Toolbar should hide with custom newtab");
+ BrowserTestUtils.loadURIString(example.linkedBrowser, AboutNewTab.newTabURL);
+ await BrowserTestUtils.browserLoaded(example.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await waitForBookmarksToolbarVisibility({ visible: true });
+ ok(
+ isBookmarksToolbarVisible(),
+ "Toolbar is visible with switch to custom newtab"
+ );
+
+ await BrowserTestUtils.removeTab(newtab);
+ await BrowserTestUtils.removeTab(example);
+ AboutNewTab.newTabURL = oldNewTab;
+});
+
+add_task(async function bookmarks_toolbar_open_persisted() {
+ let newtab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ let example = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "https://example.com",
+ });
+ let isToolbarPersistedOpen = () =>
+ Services.prefs.getCharPref("browser.toolbars.bookmarks.visibility") ==
+ "always";
+
+ ok(!isBookmarksToolbarVisible(), "Toolbar is hidden");
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({ visible: true });
+ ok(isBookmarksToolbarVisible(), "Toolbar is visible");
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await waitForBookmarksToolbarVisibility({ visible: false });
+ ok(!isBookmarksToolbarVisible(), "Toolbar is hidden");
+ ok(!isToolbarPersistedOpen(), "Toolbar is not persisted open");
+
+ let contextMenu = document.querySelector("#toolbar-context-menu");
+ let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ EventUtils.synthesizeMouseAtCenter(
+ menuButton,
+ { type: "contextmenu" },
+ window
+ );
+ await popupShown;
+ let bookmarksToolbarMenu = document.querySelector("#toggle_PersonalToolbar");
+ let subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
+ bookmarksToolbarMenu.openMenu(true);
+ await popupShown;
+ let alwaysMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="always"]'
+ );
+ let neverMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="never"]'
+ );
+ let newTabMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="newtab"]'
+ );
+ is(alwaysMenuItem.getAttribute("checked"), "false", "Menuitem isn't checked");
+ is(neverMenuItem.getAttribute("checked"), "false", "Menuitem isn't checked");
+ is(newTabMenuItem.getAttribute("checked"), "true", "Menuitem is checked");
+
+ subMenu.activateItem(alwaysMenuItem);
+
+ await waitForBookmarksToolbarVisibility({ visible: true });
+ popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ menuButton,
+ { type: "contextmenu" },
+ window
+ );
+ await popupShown;
+ bookmarksToolbarMenu = document.querySelector("#toggle_PersonalToolbar");
+ subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
+ bookmarksToolbarMenu.openMenu(true);
+ await popupShown;
+ alwaysMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="always"]'
+ );
+ neverMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="never"]'
+ );
+ newTabMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="newtab"]'
+ );
+ is(alwaysMenuItem.getAttribute("checked"), "true", "Menuitem is checked");
+ is(neverMenuItem.getAttribute("checked"), "false", "Menuitem isn't checked");
+ is(newTabMenuItem.getAttribute("checked"), "false", "Menuitem isn't checked");
+ contextMenu.hidePopup();
+ ok(isBookmarksToolbarVisible(), "Toolbar is visible");
+ ok(isToolbarPersistedOpen(), "Toolbar is persisted open");
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ ok(isBookmarksToolbarVisible(), "Toolbar is visible");
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ ok(isBookmarksToolbarVisible(), "Toolbar is visible");
+
+ popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ menuButton,
+ { type: "contextmenu" },
+ window
+ );
+ await popupShown;
+ bookmarksToolbarMenu = document.querySelector("#toggle_PersonalToolbar");
+ subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
+ bookmarksToolbarMenu.openMenu(true);
+ await popupShown;
+ alwaysMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="always"]'
+ );
+ neverMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="never"]'
+ );
+ newTabMenuItem = document.querySelector(
+ 'menuitem[data-visibility-enum="newtab"]'
+ );
+ is(alwaysMenuItem.getAttribute("checked"), "true", "Menuitem is checked");
+ is(neverMenuItem.getAttribute("checked"), "false", "Menuitem isn't checked");
+ is(newTabMenuItem.getAttribute("checked"), "false", "Menuitem isn't checked");
+ subMenu.activateItem(newTabMenuItem);
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar is hidden",
+ });
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible",
+ });
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar is hidden",
+ });
+
+ await BrowserTestUtils.removeTab(newtab);
+ await BrowserTestUtils.removeTab(example);
+});
+
+add_task(async function test_with_newtabpage_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.enabled", true]],
+ });
+
+ let tabCount = gBrowser.tabs.length;
+ document.getElementById("cmd_newNavigatorTab").doCommand();
+ // Can't use BrowserTestUtils.waitForNewTab since onLocationChange will not
+ // fire due to preloaded new tabs.
+ await TestUtils.waitForCondition(() => gBrowser.tabs.length == tabCount + 1);
+ let newtab = gBrowser.selectedTab;
+ is(newtab.linkedBrowser.currentURI.spec, "about:newtab", "newtab is loaded");
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible with NTP enabled",
+ });
+ let firstid = await SpecialPowers.spawn(newtab.linkedBrowser, [], () => {
+ return content.document.body.firstElementChild?.id;
+ });
+ is(firstid, "root", "new tab page contains content");
+ await BrowserTestUtils.removeTab(newtab);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.enabled", false]],
+ });
+
+ document.getElementById("cmd_newNavigatorTab").doCommand();
+ await TestUtils.waitForCondition(() => gBrowser.tabs.length == tabCount + 1);
+ newtab = gBrowser.selectedTab;
+
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible with NTP disabled",
+ });
+
+ is(
+ newtab.linkedBrowser.currentURI.spec,
+ "about:newtab",
+ "blank new tab is loaded"
+ );
+ firstid = await SpecialPowers.spawn(newtab.linkedBrowser, [], () => {
+ return content.document.body.firstElementChild;
+ });
+ ok(!firstid, "blank new tab page contains no content");
+
+ await BrowserTestUtils.removeTab(newtab);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.enabled", true]],
+ });
+});
+
+add_task(async function test_history_pushstate() {
+ await BrowserTestUtils.withNewTab("https://example.com/", async browser => {
+ await waitForBookmarksToolbarVisibility({ visible: false });
+ ok(!isBookmarksToolbarVisible(), "Toolbar should be hidden");
+
+ // Temporarily show the toolbar:
+ setToolbarVisibility(
+ document.querySelector("#PersonalToolbar"),
+ true,
+ false,
+ false
+ );
+ ok(isBookmarksToolbarVisible(), "Toolbar should now be visible");
+
+ // Now "navigate"
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location.href += "#foo";
+ });
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.value.endsWith("#foo"),
+ "URL bar should update"
+ );
+ ok(isBookmarksToolbarVisible(), "Toolbar should still be visible");
+ });
+});
diff --git a/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js
new file mode 100644
index 0000000000..8e9ef8d163
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const bookmarksInfo = [
+ {
+ title: "firefox",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com",
+ },
+ {
+ title: "rules",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com/2",
+ },
+ {
+ title: "yo",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com/2",
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // Ensure we can wait for about:newtab to load.
+ set: [["browser.newtab.preload", false]],
+ });
+ // Move all existing bookmarks in the Bookmarks Toolbar and
+ // Other Bookmarks to the Bookmarks Menu so they don't affect
+ // the visibility of the Bookmarks Toolbar. Restore them at
+ // the end of the test.
+ let Bookmarks = PlacesUtils.bookmarks;
+ let toolbarBookmarks = [];
+ let unfiledBookmarks = [];
+ let guidBookmarkTuples = [
+ [Bookmarks.toolbarGuid, toolbarBookmarks],
+ [Bookmarks.unfiledGuid, unfiledBookmarks],
+ ];
+ for (let [parentGuid, arr] of guidBookmarkTuples) {
+ await Bookmarks.fetch({ parentGuid }, bookmark => arr.push(bookmark));
+ }
+ await Promise.all(
+ [...toolbarBookmarks, ...unfiledBookmarks].map(async bookmark => {
+ bookmark.parentGuid = Bookmarks.menuGuid;
+ return Bookmarks.update(bookmark);
+ })
+ );
+ registerCleanupFunction(async () => {
+ for (let [parentGuid, arr] of guidBookmarkTuples) {
+ await Promise.all(
+ arr.map(async bookmark => {
+ bookmark.parentGuid = parentGuid;
+ return Bookmarks.update(bookmark);
+ })
+ );
+ }
+ });
+});
+
+add_task(async function bookmarks_toolbar_not_shown_when_empty() {
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarksInfo,
+ });
+ let example = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "https://example.com",
+ });
+ let newtab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ });
+ let emptyMessage = document.getElementById("personal-toolbar-empty");
+
+ // 1: Test that the toolbar is shown in a newly opened foreground about:newtab
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible on newtab",
+ });
+ ok(emptyMessage.hidden, "Empty message is hidden with toolbar populated");
+
+ // 2: Toolbar should get hidden when switching tab to example.com
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar should be hidden on example.com",
+ });
+
+ // 3: Remove all children of the Bookmarks Toolbar and confirm that
+ // the toolbar should not become visible when switching to newtab
+ CustomizableUI.addWidgetToArea(
+ "personal-bookmarks",
+ CustomizableUI.AREA_TABSTRIP
+ );
+ CustomizableUI.removeWidgetFromArea("import-button");
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible when there are no items in the toolbar area",
+ });
+ ok(!emptyMessage.hidden, "Empty message is shown with toolbar empty");
+ // Click the link and check we open the library:
+ let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ EventUtils.synthesizeMouseAtCenter(
+ emptyMessage.querySelector(".text-link"),
+ {}
+ );
+ let libraryWin = await winPromise;
+ is(
+ libraryWin.document.location.href,
+ "chrome://browser/content/places/places.xhtml",
+ "Should have opened library."
+ );
+ await BrowserTestUtils.closeWindow(libraryWin);
+
+ // 4: Put personal-bookmarks back in the toolbar and confirm the toolbar is visible now
+ CustomizableUI.addWidgetToArea(
+ "personal-bookmarks",
+ CustomizableUI.AREA_BOOKMARKS
+ );
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible with Bookmarks Toolbar Items restored",
+ });
+ ok(emptyMessage.hidden, "Empty message is hidden with toolbar populated");
+
+ // 5: Remove all the bookmarks in the toolbar and confirm that the toolbar
+ // is hidden on the New Tab now
+ await PlacesUtils.bookmarks.remove(bookmarks);
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message:
+ "Toolbar is visible when there are no items or nested bookmarks in the toolbar area",
+ });
+ ok(!emptyMessage.hidden, "Empty message is shown with toolbar empty");
+
+ // 6: Add a toolbarbutton and make sure that the toolbar appears when the button is visible
+ CustomizableUI.addWidgetToArea(
+ "characterencoding-button",
+ CustomizableUI.AREA_BOOKMARKS
+ );
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible when there is a visible button in the toolbar",
+ });
+ ok(emptyMessage.hidden, "Empty message is hidden with button in toolbar");
+
+ await BrowserTestUtils.removeTab(newtab);
+ await BrowserTestUtils.removeTab(example);
+ CustomizableUI.reset();
+});
diff --git a/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js
new file mode 100644
index 0000000000..19c990bbbc
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const testCases = [
+ {
+ name: "bookmarks_toolbar_shown_on_newtab_newTabEnabled",
+ newTabEnabled: true,
+ },
+ {
+ name: "bookmarks_toolbar_shown_on_newtab",
+ newTabEnabled: false,
+ },
+];
+
+async function test_bookmarks_toolbar_visibility({ newTabEnabled }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.enabled", newTabEnabled]],
+ });
+
+ // Ensure the toolbar doesnt become visible at any point before the tab finishes loading
+
+ let url =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "slow_loading_page.sjs";
+
+ let startTime = Date.now();
+ let newWindowOpened = BrowserTestUtils.domWindowOpened();
+ let beforeShown = TestUtils.topicObserved("browser-window-before-show");
+
+ openTrustedLinkIn(url, "window");
+
+ let newWin = await newWindowOpened;
+ let slowSiteLoaded = BrowserTestUtils.firstBrowserLoaded(newWin, false);
+
+ function checkToolbarIsCollapsed(win, message) {
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ ok(toolbar && toolbar.collapsed, message);
+ }
+
+ await beforeShown;
+ checkToolbarIsCollapsed(
+ newWin,
+ "Toolbar is initially hidden on the new window"
+ );
+
+ function onToolbarMutation() {
+ checkToolbarIsCollapsed(newWin, "Toolbar should remain collapsed");
+ }
+ let toolbarMutationObserver = new newWin.MutationObserver(onToolbarMutation);
+ toolbarMutationObserver.observe(
+ newWin.document.getElementById("PersonalToolbar"),
+ {
+ attributeFilter: ["collapsed"],
+ }
+ );
+
+ info("Waiting for the slow site to load");
+ await slowSiteLoaded;
+ info(`Window opened and slow site loaded in: ${Date.now() - startTime}ms`);
+
+ checkToolbarIsCollapsed(newWin, "Finally, the toolbar is still hidden");
+
+ toolbarMutationObserver.disconnect();
+ await BrowserTestUtils.closeWindow(newWin);
+}
+
+// Make separate tasks for each test case, so we get more useful stack traces on failure
+for (let testData of testCases) {
+ let tmp = {
+ async [testData.name]() {
+ info("testing with: " + JSON.stringify(testData));
+ await test_bookmarks_toolbar_visibility(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
diff --git a/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarPrefs.js b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarPrefs.js
new file mode 100644
index 0000000000..e9f7768beb
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarPrefs.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(3);
+
+add_task(async function test_with_different_pref_states() {
+ // [prefName, prefValue, toolbarVisibleExampleCom, toolbarVisibleNewTab]
+ let bookmarksToolbarVisibilityStates = [
+ ["browser.toolbars.bookmarks.visibility", "newtab"],
+ ["browser.toolbars.bookmarks.visibility", "always"],
+ ["browser.toolbars.bookmarks.visibility", "never"],
+ ];
+ for (let visibilityState of bookmarksToolbarVisibilityStates) {
+ await SpecialPowers.pushPrefEnv({
+ set: [visibilityState],
+ });
+
+ for (let privateWin of [true, false]) {
+ info(
+ `Testing with ${visibilityState} in a ${
+ privateWin ? "private" : "non-private"
+ } window`
+ );
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: privateWin,
+ });
+ is(
+ win.gBrowser.currentURI.spec,
+ privateWin ? "about:privatebrowsing" : "about:blank",
+ "Expecting about:privatebrowsing or about:blank as URI of new window"
+ );
+
+ if (!privateWin) {
+ await waitForBookmarksToolbarVisibility({
+ win,
+ visible: visibilityState[1] == "always",
+ message:
+ "Toolbar should be visible only if visibilityState is 'always'. State: " +
+ visibilityState[1],
+ });
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ }
+
+ await waitForBookmarksToolbarVisibility({
+ win,
+ visible:
+ visibilityState[1] == "newtab" || visibilityState[1] == "always",
+ message:
+ "Toolbar should be visible as long as visibilityState isn't set to 'never'. State: " +
+ visibilityState[1],
+ });
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ opening: "http://example.com",
+ });
+ await waitForBookmarksToolbarVisibility({
+ win,
+ visible: visibilityState[1] == "always",
+ message:
+ "Toolbar should be visible only if visibilityState is 'always'. State: " +
+ visibilityState[1],
+ });
+ await BrowserTestUtils.closeWindow(win);
+ }
+ }
+});
diff --git a/browser/base/content/test/about/browser_aboutStopReload.js b/browser/base/content/test/about/browser_aboutStopReload.js
new file mode 100644
index 0000000000..66c11a3de3
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutStopReload.js
@@ -0,0 +1,169 @@
+async function waitForNoAnimation(elt) {
+ return TestUtils.waitForCondition(() => !elt.hasAttribute("animate"));
+}
+
+async function getAnimatePromise(elt) {
+ return BrowserTestUtils.waitForAttribute("animate", elt).then(() =>
+ Assert.ok(true, `${elt.id} should animate`)
+ );
+}
+
+function stopReloadMutationCallback() {
+ Assert.ok(
+ false,
+ "stop-reload's animate attribute should not have been mutated"
+ );
+}
+
+// Force-enable the animation
+gReduceMotionOverride = false;
+
+add_task(async function checkDontShowStopOnNewTab() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+ let stopReloadContainerObserver = new MutationObserver(
+ stopReloadMutationCallback
+ );
+
+ await waitForNoAnimation(stopReloadContainer);
+ stopReloadContainerObserver.observe(stopReloadContainer, {
+ attributeFilter: ["animate"],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ waitForStateStop: true,
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ Assert.ok(
+ true,
+ "Test finished: stop-reload does not animate when navigating to local URI on new tab"
+ );
+ stopReloadContainerObserver.disconnect();
+});
+
+add_task(async function checkDontShowStopFromLocalURI() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+ let stopReloadContainerObserver = new MutationObserver(
+ stopReloadMutationCallback
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ waitForStateStop: true,
+ });
+ await waitForNoAnimation(stopReloadContainer);
+ stopReloadContainerObserver.observe(stopReloadContainer, {
+ attributeFilter: ["animate"],
+ });
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:mozilla");
+ BrowserTestUtils.removeTab(tab);
+
+ Assert.ok(
+ true,
+ "Test finished: stop-reload does not animate when navigating between local URIs"
+ );
+ stopReloadContainerObserver.disconnect();
+});
+
+add_task(async function checkDontShowStopFromNonLocalURI() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+ let stopReloadContainerObserver = new MutationObserver(
+ stopReloadMutationCallback
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "https://example.com",
+ waitForStateStop: true,
+ });
+ await waitForNoAnimation(stopReloadContainer);
+ stopReloadContainerObserver.observe(stopReloadContainer, {
+ attributeFilter: ["animate"],
+ });
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:mozilla");
+ BrowserTestUtils.removeTab(tab);
+
+ Assert.ok(
+ true,
+ "Test finished: stop-reload does not animate when navigating to local URI from non-local URI"
+ );
+ stopReloadContainerObserver.disconnect();
+});
+
+add_task(async function checkDoShowStopOnNewTab() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+ let reloadButton = document.getElementById("reload-button");
+ let stopPromise = BrowserTestUtils.waitForAttribute(
+ "displaystop",
+ reloadButton
+ );
+
+ await waitForNoAnimation(stopReloadContainer);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "https://example.com",
+ waitForStateStop: true,
+ });
+ await stopPromise;
+ await waitForNoAnimation(stopReloadContainer);
+ BrowserTestUtils.removeTab(tab);
+
+ info(
+ "Test finished: stop-reload shows stop when navigating to non-local URI during tab opening"
+ );
+});
+
+add_task(async function checkAnimateStopOnTabAfterTabFinishesOpening() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+
+ await waitForNoAnimation(stopReloadContainer);
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ waitForStateStop: true,
+ });
+ await TestUtils.waitForCondition(() => {
+ info(
+ "Waiting for tabAnimationsInProgress to equal 0, currently " +
+ gBrowser.tabAnimationsInProgress
+ );
+ return !gBrowser.tabAnimationsInProgress;
+ });
+ let animatePromise = getAnimatePromise(stopReloadContainer);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "https://example.com");
+ await animatePromise;
+ BrowserTestUtils.removeTab(tab);
+
+ info(
+ "Test finished: stop-reload animates when navigating to non-local URI on new tab after tab has opened"
+ );
+});
+
+add_task(async function checkDoShowStopFromLocalURI() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+
+ await waitForNoAnimation(stopReloadContainer);
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ waitForStateStop: true,
+ });
+ await TestUtils.waitForCondition(() => {
+ info(
+ "Waiting for tabAnimationsInProgress to equal 0, currently " +
+ gBrowser.tabAnimationsInProgress
+ );
+ return !gBrowser.tabAnimationsInProgress;
+ });
+ let animatePromise = getAnimatePromise(stopReloadContainer);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "https://example.com");
+ await animatePromise;
+ await waitForNoAnimation(stopReloadContainer);
+ BrowserTestUtils.removeTab(tab);
+
+ info(
+ "Test finished: stop-reload animates when navigating to non-local URI from local URI"
+ );
+});
diff --git a/browser/base/content/test/about/browser_aboutSupport.js b/browser/base/content/test/about/browser_aboutSupport.js
new file mode 100644
index 0000000000..e846a2b493
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutSupport.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ExperimentAPI } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { ExperimentFakes } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:support" },
+ async function (browser) {
+ let keyLocationServiceGoogleStatus = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let textBox = content.document.getElementById(
+ "key-location-service-google-box"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.l10n.getAttributes(textBox).id,
+ "Google location service API key status loaded"
+ );
+ return content.document.l10n.getAttributes(textBox).id;
+ }
+ );
+ ok(
+ keyLocationServiceGoogleStatus,
+ "Google location service API key status shown"
+ );
+
+ let keySafebrowsingGoogleStatus = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let textBox = content.document.getElementById(
+ "key-safebrowsing-google-box"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.l10n.getAttributes(textBox).id,
+ "Google Safebrowsing API key status loaded"
+ );
+ return content.document.l10n.getAttributes(textBox).id;
+ }
+ );
+ ok(
+ keySafebrowsingGoogleStatus,
+ "Google Safebrowsing API key status shown"
+ );
+
+ let keyMozillaStatus = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let textBox = content.document.getElementById("key-mozilla-box");
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.l10n.getAttributes(textBox).id,
+ "Mozilla API key status loaded"
+ );
+ return content.document.l10n.getAttributes(textBox).id;
+ }
+ );
+ ok(keyMozillaStatus, "Mozilla API key status shown");
+ }
+ );
+});
+
+add_task(async function test_nimbus_experiments() {
+ await ExperimentAPI.ready();
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: { enabled: true },
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:support" },
+ async function (browser) {
+ let experimentName = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector(
+ "#remote-experiments-tbody tr:first-child td"
+ )?.innerText
+ );
+ return content.document.querySelector(
+ "#remote-experiments-tbody tr:first-child td"
+ ).innerText;
+ }
+ );
+ ok(
+ experimentName.match("Nimbus"),
+ "Rendered the expected experiment slug"
+ );
+ }
+ );
+
+ await doExperimentCleanup();
+});
+
+add_task(async function test_remote_configuration() {
+ await ExperimentAPI.ready();
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.aboutwelcome.featureId,
+ value: { enabled: true },
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:support" },
+ async function (browser) {
+ let [userFacingName, branch] = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector(
+ "#remote-features-tbody tr:first-child td"
+ )?.innerText
+ );
+ let rolloutName = content.document.querySelector(
+ "#remote-features-tbody tr:first-child td"
+ ).innerText;
+ let branchName = content.document.querySelector(
+ "#remote-features-tbody tr:first-child td:nth-child(2)"
+ ).innerText;
+
+ return [rolloutName, branchName];
+ }
+ );
+ ok(
+ userFacingName.match("NimbusTestUtils"),
+ "Rendered the expected rollout"
+ );
+ ok(branch.match("aboutwelcome"), "Rendered the expected rollout branch");
+ }
+ );
+
+ await doCleanup();
+});
diff --git a/browser/base/content/test/about/browser_aboutSupport_newtab_security_state.js b/browser/base/content/test/about/browser_aboutSupport_newtab_security_state.js
new file mode 100644
index 0000000000..caa45a1af5
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutSupport_newtab_security_state.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function checkIdentityOfAboutSupport() {
+ let tab = gBrowser.addTab("about:support", {
+ referrerURI: null,
+ inBackground: false,
+ allowThirdPartyFixup: false,
+ relatedToCurrent: false,
+ skipAnimation: true,
+ allowMixedContent: false,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await promiseTabLoaded(tab);
+ let identityBox = document.getElementById("identity-box");
+ is(identityBox.className, "chromeUI", "Should know that we're chrome.");
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/about/browser_aboutSupport_places.js b/browser/base/content/test/about/browser_aboutSupport_places.js
new file mode 100644
index 0000000000..e971de7f0e
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutSupport_places.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_places_db_stats_table() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:support" },
+ async function (browser) {
+ const [initialToggleText, toggleTextAfterShow, toggleTextAfterHide] =
+ await SpecialPowers.spawn(browser, [], async function () {
+ const toggleButton = content.document.getElementById(
+ "place-database-stats-toggle"
+ );
+ const getToggleText = () =>
+ content.document.l10n.getAttributes(toggleButton).id;
+ const toggleTexts = [];
+ const table = content.document.getElementById(
+ "place-database-stats-tbody"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => table.style.display === "none",
+ "Stats table is hidden initially"
+ );
+ toggleTexts.push(getToggleText());
+ toggleButton.click();
+ await ContentTaskUtils.waitForCondition(
+ () => table.style.display === "",
+ "Stats table is shown after first toggle"
+ );
+ toggleTexts.push(getToggleText());
+ toggleButton.click();
+ await ContentTaskUtils.waitForCondition(
+ () => table.style.display === "none",
+ "Stats table is hidden after second toggle"
+ );
+ toggleTexts.push(getToggleText());
+ return toggleTexts;
+ });
+ Assert.equal(initialToggleText, "place-database-stats-show");
+ Assert.equal(toggleTextAfterShow, "place-database-stats-hide");
+ Assert.equal(toggleTextAfterHide, "place-database-stats-show");
+ }
+ );
+});
diff --git a/browser/base/content/test/about/browser_bug435325.js b/browser/base/content/test/about/browser_bug435325.js
new file mode 100644
index 0000000000..70a3b272a9
--- /dev/null
+++ b/browser/base/content/test/about/browser_bug435325.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Ensure that clicking the button in the Offline mode neterror page makes the browser go online. See bug 435325. */
+
+add_task(async function checkSwitchPageToOnlineMode() {
+ // Go offline and disable the proxy and cache, then try to load the test URL.
+ Services.io.offline = true;
+
+ // Tests always connect to localhost, and per bug 87717, localhost is now
+ // reachable in offline mode. To avoid this, disable any proxy.
+ let proxyPrefValue = SpecialPowers.getIntPref("network.proxy.type");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.proxy.type", 0],
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let netErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(browser, "http://example.com/");
+ await netErrorLoaded;
+
+ // Re-enable the proxy so example.com is resolved to localhost, rather than
+ // the actual example.com.
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.proxy.type", proxyPrefValue]],
+ });
+ let changeObserved = TestUtils.topicObserved(
+ "network:offline-status-changed"
+ );
+
+ // Click on the 'Try again' button.
+ await SpecialPowers.spawn(browser, [], async function () {
+ ok(
+ content.document.documentURI.startsWith("about:neterror?e=netOffline"),
+ "Should be showing error page"
+ );
+ content.document
+ .querySelector("#netErrorButtonContainer > .try-again")
+ .click();
+ });
+
+ await changeObserved;
+ ok(
+ !Services.io.offline,
+ "After clicking the 'Try Again' button, we're back online."
+ );
+ });
+});
+
+registerCleanupFunction(function () {
+ Services.io.offline = false;
+});
diff --git a/browser/base/content/test/about/browser_bug633691.js b/browser/base/content/test/about/browser_bug633691.js
new file mode 100644
index 0000000000..33d58475f6
--- /dev/null
+++ b/browser/base/content/test/about/browser_bug633691.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function test() {
+ const URL = "data:text/html,<iframe width='700' height='700'></iframe>";
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ let context = await SpecialPowers.spawn(browser, [], function () {
+ let iframe = content.document.querySelector("iframe");
+ iframe.src = "https://expired.example.com/";
+ return BrowsingContext.getFromWindow(iframe.contentWindow);
+ });
+ await TestUtils.waitForCondition(() => {
+ let frame = context.currentWindowGlobal;
+ return frame && frame.documentURI.spec.startsWith("about:certerror");
+ });
+ await SpecialPowers.spawn(context, [], async function () {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "interactive"
+ );
+ let aP = content.document.getElementById("badCertAdvancedPanel");
+ Assert.ok(aP, "Advanced content should exist");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(aP),
+ "Advanced content should not be visible by default"
+ );
+ });
+ }
+ );
+});
diff --git a/browser/base/content/test/about/csp_iframe.sjs b/browser/base/content/test/about/csp_iframe.sjs
new file mode 100644
index 0000000000..f53ed8498f
--- /dev/null
+++ b/browser/base/content/test/about/csp_iframe.sjs
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ // let's enjoy the amazing CSP setting
+ response.setHeader(
+ "Content-Security-Policy",
+ "frame-ancestors 'self'",
+ false
+ );
+
+ // let's avoid caching issues
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // everything is fine - no needs to worry :)
+ response.setStatusLine(request.httpVersion, 200);
+ response.setHeader("Content-Type", "text/html", false);
+ let txt = "<html><body><h1>CSP Page opened in new window!</h1></body></html>";
+ response.write(txt);
+
+ let cookie = request.hasHeader("Cookie")
+ ? request.getHeader("Cookie")
+ : "<html><body>" +
+ "<h2 id='strictCookie'>No same site strict cookie header</h2>" +
+ "</body></html>";
+ response.write(cookie);
+
+ if (!request.hasHeader("Cookie")) {
+ let strictCookie = `matchaCookie=green; Domain=.example.org; SameSite=Strict`;
+ response.setHeader("Set-Cookie", strictCookie);
+ }
+}
diff --git a/browser/base/content/test/about/dummy_page.html b/browser/base/content/test/about/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/base/content/test/about/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/base/content/test/about/head.js b/browser/base/content/test/about/head.js
new file mode 100644
index 0000000000..c723fbee33
--- /dev/null
+++ b/browser/base/content/test/about/head.js
@@ -0,0 +1,220 @@
+ChromeUtils.defineESModuleGetters(this, {
+ FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+});
+
+SearchTestUtils.init(this);
+
+function getCertChainAsString(certBase64Array) {
+ let certChain = "";
+ for (let cert of certBase64Array) {
+ certChain += getPEMString(cert);
+ }
+ return certChain;
+}
+
+function getPEMString(derb64) {
+ // Wrap the Base64 string into lines of 64 characters,
+ // with CRLF line breaks (as specified in RFC 1421).
+ var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
+ return (
+ "-----BEGIN CERTIFICATE-----\r\n" +
+ wrapped +
+ "\r\n-----END CERTIFICATE-----\r\n"
+ );
+}
+
+async function injectErrorPageFrame(tab, src, sandboxed) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true,
+ null,
+ true
+ );
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [src, sandboxed],
+ async function (frameSrc, frameSandboxed) {
+ let iframe = content.document.createElement("iframe");
+ iframe.src = frameSrc;
+ if (frameSandboxed) {
+ iframe.setAttribute("sandbox", "allow-scripts");
+ }
+ content.document.body.appendChild(iframe);
+ }
+ );
+
+ await loadedPromise;
+}
+
+async function openErrorPage(src, useFrame, sandboxed) {
+ let dummyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+
+ let tab;
+ if (useFrame) {
+ info("Loading cert error page in an iframe");
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, dummyPage);
+ await injectErrorPageFrame(tab, src, sandboxed);
+ } else {
+ let certErrorLoaded;
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, src);
+ let browser = gBrowser.selectedBrowser;
+ certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+ info("Loading and waiting for the cert error");
+ await certErrorLoaded;
+ }
+
+ return tab;
+}
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+ retryTimes = typeof retryTimes !== "undefined" ? retryTimes : 30;
+ var tries = 0;
+ var interval = setInterval(function () {
+ if (tries >= retryTimes) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function () {
+ clearInterval(interval);
+ nextTest();
+ };
+}
+
+function whenTabLoaded(aTab, aCallback) {
+ promiseTabLoadEvent(aTab).then(aCallback);
+}
+
+function promiseTabLoaded(aTab) {
+ return new Promise(resolve => {
+ whenTabLoaded(aTab, resolve);
+ });
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url) {
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+/**
+ * Wait for the search engine to change. searchEngineChangeFn is a function
+ * that will be called to change the search engine.
+ */
+async function promiseContentSearchChange(browser, searchEngineChangeFn) {
+ // Add an event listener manually then perform the action, rather than using
+ // BrowserTestUtils.addContentEventListener as that doesn't add the listener
+ // early enough.
+ await SpecialPowers.spawn(browser, [], async () => {
+ // Store the results in a temporary place.
+ content._searchDetails = {
+ defaultEnginesList: [],
+ listener: event => {
+ if (event.detail.type == "CurrentState") {
+ content._searchDetails.defaultEnginesList.push(
+ content.wrappedJSObject.gContentSearchController.defaultEngine.name
+ );
+ }
+ },
+ };
+
+ // Listen using the system group to ensure that it fires after
+ // the default behaviour.
+ content.addEventListener(
+ "ContentSearchService",
+ content._searchDetails.listener,
+ { mozSystemGroup: true }
+ );
+ });
+
+ let expectedEngineName = await searchEngineChangeFn();
+
+ await SpecialPowers.spawn(
+ browser,
+ [expectedEngineName],
+ async expectedEngineNameChild => {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content._searchDetails.defaultEnginesList &&
+ content._searchDetails.defaultEnginesList[
+ content._searchDetails.defaultEnginesList.length - 1
+ ] == expectedEngineNameChild
+ );
+ content.removeEventListener(
+ "ContentSearchService",
+ content._searchDetails.listener,
+ { mozSystemGroup: true }
+ );
+ delete content._searchDetails;
+ }
+ );
+}
+
+async function waitForBookmarksToolbarVisibility({
+ win = window,
+ visible,
+ message,
+}) {
+ let result = await TestUtils.waitForCondition(() => {
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ return toolbar && (visible ? !toolbar.collapsed : toolbar.collapsed);
+ }, message || "waiting for toolbar to become " + (visible ? "visible" : "hidden"));
+ ok(result, message);
+ return result;
+}
+
+function isBookmarksToolbarVisible(win = window) {
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ return !toolbar.collapsed;
+}
diff --git a/browser/base/content/test/about/iframe_page_csp.html b/browser/base/content/test/about/iframe_page_csp.html
new file mode 100644
index 0000000000..93a23de15d
--- /dev/null
+++ b/browser/base/content/test/about/iframe_page_csp.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Dummy iFrame page</title>
+</head>
+<body>
+<h1>iFrame CSP test</h1>
+<iframe id="theIframe"
+ sandbox="allow-scripts"
+ width=800
+ height=800
+ src="http://example.org:8000/browser/browser/base/content/test/about/csp_iframe.sjs">
+</iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/about/iframe_page_xfo.html b/browser/base/content/test/about/iframe_page_xfo.html
new file mode 100644
index 0000000000..34e7f5cc52
--- /dev/null
+++ b/browser/base/content/test/about/iframe_page_xfo.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Dummy iFrame page</title>
+</head>
+<body>
+<h1>iFrame XFO test</h1>
+<iframe id="theIframe"
+ sandbox="allow-scripts"
+ width=800
+ height=800
+ src="http://example.org:8000/browser/browser/base/content/test/about/xfo_iframe.sjs">
+</iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/about/print_postdata.sjs b/browser/base/content/test/about/print_postdata.sjs
new file mode 100644
index 0000000000..0e3ef38419
--- /dev/null
+++ b/browser/base/content/test/about/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 {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/browser/base/content/test/about/searchSuggestionEngine.sjs b/browser/base/content/test/about/searchSuggestionEngine.sjs
new file mode 100644
index 0000000000..1978b4f665
--- /dev/null
+++ b/browser/base/content/test/about/searchSuggestionEngine.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/base/content/test/about/searchSuggestionEngine.xml b/browser/base/content/test/about/searchSuggestionEngine.xml
new file mode 100644
index 0000000000..409d0b4084
--- /dev/null
+++ b/browser/base/content/test/about/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/base/content/test/about/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/base/content/test/about/slow_loading_page.sjs b/browser/base/content/test/about/slow_loading_page.sjs
new file mode 100644
index 0000000000..747390cdf7
--- /dev/null
+++ b/browser/base/content/test/about/slow_loading_page.sjs
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const DELAY_MS = 400;
+
+const HTML = `<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>hi mom!
+ </body>
+</html>`;
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(
+ () => {
+ resp.setHeader("Cache-Control", "no-cache", false);
+ resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ resp.write(HTML);
+ resp.finish();
+ },
+ DELAY_MS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/browser/base/content/test/about/xfo_iframe.sjs b/browser/base/content/test/about/xfo_iframe.sjs
new file mode 100644
index 0000000000..e8a6352ce0
--- /dev/null
+++ b/browser/base/content/test/about/xfo_iframe.sjs
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ // let's enjoy the amazing XFO setting
+ response.setHeader("X-Frame-Options", "SAMEORIGIN");
+
+ // let's avoid caching issues
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // everything is fine - no needs to worry :)
+ response.setStatusLine(request.httpVersion, 200);
+
+ response.setHeader("Content-Type", "text/html", false);
+ let txt =
+ "<html><head><title>XFO page</title></head>" +
+ "<body><h1>" +
+ "XFO blocked page opened in new window!" +
+ "</h1></body></html>";
+ response.write(txt);
+
+ let cookie = request.hasHeader("Cookie")
+ ? request.getHeader("Cookie")
+ : "<html><body>" +
+ "<h2 id='strictCookie'>No same site strict cookie header</h2></body>" +
+ "</html>";
+ response.write(cookie);
+
+ if (!request.hasHeader("Cookie")) {
+ let strictCookie = `matchaCookie=creamy; Domain=.example.org; SameSite=Strict`;
+ response.setHeader("Set-Cookie", strictCookie);
+ }
+}
diff --git a/browser/base/content/test/alerts/browser.ini b/browser/base/content/test/alerts/browser.ini
new file mode 100644
index 0000000000..c06f0d03d9
--- /dev/null
+++ b/browser/base/content/test/alerts/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+support-files =
+ head.js
+ file_dom_notifications.html
+
+[browser_notification_close.js]
+https_first_disabled = true
+skip-if = os == 'win' # Bug 1227785
+[browser_notification_do_not_disturb.js]
+https_first_disabled = true
+[browser_notification_open_settings.js]
+https_first_disabled = true
+skip-if = os == 'win' # Bug 1411118
+[browser_notification_remove_permission.js]
+https_first_disabled = true
+skip-if = os == 'win' # Bug 1411118
+[browser_notification_replace.js]
+https_first_disabled = true
+skip-if = os == 'win' # Bug 1422928
+[browser_notification_tab_switching.js]
+https_first_disabled = true
+skip-if = os == 'win' # Bug 1243263
diff --git a/browser/base/content/test/alerts/browser_notification_close.js b/browser/base/content/test/alerts/browser_notification_close.js
new file mode 100644
index 0000000000..2d71db31a0
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_close.js
@@ -0,0 +1,107 @@
+"use strict";
+
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+let notificationURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+let oldShowFavicons;
+
+add_task(async function test_notificationClose() {
+ let notificationURI = makeURI(notificationURL);
+ await addNotificationPermission(notificationURL);
+
+ oldShowFavicons = Services.prefs.getBoolPref("alerts.showFavicons");
+ Services.prefs.setBoolPref("alerts.showFavicons", true);
+
+ await PlacesTestUtils.addVisits(notificationURI);
+ let faviconURI = await new Promise(resolve => {
+ let uri = makeURI(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"
+ );
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ notificationURI,
+ uri,
+ true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ uriResult => resolve(uriResult),
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: notificationURL,
+ },
+ async function dummyTabTask(aBrowser) {
+ await openNotification(aBrowser, "showNotification2");
+
+ info("Notification alert showing");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ await closeNotification(aBrowser);
+ return;
+ }
+
+ let alertTitleLabel =
+ alertWindow.document.getElementById("alertTitleLabel");
+ is(
+ alertTitleLabel.value,
+ "Test title",
+ "Title text of notification should be present"
+ );
+ let alertTextLabel =
+ alertWindow.document.getElementById("alertTextLabel");
+ is(
+ alertTextLabel.textContent,
+ "Test body 2",
+ "Body text of notification should be present"
+ );
+ let alertIcon = alertWindow.document.getElementById("alertIcon");
+ is(
+ alertIcon.src,
+ faviconURI.spec,
+ "Icon of notification should be present"
+ );
+
+ let alertCloseButton = alertWindow.document.querySelector(".close-icon");
+ is(alertCloseButton.localName, "toolbarbutton", "close button found");
+ let promiseBeforeUnloadEvent = BrowserTestUtils.waitForEvent(
+ alertWindow,
+ "beforeunload"
+ );
+ let closedTime = alertWindow.Date.now();
+ alertCloseButton.click();
+ info("Clicked on close button");
+ await promiseBeforeUnloadEvent;
+
+ ok(true, "Alert should close when the close button is clicked");
+ let currentTime = alertWindow.Date.now();
+ // The notification will self-close at 12 seconds, so this checks
+ // that the notification closed before the timeout.
+ ok(
+ currentTime - closedTime < 5000,
+ "Close requested at " +
+ closedTime +
+ ", actually closed at " +
+ currentTime
+ );
+ }
+ );
+});
+
+add_task(async function cleanup() {
+ PermissionTestUtils.remove(notificationURL, "desktop-notification");
+ if (typeof oldShowFavicons == "boolean") {
+ Services.prefs.setBoolPref("alerts.showFavicons", oldShowFavicons);
+ }
+});
diff --git a/browser/base/content/test/alerts/browser_notification_do_not_disturb.js b/browser/base/content/test/alerts/browser_notification_do_not_disturb.js
new file mode 100644
index 0000000000..8fb5a8a52b
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_do_not_disturb.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that notifications can be silenced using nsIAlertsDoNotDisturb
+ * on systems where that interface and its methods are implemented for
+ * the nsIAlertService.
+ */
+
+const ALERT_SERVICE = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIAlertsDoNotDisturb);
+
+const PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+
+// The amount of time in seconds that we will wait for a notification
+// to show up before we decide that it's not coming.
+const NOTIFICATION_TIMEOUT_SECS = 2000;
+
+add_setup(async function () {
+ await addNotificationPermission(PAGE);
+});
+
+/**
+ * Test that the manualDoNotDisturb attribute can prevent
+ * notifications from appearing.
+ */
+add_task(async function test_manualDoNotDisturb() {
+ try {
+ // Only run the test if the do-not-disturb
+ // interface has been implemented.
+ ALERT_SERVICE.manualDoNotDisturb;
+ ok(true, "Alert service implements do-not-disturb interface");
+ } catch (e) {
+ ok(
+ true,
+ "Alert service doesn't implement do-not-disturb interface, exiting test"
+ );
+ return;
+ }
+
+ // In the event that something goes wrong during this test, make sure
+ // we put the attribute back to the default setting when this test file
+ // exits.
+ registerCleanupFunction(() => {
+ ALERT_SERVICE.manualDoNotDisturb = false;
+ });
+
+ // Make sure that do-not-disturb is not enabled before we start.
+ ok(
+ !ALERT_SERVICE.manualDoNotDisturb,
+ "Alert service should not be disabled when test starts"
+ );
+
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ await openNotification(browser, "showNotification2");
+
+ info("Notification alert showing");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+
+ // For now, only the XUL alert backend implements the manualDoNotDisturb
+ // method for nsIAlertsDoNotDisturb, so we expect there to be a XUL alert
+ // window. If the method gets implemented by native backends in the future,
+ // we'll probably want to branch here and set the manualDoNotDisturb
+ // attribute manually.
+ ok(alertWindow, "Expected a XUL alert window.");
+
+ // We're using the XUL notification backend. This means that there's
+ // a menuitem for enabling manualDoNotDisturb. We exercise that
+ // menuitem here.
+ let doNotDisturbMenuItem = alertWindow.document.getElementById(
+ "doNotDisturbMenuItem"
+ );
+ is(doNotDisturbMenuItem.localName, "menuitem", "menuitem found");
+
+ let unloadPromise = BrowserTestUtils.waitForEvent(
+ alertWindow,
+ "beforeunload"
+ );
+
+ doNotDisturbMenuItem.click();
+ info("Clicked on do-not-disturb menuitem");
+ await unloadPromise;
+
+ // At this point, we should be configured to not display notifications
+ // to the user.
+ ok(
+ ALERT_SERVICE.manualDoNotDisturb,
+ "Alert service should be disabled after clicking menuitem"
+ );
+
+ // The notification should not appear, but there is no way from the
+ // client-side to know that it was blocked, except for waiting some time
+ // and realizing that the "onshow" event never fired.
+ await Assert.rejects(
+ openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
+ /timed out/,
+ "The notification should never display."
+ );
+
+ ALERT_SERVICE.manualDoNotDisturb = false;
+ });
+});
+
+/**
+ * Test that the suppressForScreenSharing attribute can prevent
+ * notifications from appearing.
+ */
+add_task(async function test_suppressForScreenSharing() {
+ try {
+ // Only run the test if the do-not-disturb
+ // interface has been implemented.
+ ALERT_SERVICE.suppressForScreenSharing;
+ ok(true, "Alert service implements do-not-disturb interface");
+ } catch (e) {
+ ok(
+ true,
+ "Alert service doesn't implement do-not-disturb interface, exiting test"
+ );
+ return;
+ }
+
+ // In the event that something goes wrong during this test, make sure
+ // we put the attribute back to the default setting when this test file
+ // exits.
+ registerCleanupFunction(() => {
+ ALERT_SERVICE.suppressForScreenSharing = false;
+ });
+
+ // Make sure that do-not-disturb is not enabled before we start.
+ ok(
+ !ALERT_SERVICE.suppressForScreenSharing,
+ "Alert service should not be suppressing for screen sharing when test " +
+ "starts"
+ );
+
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ await openNotification(browser, "showNotification2");
+
+ info("Notification alert showing");
+ await closeNotification(browser);
+ ALERT_SERVICE.suppressForScreenSharing = true;
+
+ // The notification should not appear, but there is no way from the
+ // client-side to know that it was blocked, except for waiting some time
+ // and realizing that the "onshow" event never fired.
+ await Assert.rejects(
+ openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
+ /timed out/,
+ "The notification should never display."
+ );
+ });
+
+ ALERT_SERVICE.suppressForScreenSharing = false;
+});
diff --git a/browser/base/content/test/alerts/browser_notification_open_settings.js b/browser/base/content/test/alerts/browser_notification_open_settings.js
new file mode 100644
index 0000000000..ed51cd782b
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_open_settings.js
@@ -0,0 +1,80 @@
+"use strict";
+
+var notificationURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+var expectedURL = "about:preferences#privacy";
+
+add_task(async function test_settingsOpen_observer() {
+ info(
+ "Opening a dummy tab so openPreferences=>switchToTabHavingURI doesn't use the blank tab."
+ );
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:robots",
+ },
+ async function dummyTabTask(aBrowser) {
+ // Ensure preferences is loaded before removing the tab.
+ let syncPaneLoadedPromise = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
+ info("simulate a notifications-open-settings notification");
+ let uri = NetUtil.newURI("https://example.com");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ Services.obs.notifyObservers(principal, "notifications-open-settings");
+ let tab = await tabPromise;
+ ok(tab, "The notification settings tab opened");
+ await syncPaneLoadedPromise;
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_settingsOpen_button() {
+ info("Adding notification permission");
+ await addNotificationPermission(notificationURL);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: notificationURL,
+ },
+ async function tabTask(aBrowser) {
+ info("Waiting for notification");
+ await openNotification(aBrowser, "showNotification2");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ await closeNotification(aBrowser);
+ return;
+ }
+
+ // Ensure preferences is loaded before removing the tab.
+ let syncPaneLoadedPromise = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ let closePromise = promiseWindowClosed(alertWindow);
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
+ let openSettingsMenuItem = alertWindow.document.getElementById(
+ "openSettingsMenuItem"
+ );
+ openSettingsMenuItem.click();
+
+ info("Waiting for notification settings tab");
+ let tab = await tabPromise;
+ ok(tab, "The notification settings tab opened");
+
+ await syncPaneLoadedPromise;
+ await closePromise;
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
diff --git a/browser/base/content/test/alerts/browser_notification_remove_permission.js b/browser/base/content/test/alerts/browser_notification_remove_permission.js
new file mode 100644
index 0000000000..ba198870a3
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_remove_permission.js
@@ -0,0 +1,86 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+var tab;
+var notificationURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+var alertWindowClosed = false;
+var permRemoved = false;
+
+function test() {
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ window.restore();
+ });
+
+ addNotificationPermission(notificationURL).then(function openTab() {
+ tab = BrowserTestUtils.addTab(gBrowser, notificationURL);
+ gBrowser.selectedTab = tab;
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => onLoad());
+ });
+}
+
+function onLoad() {
+ openNotification(tab.linkedBrowser, "showNotification2").then(onAlertShowing);
+}
+
+function onAlertShowing() {
+ info("Notification alert showing");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ closeNotification(tab.linkedBrowser).then(finish);
+ return;
+ }
+ ok(
+ PermissionTestUtils.testExactPermission(
+ notificationURL,
+ "desktop-notification"
+ ),
+ "Permission should exist prior to removal"
+ );
+ let disableForOriginMenuItem = alertWindow.document.getElementById(
+ "disableForOriginMenuItem"
+ );
+ is(disableForOriginMenuItem.localName, "menuitem", "menuitem found");
+ Services.obs.addObserver(permObserver, "perm-changed");
+ alertWindow.addEventListener("beforeunload", onAlertClosing);
+ disableForOriginMenuItem.click();
+ info("Clicked on disable-for-origin menuitem");
+}
+
+function permObserver(subject, topic, data) {
+ if (topic != "perm-changed") {
+ return;
+ }
+
+ let permission = subject.QueryInterface(Ci.nsIPermission);
+ is(
+ permission.type,
+ "desktop-notification",
+ "desktop-notification permission changed"
+ );
+ is(data, "deleted", "desktop-notification permission deleted");
+
+ Services.obs.removeObserver(permObserver, "perm-changed");
+ permRemoved = true;
+ if (alertWindowClosed) {
+ finish();
+ }
+}
+
+function onAlertClosing(event) {
+ event.target.removeEventListener("beforeunload", onAlertClosing);
+
+ alertWindowClosed = true;
+ if (permRemoved) {
+ finish();
+ }
+}
diff --git a/browser/base/content/test/alerts/browser_notification_replace.js b/browser/base/content/test/alerts/browser_notification_replace.js
new file mode 100644
index 0000000000..9c72e90ab1
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_replace.js
@@ -0,0 +1,66 @@
+"use strict";
+
+let notificationURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+
+add_task(async function test_notificationReplace() {
+ await addNotificationPermission(notificationURL);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: notificationURL,
+ },
+ async function dummyTabTask(aBrowser) {
+ await SpecialPowers.spawn(aBrowser, [], async function () {
+ let win = content.window.wrappedJSObject;
+ let notification = win.showNotification1();
+ let promiseCloseEvent = ContentTaskUtils.waitForEvent(
+ notification,
+ "close"
+ );
+
+ let showEvent = await ContentTaskUtils.waitForEvent(
+ notification,
+ "show"
+ );
+ Assert.equal(
+ showEvent.target.body,
+ "Test body 1",
+ "Showed tagged notification"
+ );
+
+ let newNotification = win.showNotification2();
+ let newShowEvent = await ContentTaskUtils.waitForEvent(
+ newNotification,
+ "show"
+ );
+ Assert.equal(
+ newShowEvent.target.body,
+ "Test body 2",
+ "Showed new notification with same tag"
+ );
+
+ let closeEvent = await promiseCloseEvent;
+ Assert.equal(
+ closeEvent.target.body,
+ "Test body 1",
+ "Closed previous tagged notification"
+ );
+
+ let promiseNewCloseEvent = ContentTaskUtils.waitForEvent(
+ newNotification,
+ "close"
+ );
+ newNotification.close();
+ let newCloseEvent = await promiseNewCloseEvent;
+ Assert.equal(
+ newCloseEvent.target.body,
+ "Test body 2",
+ "Closed new notification"
+ );
+ });
+ }
+ );
+});
diff --git a/browser/base/content/test/alerts/browser_notification_tab_switching.js b/browser/base/content/test/alerts/browser_notification_tab_switching.js
new file mode 100644
index 0000000000..ee675670cb
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_tab_switching.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+var tab;
+var notification;
+var notificationURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+var newWindowOpenedFromTab;
+
+add_task(async function test_notificationPreventDefaultAndSwitchTabs() {
+ await addNotificationPermission(notificationURL);
+
+ let originalTab = gBrowser.selectedTab;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: notificationURL,
+ },
+ async function dummyTabTask(aBrowser) {
+ // Put new tab in background so it is obvious when it is re-focused.
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+ isnot(
+ gBrowser.selectedBrowser,
+ aBrowser,
+ "Notification page loaded as a background tab"
+ );
+
+ // First, show a notification that will be have the tab-switching prevented.
+ function promiseNotificationEvent(evt) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [evt],
+ async function (contentEvt) {
+ return new Promise(resolve => {
+ let contentNotification = content.wrappedJSObject._notification;
+ contentNotification.addEventListener(
+ contentEvt,
+ function (event) {
+ resolve({ defaultPrevented: event.defaultPrevented });
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+ }
+ await openNotification(aBrowser, "showNotification1");
+ info("Notification alert showing");
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ await closeNotification(aBrowser);
+ return;
+ }
+ info("Clicking on notification");
+ let promiseClickEvent = promiseNotificationEvent("click");
+
+ // NB: This executeSoon is needed to allow the non-e10s runs of this test
+ // a chance to set the event listener on the page. Otherwise, we
+ // synchronously fire the click event before we listen for the event.
+ executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(
+ alertWindow.document.getElementById("alertTitleLabel"),
+ {},
+ alertWindow
+ );
+ });
+ let clickEvent = await promiseClickEvent;
+ ok(
+ clickEvent.defaultPrevented,
+ "The event handler for the first notification cancels the event"
+ );
+ isnot(
+ gBrowser.selectedBrowser,
+ aBrowser,
+ "Notification page still a background tab"
+ );
+ let notificationClosed = promiseNotificationEvent("close");
+ await closeNotification(aBrowser);
+ await notificationClosed;
+
+ // Second, show a notification that will cause the tab to get switched.
+ await openNotification(aBrowser, "showNotification2");
+ alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ let promiseTabSelect = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabSelect"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ alertWindow.document.getElementById("alertTitleLabel"),
+ {},
+ alertWindow
+ );
+ await promiseTabSelect;
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ notificationURL,
+ "Clicking on the second notification should select its originating tab"
+ );
+ notificationClosed = promiseNotificationEvent("close");
+ await closeNotification(aBrowser);
+ await notificationClosed;
+ }
+ );
+});
+
+add_task(async function cleanup() {
+ PermissionTestUtils.remove(notificationURL, "desktop-notification");
+});
diff --git a/browser/base/content/test/alerts/file_dom_notifications.html b/browser/base/content/test/alerts/file_dom_notifications.html
new file mode 100644
index 0000000000..6deede8fcf
--- /dev/null
+++ b/browser/base/content/test/alerts/file_dom_notifications.html
@@ -0,0 +1,39 @@
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+"use strict";
+
+function showNotification1() {
+ var options = {
+ dir: undefined,
+ lang: undefined,
+ body: "Test body 1",
+ tag: "Test tag",
+ icon: undefined,
+ };
+ var n = new Notification("Test title", options);
+ n.addEventListener("click", function(event) {
+ event.preventDefault();
+ });
+ return n;
+}
+
+function showNotification2() {
+ var options = {
+ dir: undefined,
+ lang: undefined,
+ body: "Test body 2",
+ tag: "Test tag",
+ icon: undefined,
+ };
+ return new Notification("Test title", options);
+}
+</script>
+</head>
+<body>
+<form id="notificationForm" onsubmit="showNotification();">
+ <input type="submit" value="Show notification" id="submit"/>
+</form>
+</body>
+</html>
diff --git a/browser/base/content/test/alerts/head.js b/browser/base/content/test/alerts/head.js
new file mode 100644
index 0000000000..4be18f6c41
--- /dev/null
+++ b/browser/base/content/test/alerts/head.js
@@ -0,0 +1,73 @@
+// Platforms may default to reducing motion. We override this to ensure the
+// alert slide animation is enabled in tests.
+SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 0]],
+});
+
+async function addNotificationPermission(originString) {
+ return SpecialPowers.pushPermissions([
+ {
+ type: "desktop-notification",
+ allow: true,
+ context: originString,
+ },
+ ]);
+}
+
+/**
+ * Similar to `BrowserTestUtils.closeWindow`, but
+ * doesn't call `window.close()`.
+ */
+function promiseWindowClosed(window) {
+ return new Promise(function (resolve) {
+ Services.ww.registerNotification(function observer(subject, topic, data) {
+ if (topic == "domwindowclosed" && subject == window) {
+ Services.ww.unregisterNotification(observer);
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * These two functions work with file_dom_notifications.html to open the
+ * notification and close it.
+ *
+ * |fn| can be showNotification1 or showNotification2.
+ * if |timeout| is passed, then the promise returned from this function is
+ * rejected after the requested number of miliseconds.
+ */
+function openNotification(aBrowser, fn, timeout) {
+ info(`openNotification: ${fn}`);
+ return SpecialPowers.spawn(
+ aBrowser,
+ [[fn, timeout]],
+ async function ([contentFn, contentTimeout]) {
+ await new Promise((resolve, reject) => {
+ let win = content.wrappedJSObject;
+ let notification = win[contentFn]();
+ win._notification = notification;
+
+ function listener() {
+ notification.removeEventListener("show", listener);
+ resolve();
+ }
+
+ notification.addEventListener("show", listener);
+
+ if (contentTimeout) {
+ content.setTimeout(() => {
+ notification.removeEventListener("show", listener);
+ reject("timed out");
+ }, contentTimeout);
+ }
+ });
+ }
+ );
+}
+
+function closeNotification(aBrowser) {
+ return SpecialPowers.spawn(aBrowser, [], function () {
+ content.wrappedJSObject._notification.close();
+ });
+}
diff --git a/browser/base/content/test/backforward/browser.ini b/browser/base/content/test/backforward/browser.ini
new file mode 100644
index 0000000000..2bfb071ee4
--- /dev/null
+++ b/browser/base/content/test/backforward/browser.ini
@@ -0,0 +1,2 @@
+[browser_history_menu.js]
+https_first_disabled = true
diff --git a/browser/base/content/test/backforward/browser_history_menu.js b/browser/base/content/test/backforward/browser_history_menu.js
new file mode 100644
index 0000000000..b812a6ae24
--- /dev/null
+++ b/browser/base/content/test/backforward/browser_history_menu.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test verifies that the back forward button long-press menu and context menu
+// shows the correct history items.
+
+add_task(async function mousedown_back() {
+ await testBackForwardMenu(false);
+});
+
+add_task(async function contextmenu_back() {
+ await testBackForwardMenu(true);
+});
+
+async function openHistoryMenu(useContextMenu) {
+ let backButton = document.getElementById("back-button");
+ let rect = backButton.getBoundingClientRect();
+
+ info("waiting for the history menu to open");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ useContextMenu ? document.getElementById("backForwardMenu") : backButton,
+ "popupshown"
+ );
+ if (useContextMenu) {
+ EventUtils.synthesizeMouseAtCenter(backButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ } else {
+ EventUtils.synthesizeMouseAtCenter(backButton, { type: "mousedown" });
+ }
+
+ EventUtils.synthesizeMouse(backButton, rect.width / 2, rect.height, {
+ type: "mouseup",
+ });
+ let popupEvent = await popupShownPromise;
+
+ ok(true, "history menu opened");
+
+ return popupEvent;
+}
+
+async function testBackForwardMenu(useContextMenu) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ );
+
+ for (let iter = 2; iter <= 4; iter++) {
+ // Iterate three times. For the first two times through the loop, add a new history item.
+ // But for the last iteration, go back in the history instead.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [iter],
+ async function (iterChild) {
+ if (iterChild == 4) {
+ let popStatePromise = new Promise(function (resolve) {
+ content.onpopstate = resolve;
+ });
+ content.history.back();
+ await popStatePromise;
+ } else {
+ content.history.pushState({}, "" + iterChild, iterChild + ".html");
+ }
+ }
+ );
+
+ // Wait for the session data to be flushed before continuing the test
+ await new Promise(resolve =>
+ SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
+ );
+
+ let popupEvent = await openHistoryMenu(useContextMenu);
+
+ // Wait for the session data to be flushed before continuing the test
+ await new Promise(resolve =>
+ SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
+ );
+
+ is(
+ popupEvent.target.children.length,
+ iter > 3 ? 3 : iter,
+ "Correct number of history items"
+ );
+
+ let node = popupEvent.target.lastElementChild;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(node.getAttribute("uri"), "http://example.com/", "'1' item uri");
+ is(node.getAttribute("index"), "0", "'1' item index");
+ is(
+ node.getAttribute("historyindex"),
+ iter == 3 ? "-2" : "-1",
+ "'1' item historyindex"
+ );
+
+ node = node.previousElementSibling;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(node.getAttribute("uri"), "http://example.com/2.html", "'2' item uri");
+ is(node.getAttribute("index"), "1", "'2' item index");
+ is(
+ node.getAttribute("historyindex"),
+ iter == 3 ? "-1" : "0",
+ "'2' item historyindex"
+ );
+
+ if (iter >= 3) {
+ node = node.previousElementSibling;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(node.getAttribute("uri"), "http://example.com/3.html", "'3' item uri");
+ is(node.getAttribute("index"), "2", "'3' item index");
+ is(
+ node.getAttribute("historyindex"),
+ iter == 4 ? "1" : "0",
+ "'3' item historyindex"
+ );
+ }
+
+ // Close the popup, but on the last iteration, click on one of the history items
+ // to ensure it opens in a new tab.
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ popupEvent.target,
+ "popuphidden"
+ );
+
+ if (iter < 4) {
+ popupEvent.target.hidePopup();
+ } else {
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url => url == "http://example.com/"
+ );
+
+ popupEvent.target.activateItem(popupEvent.target.children[2], {
+ button: 1,
+ });
+
+ let newtab = await newTabPromise;
+ gBrowser.removeTab(newtab);
+ }
+
+ await popupHiddenPromise;
+ }
+
+ gBrowser.removeTab(tab);
+}
+
+// Make sure that the history popup appears after navigating around in a preferences page.
+add_task(async function test_preferences_page() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+
+ openPreferences("search");
+ let popupEvent = await openHistoryMenu(true);
+
+ // Wait for the session data to be flushed before continuing the test
+ await new Promise(resolve =>
+ SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
+ );
+
+ is(popupEvent.target.children.length, 2, "Correct number of history items");
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ popupEvent.target,
+ "popuphidden"
+ );
+ popupEvent.target.hidePopup();
+ await popupHiddenPromise;
+
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/caps/browser.ini b/browser/base/content/test/caps/browser.ini
new file mode 100644
index 0000000000..18464cdf77
--- /dev/null
+++ b/browser/base/content/test/caps/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+
+[browser_principalSerialization_csp.js]
+[browser_principalSerialization_json.js]
+skip-if = debug # deliberately bypass assertions when deserializing. Bug 965637 removed the CSP from Principals, but the remaining bits in such Principals should deserialize correctly.
+[browser_principalSerialization_version1.js]
diff --git a/browser/base/content/test/caps/browser_principalSerialization_csp.js b/browser/base/content/test/caps/browser_principalSerialization_csp.js
new file mode 100644
index 0000000000..909e728794
--- /dev/null
+++ b/browser/base/content/test/caps/browser_principalSerialization_csp.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Within Bug 965637 we move the CSP away from the Principal. Serialized Principals however
+ * might still have CSPs serialized within them. This tests ensures that we do not
+ * encounter a memory corruption when deserializing. It's fine that the deserialized
+ * CSP is null, but the Principal itself should deserialize correctly.
+ */
+
+add_task(async function test_deserialize_principal_with_csp() {
+ /*
+ This test should be resilient to changes in principal serialization, if these are failing then it's likely the code will break session storage.
+ To recreate this for another version, copy the function into the browser console, browse some pages and printHistory.
+
+ Generated with:
+ function printHistory() {
+ let tests = [];
+ let entries = SessionStore.getSessionHistory(gBrowser.selectedTab).entries.map((entry) => { return entry.triggeringPrincipal_base64 });
+ entries.push(E10SUtils.serializePrincipal(gBrowser.selectedTab.linkedBrowser._contentPrincipal));
+ for (let entry of entries) {
+ console.log(entry);
+ let testData = {};
+ testData.input = entry;
+ let principal = E10SUtils.deserializePrincipal(testData.input);
+ testData.output = {};
+ if (principal.URI === null) {
+ testData.output.URI = false;
+ } else {
+ testData.output.URISpec = principal.URI.spec;
+ }
+ testData.output.originAttributes = principal.originAttributes;
+ testData.output.cspJSON = principal.cspJSON;
+
+ tests.push(testData);
+ }
+ return tests;
+ }
+ printHistory(); // Copy this into: serializedPrincipalsFromFirefox
+ */
+
+ let serializedPrincipalsFromFirefox = [
+ {
+ input:
+ "ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAHmh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLwAAAAAAAAAFAAAACAAAAA8AAAAA/////wAAAAD/////AAAACAAAAA8AAAAXAAAABwAAABcAAAAHAAAAFwAAAAcAAAAeAAAAAAAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AQAAAAAAAAAAAAAAAQnZ7Rrl1EAEv+Anzrkj2ayzxMCuvV5MrYfgjSENuz+fAd6UctCANBHTk5kAEEug/UCSBzpUbXhPMJE6uHGBMgjGAAAAAv////8AAAG7AQAAAB5odHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8AAAAAAAAABQAAAAgAAAAPAAAAAP////8AAAAA/////wAAAAgAAAAPAAAAFwAAAAcAAAAXAAAABwAAABcAAAAHAAAAHgAAAAAAAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wEAAAAAAAAAAAABAAAFtgBzAGMAcgBpAHAAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdABhAGcAbQBhAG4AYQBnAGUAcgAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AcwAuAHkAdABpAG0AZwAuAGMAbwBtADsAIABpAG0AZwAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABkAGEAdABhADoAIABoAHQAdABwAHMAOgAvAC8AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AZABlACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBkAGsAIABoAHQAdABwAHMAOgAvAC8AYwByAGUAYQB0AGkAdgBlAGMAbwBtAG0AbwBuAHMALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwBhAGQALgBkAG8AdQBiAGwAZQBjAGwAaQBjAGsALgBuAGUAdAA7ACAAZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AOwAgAGYAcgBhAG0AZQAtAHMAcgBjACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAtAG4AbwBjAG8AbwBrAGkAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAcgBhAGMAawBlAHIAdABlAHMAdAAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AcwB1AHIAdgBlAHkAZwBpAHoAbQBvAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAuAGMAbgAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA7ACAAcwB0AHkAbABlAC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnADsAIABjAG8AbgBuAGUAYwB0AC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALwAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALgBjAG4ALwA7ACAAYwBoAGkAbABkAC0AcwByAGMAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC0AbgBvAGMAbwBvAGsAaQBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdAByAGEAYwBrAGUAcgB0AGUAcwB0AC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBzAHUAcgB2AGUAeQBnAGkAegBtAG8ALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtAAA=",
+ output: {
+ // Within Bug 965637 we removed CSP from Principals. Already serialized Principals however should still deserialize correctly (just without the CSP).
+ // "cspJSON": "{\"csp-policies\":[{\"child-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"connect-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://accounts.firefox.com/\",\"https://accounts.firefox.com.cn/\"],\"default-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\"],\"frame-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"img-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"data:\",\"https://mozilla.org\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://adservice.google.com\",\"https://adservice.google.de\",\"https://adservice.google.dk\",\"https://creativecommons.org\",\"https://ad.doubleclick.net\"],\"report-only\":false,\"script-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\",\"'unsafe-eval'\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://tagmanager.google.com\",\"https://www.youtube.com\",\"https://s.ytimg.com\"],\"style-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\"]}]}",
+ URISpec: "https://www.mozilla.org/en-US/",
+ originAttributes: {
+ firstPartyDomain: "",
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+ userContextId: 0,
+ geckoViewSessionContextId: "",
+ partitionKey: "",
+ },
+ },
+ },
+ {
+ input:
+ "ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAL2h0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTL2ZpcmVmb3gvYWNjb3VudHMvAAAAAAAAAAUAAAAIAAAADwAAAAj/////AAAACP////8AAAAIAAAADwAAABcAAAAYAAAAFwAAABgAAAAXAAAAGAAAAC8AAAAAAAAAL/////8AAAAA/////wAAABf/////AAAAF/////8BAAAAAAAAAAAAAAABCdntGuXUQAS/4CfOuSPZrLPEwK69Xkyth+CNIQ27P58B3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAL2h0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTL2ZpcmVmb3gvYWNjb3VudHMvAAAAAAAAAAUAAAAIAAAADwAAAAj/////AAAACP////8AAAAIAAAADwAAABcAAAAYAAAAFwAAABgAAAAXAAAAGAAAAC8AAAAAAAAAL/////8AAAAA/////wAAABf/////AAAAF/////8BAAAAAAAAAAAAAQAABbYAcwBjAHIAaQBwAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAJwB1AG4AcwBhAGYAZQAtAGkAbgBsAGkAbgBlACcAIAAnAHUAbgBzAGEAZgBlAC0AZQB2AGEAbAAnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBnAG8AbwBnAGwAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHMALgB5AHQAaQBtAGcALgBjAG8AbQA7ACAAaQBtAGcALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAZABhAHQAYQA6ACAAaAB0AHQAcABzADoALwAvAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBkAHMAZQByAHYAaQBjAGUALgBnAG8AbwBnAGwAZQAuAGQAZQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AZABrACAAaAB0AHQAcABzADoALwAvAGMAcgBlAGEAdABpAHYAZQBjAG8AbQBtAG8AbgBzAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AYQBkAC4AZABvAHUAYgBsAGUAYwBsAGkAYwBrAC4AbgBlAHQAOwAgAGQAZQBmAGEAdQBsAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtADsAIABmAHIAYQBtAGUALQBzAHIAYwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALQBuAG8AYwBvAG8AawBpAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB0AHIAYQBjAGsAZQByAHQAZQBzAHQALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHMAdQByAHYAZQB5AGcAaQB6AG0AbwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALgBjAG4AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AOwAgAHMAdAB5AGwAZQAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwA7ACAAYwBvAG4AbgBlAGMAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC8AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuAC8AOwAgAGMAaABpAGwAZAAtAHMAcgBjACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAtAG4AbwBjAG8AbwBrAGkAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAcgBhAGMAawBlAHIAdABlAHMAdAAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AcwB1AHIAdgBlAHkAZwBpAHoAbQBvAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAuAGMAbgAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQAA",
+ output: {
+ URISpec: "https://www.mozilla.org/en-US/firefox/accounts/",
+ originAttributes: {
+ firstPartyDomain: "",
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+ userContextId: 0,
+ geckoViewSessionContextId: "",
+ partitionKey: "",
+ },
+ // Within Bug 965637 we removed CSP from Principals. Already serialized Principals however should still deserialize correctly (just without the CSP).
+ // "cspJSON": "{\"csp-policies\":[{\"child-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"connect-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://accounts.firefox.com/\",\"https://accounts.firefox.com.cn/\"],\"default-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\"],\"frame-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"img-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"data:\",\"https://mozilla.org\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://adservice.google.com\",\"https://adservice.google.de\",\"https://adservice.google.dk\",\"https://creativecommons.org\",\"https://ad.doubleclick.net\"],\"report-only\":false,\"script-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\",\"'unsafe-eval'\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://tagmanager.google.com\",\"https://www.youtube.com\",\"https://s.ytimg.com\"],\"style-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\"]}]}",
+ },
+ },
+ ];
+
+ for (let test of serializedPrincipalsFromFirefox) {
+ let principal = E10SUtils.deserializePrincipal(test.input);
+
+ for (let key in principal.originAttributes) {
+ is(
+ principal.originAttributes[key],
+ test.output.originAttributes[key],
+ `Ensure value of ${key} is ${test.output.originAttributes[key]}`
+ );
+ }
+
+ if ("URI" in test.output && test.output.URI === false) {
+ is(
+ principal.isContentPrincipal,
+ false,
+ "Should have not have a URI for system"
+ );
+ } else {
+ is(
+ principal.spec,
+ test.output.URISpec,
+ `Should have spec ${test.output.URISpec}`
+ );
+ }
+ }
+});
diff --git a/browser/base/content/test/caps/browser_principalSerialization_json.js b/browser/base/content/test/caps/browser_principalSerialization_json.js
new file mode 100644
index 0000000000..f79f269fd7
--- /dev/null
+++ b/browser/base/content/test/caps/browser_principalSerialization_json.js
@@ -0,0 +1,161 @@
+"use strict";
+
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ This test file exists to ensure whenever changes to principal serialization happens,
+ we guarantee that the data can be restored and generated into a new principal.
+
+ The tests are written to be brittle so we encode all versions of the changes into the tests.
+*/
+
+add_task(async function test_nullPrincipal() {
+ const nullId = "0";
+ // fields
+ const uri = 0;
+ const suffix = 1;
+
+ const nullReplaceRegex =
+ /moz-nullprincipal:{[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}}/;
+ const NULL_REPLACE = "NULL_PRINCIPAL_URL";
+
+ /*
+ This test should NOT be resilient to changes in versioning,
+ however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code.
+ */
+ let tests = [
+ {
+ input: { OA: {} },
+ expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
+ },
+ {
+ input: { OA: {} },
+ expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
+ },
+ {
+ input: { OA: { userContextId: 0 } },
+ expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
+ },
+ {
+ input: { OA: { userContextId: 2 } },
+ expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}","${suffix}":"^userContextId=2"}}`,
+ },
+ {
+ input: { OA: { privateBrowsingId: 1 } },
+ expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}","${suffix}":"^privateBrowsingId=1"}}`,
+ },
+ {
+ input: { OA: { privateBrowsingId: 0 } },
+ expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
+ },
+ ];
+
+ for (let test of tests) {
+ let p = Services.scriptSecurityManager.createNullPrincipal(test.input.OA);
+ let sp = E10SUtils.serializePrincipal(p);
+ // Not sure why cppjson is adding a \n here
+ let spr = sp.replace(nullReplaceRegex, NULL_REPLACE);
+ is(
+ test.expected,
+ spr,
+ "Expected serialized object for " + JSON.stringify(test.input)
+ );
+ let dp = E10SUtils.deserializePrincipal(sp);
+
+ // Check all the origin attributes
+ for (let key in test.input.OA) {
+ is(
+ dp.originAttributes[key],
+ test.input.OA[key],
+ "Ensure value of " + key + " is " + test.input.OA[key]
+ );
+ }
+ }
+});
+
+add_task(async function test_contentPrincipal() {
+ const contentId = "1";
+ // fields
+ const content = 0;
+ // const domain = 1;
+ const suffix = 2;
+ // const csp = 3;
+
+ /*
+ This test should NOT be resilient to changes in versioning,
+ however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code.
+ */
+ let tests = [
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ input: { uri: "http://example.com/", OA: {} },
+ expected: `{"${contentId}":{"${content}":"http://example.com/"}}`,
+ },
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ input: { uri: "http://mozilla1.com/", OA: {} },
+ expected: `{"${contentId}":{"${content}":"http://mozilla1.com/"}}`,
+ },
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ input: { uri: "http://mozilla2.com/", OA: { userContextId: 0 } },
+ expected: `{"${contentId}":{"${content}":"http://mozilla2.com/"}}`,
+ },
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ input: { uri: "http://mozilla3.com/", OA: { userContextId: 2 } },
+ expected: `{"${contentId}":{"${content}":"http://mozilla3.com/","${suffix}":"^userContextId=2"}}`,
+ },
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ input: { uri: "http://mozilla4.com/", OA: { privateBrowsingId: 1 } },
+ expected: `{"${contentId}":{"${content}":"http://mozilla4.com/","${suffix}":"^privateBrowsingId=1"}}`,
+ },
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ input: { uri: "http://mozilla5.com/", OA: { privateBrowsingId: 0 } },
+ expected: `{"${contentId}":{"${content}":"http://mozilla5.com/"}}`,
+ },
+ ];
+
+ for (let test of tests) {
+ let uri = Services.io.newURI(test.input.uri);
+ let p = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ test.input.OA
+ );
+ let sp = E10SUtils.serializePrincipal(p);
+ is(test.expected, sp, "Expected serialized object for " + test.input.uri);
+ let dp = E10SUtils.deserializePrincipal(sp);
+ is(dp.URI.spec, test.input.uri, "Ensure spec is the same");
+
+ // Check all the origin attributes
+ for (let key in test.input.OA) {
+ is(
+ dp.originAttributes[key],
+ test.input.OA[key],
+ "Ensure value of " + key + " is " + test.input.OA[key]
+ );
+ }
+ }
+});
+
+add_task(async function test_systemPrincipal() {
+ const systemId = "3";
+ /*
+ This test should NOT be resilient to changes in versioning,
+ however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code.
+ */
+ const expected = `{"${systemId}":{}}`;
+
+ let p = Services.scriptSecurityManager.getSystemPrincipal();
+ let sp = E10SUtils.serializePrincipal(p);
+ is(expected, sp, "Expected serialized object for system principal");
+ let dp = E10SUtils.deserializePrincipal(sp);
+ is(
+ dp,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ "Deserialized the system principal"
+ );
+});
diff --git a/browser/base/content/test/caps/browser_principalSerialization_version1.js b/browser/base/content/test/caps/browser_principalSerialization_version1.js
new file mode 100644
index 0000000000..6c4a41e911
--- /dev/null
+++ b/browser/base/content/test/caps/browser_principalSerialization_version1.js
@@ -0,0 +1,159 @@
+"use strict";
+
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ This test file exists to ensure whenever changes to principal serialization happens,
+ we guarantee that the data can be restored and generated into a new principal.
+
+ The tests are written to be brittle so we encode all versions of the changes into the tests.
+*/
+
+add_task(function test_nullPrincipal() {
+ /*
+ As Null principals are designed to be non deterministic we just need to ensure that
+ a previous serialized version matches what it was generated as.
+
+ This test should be resilient to changes in versioning, however it should also be duplicated for a new serialization change.
+ */
+ // Principal created with: E10SUtils.serializePrincipal(Services.scriptSecurityManager.createNullPrincipal({ }));
+ let p = E10SUtils.deserializePrincipal(
+ "vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezU2Y2FjNTQwLTg2NGQtNDdlNy04ZTI1LTE2MTRlYWI1MTU1ZX0AAAAA"
+ );
+ is(
+ "moz-nullprincipal:{56cac540-864d-47e7-8e25-1614eab5155e}",
+ p.URI.spec,
+ "Deserialized principal doesn't have the correct URI"
+ );
+
+ // Principal created with: E10SUtils.serializePrincipal(Services.scriptSecurityManager.createNullPrincipal({ userContextId: 2 }));
+ let p2 = E10SUtils.deserializePrincipal(
+ "vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezA1ZjllN2JhLWIwODMtNDJhMi1iNDdkLTZiODRmNmYwYTM3OX0AAAAQXnVzZXJDb250ZXh0SWQ9Mg=="
+ );
+ is(
+ "moz-nullprincipal:{05f9e7ba-b083-42a2-b47d-6b84f6f0a379}",
+ p2.URI.spec,
+ "Deserialized principal doesn't have the correct URI"
+ );
+ is(p2.originAttributes.userContextId, 2, "Expected a userContextId of 2");
+});
+
+add_task(async function test_realHistoryCheck() {
+ /*
+ This test should be resilient to changes in principal serialization, if these are failing then it's likely the code will break session storage.
+ To recreate this for another version, copy the function into the browser console, browse some pages and printHistory.
+
+ Generated with:
+ function printHistory() {
+ let tests = [];
+ let entries = SessionStore.getSessionHistory(gBrowser.selectedTab).entries.map((entry) => { return entry.triggeringPrincipal_base64 });
+ entries.push(E10SUtils.serializePrincipal(gBrowser.selectedTab.linkedBrowser._contentPrincipal));
+ for (let entry of entries) {
+ console.log(entry);
+ let testData = {};
+ testData.input = entry;
+ let principal = E10SUtils.deserializePrincipal(testData.input);
+ testData.output = {};
+ if (principal.URI === null) {
+ testData.output.URI = false;
+ } else {
+ testData.output.URISpec = principal.URI.spec;
+ }
+ testData.output.originAttributes = principal.originAttributes;
+
+ tests.push(testData);
+ }
+ return tests;
+ }
+ printHistory(); // Copy this into: serializedPrincipalsFromFirefox
+ */
+
+ let serializedPrincipalsFromFirefox = [
+ {
+ input: "SmIS26zLEdO3ZQBgsLbOywAAAAAAAAAAwAAAAAAAAEY=",
+ output: {
+ URI: false,
+ originAttributes: {
+ firstPartyDomain: "",
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+ userContextId: 0,
+ geckoViewSessionContextId: "",
+ partitionKey: "",
+ },
+ },
+ },
+ {
+ input:
+ "ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAe2h0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTLz91dG1fc291cmNlPXd3dy5tb3ppbGxhLm9yZyZ1dG1fbWVkaXVtPXJlZmVycmFsJnV0bV9jYW1wYWlnbj1uYXYmdXRtX2NvbnRlbnQ9ZGV2ZWxvcGVycwAAAAAAAAAFAAAACAAAABUAAAAA/////wAAAAD/////AAAACAAAABUAAAAdAAAAXgAAAB0AAAAHAAAAHQAAAAcAAAAkAAAAAAAAAAD/////AAAAAP////8AAAAlAAAAVgAAAAD/////AQAAAAAAAAAAAAAAAA==",
+ output: {
+ URISpec:
+ "https://developer.mozilla.org/en-US/?utm_source=www.mozilla.org&utm_medium=referral&utm_campaign=nav&utm_content=developers",
+ originAttributes: {
+ firstPartyDomain: "",
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+ userContextId: 0,
+ geckoViewSessionContextId: "",
+ partitionKey: "",
+ },
+ },
+ },
+ {
+ input: "SmIS26zLEdO3ZQBgsLbOywAAAAAAAAAAwAAAAAAAAEY=",
+ output: {
+ URI: false,
+ originAttributes: {
+ firstPartyDomain: "",
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+ userContextId: 0,
+ geckoViewSessionContextId: "",
+ partitionKey: "",
+ },
+ },
+ },
+ {
+ input:
+ "vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezA0NWNhMThkLTQzNmMtNDc0NC1iYmI2LWIxYTE1MzY2ZGY3OX0AAAAA",
+ output: {
+ URISpec: "moz-nullprincipal:{045ca18d-436c-4744-bbb6-b1a15366df79}",
+ originAttributes: {
+ firstPartyDomain: "",
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+ userContextId: 0,
+ geckoViewSessionContextId: "",
+ partitionKey: "",
+ },
+ },
+ },
+ ];
+
+ for (let test of serializedPrincipalsFromFirefox) {
+ let principal = E10SUtils.deserializePrincipal(test.input);
+
+ for (let key in principal.originAttributes) {
+ is(
+ principal.originAttributes[key],
+ test.output.originAttributes[key],
+ `Ensure value of ${key} is ${test.output.originAttributes[key]}`
+ );
+ }
+
+ if ("URI" in test.output && test.output.URI === false) {
+ is(
+ principal.isContentPrincipal,
+ false,
+ "Should have not have a URI for system"
+ );
+ } else {
+ is(
+ principal.spec,
+ test.output.URISpec,
+ `Should have spec ${test.output.URISpec}`
+ );
+ }
+ }
+});
diff --git a/browser/base/content/test/captivePortal/browser.ini b/browser/base/content/test/captivePortal/browser.ini
new file mode 100644
index 0000000000..37b72ab758
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_CaptivePortalWatcher.js]
+skip-if = os == "win" # Bug 1313894
+[browser_CaptivePortalWatcher_1.js]
+skip-if = os == "win" # Bug 1313894
+[browser_captivePortalTabReference.js]
+[browser_captivePortal_certErrorUI.js]
+[browser_captivePortal_https_only.js]
+[browser_closeCapPortalTabCanonicalURL.js]
diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
new file mode 100644
index 0000000000..aeafae21d8
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
@@ -0,0 +1,125 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+// Bug 1318389 - This test does a lot of window and tab manipulation,
+// causing it to take a long time on debug.
+requestLongerTimeout(2);
+
+add_task(setupPrefsAndRecentWindowBehavior);
+
+// Each of the test cases below is run twice: once for login-success and once
+// for login-abort (aSuccess set to true and false respectively).
+let testCasesForBothSuccessAndAbort = [
+ /**
+ * A portal is detected when there's no browser window, then a browser
+ * window is opened, then the portal is freed.
+ * The portal tab should be added and focused when the window is
+ * opened, and closed automatically when the success event is fired.
+ * The captive portal notification should be shown when the window is
+ * opened, and closed automatically when the success event is fired.
+ */
+ async function test_detectedWithNoBrowserWindow_Open(aSuccess) {
+ await portalDetected();
+ let win = await focusWindowAndWaitForPortalUI();
+ await freePortal(aSuccess);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * A portal is detected when multiple browser windows are open but none
+ * have focus. A browser window is focused, then the portal is freed.
+ * The portal tab should be added and focused when the window is
+ * focused, and closed automatically when the success event is fired.
+ * The captive portal notification should be shown in all windows upon
+ * detection, and closed automatically when the success event is fired.
+ */
+ async function test_detectedWithNoBrowserWindow_Focused(aSuccess) {
+ let win1 = await openWindowAndWaitForFocus();
+ let win2 = await openWindowAndWaitForFocus();
+ // Defocus both windows.
+ await SimpleTest.promiseFocus(window);
+
+ await portalDetected();
+
+ // Notification should be shown in both windows.
+ ensurePortalNotification(win1);
+ ensureNoPortalTab(win1);
+ ensurePortalNotification(win2);
+ ensureNoPortalTab(win2);
+
+ await focusWindowAndWaitForPortalUI(false, win2);
+
+ await freePortal(aSuccess);
+
+ ensureNoPortalNotification(win1);
+ ensureNoPortalTab(win2);
+ ensureNoPortalNotification(win2);
+
+ await closeWindowAndWaitForWindowActivate(win2);
+ // No need to wait for xul-window-visible: after win2 is closed, focus
+ // is restored to the default window and win1 remains in the background.
+ await BrowserTestUtils.closeWindow(win1);
+ },
+
+ /**
+ * A portal is detected when there's no browser window, then a browser
+ * window is opened, then the portal is freed.
+ * The recheck triggered when the browser window is opened takes a
+ * long time. No portal tab should be added.
+ * The captive portal notification should be shown when the window is
+ * opened, and closed automatically when the success event is fired.
+ */
+ async function test_detectedWithNoBrowserWindow_LongRecheck(aSuccess) {
+ await portalDetected();
+ let win = await focusWindowAndWaitForPortalUI(true);
+ await freePortal(aSuccess);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * A portal is detected when there's no browser window, and the
+ * portal is freed before a browser window is opened. No portal
+ * UI should be shown when a browser window is opened.
+ */
+ async function test_detectedWithNoBrowserWindow_GoneBeforeOpen(aSuccess) {
+ await portalDetected();
+ await freePortal(aSuccess);
+ let win = await openWindowAndWaitForFocus();
+ // Wait for a while to make sure no UI is shown.
+ await new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ });
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * A portal is detected when a browser window has focus. No portal tab should
+ * be opened. A notification bar should be displayed in all browser windows.
+ */
+ async function test_detectedWithFocus(aSuccess) {
+ let win1 = await openWindowAndWaitForFocus();
+ let win2 = await openWindowAndWaitForFocus();
+ await portalDetected();
+ ensureNoPortalTab(win1);
+ ensureNoPortalTab(win2);
+ ensurePortalNotification(win1);
+ ensurePortalNotification(win2);
+ await freePortal(aSuccess);
+ ensureNoPortalNotification(win1);
+ ensureNoPortalNotification(win2);
+ await BrowserTestUtils.closeWindow(win2);
+ await BrowserTestUtils.closeWindow(win1);
+ await waitForBrowserWindowActive(window);
+ },
+];
+
+for (let testcase of testCasesForBothSuccessAndAbort) {
+ add_task(testcase.bind(null, true));
+ add_task(testcase.bind(null, false));
+}
diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js
new file mode 100644
index 0000000000..6c6cc5f438
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js
@@ -0,0 +1,108 @@
+"use strict";
+
+add_task(setupPrefsAndRecentWindowBehavior);
+
+let testcases = [
+ /**
+ * A portal is detected when there's no browser window,
+ * then a browser window is opened, and the portal is logged into
+ * and redirects to a different page. The portal tab should be added
+ * and focused when the window is opened, and left open after login
+ * since it redirected.
+ */
+ async function test_detectedWithNoBrowserWindow_Redirect() {
+ await portalDetected();
+ let win = await focusWindowAndWaitForPortalUI();
+ let browser = win.gBrowser.selectedTab.linkedBrowser;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ CANONICAL_URL_REDIRECTED
+ );
+ BrowserTestUtils.loadURIString(browser, CANONICAL_URL_REDIRECTED);
+ await loadPromise;
+ await freePortal(true);
+ ensurePortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * Test the various expected behaviors of the "Show Login Page" button
+ * in the captive portal notification. The button should be visible for
+ * all tabs except the captive portal tab, and when clicked, should
+ * ensure a captive portal tab is open and select it.
+ */
+ async function test_showLoginPageButton() {
+ let win = await openWindowAndWaitForFocus();
+ await portalDetected();
+ let notification = ensurePortalNotification(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+
+ function testPortalTabSelectedAndButtonNotVisible() {
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be selected."
+ );
+ testShowLoginPageButtonVisibility(notification, "hidden");
+ }
+
+ let button = notification.buttonContainer.querySelector(
+ "button.notification-button"
+ );
+ async function clickButtonAndExpectNewPortalTab() {
+ let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
+ button.click();
+ let tab = await p;
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be selected."
+ );
+ return tab;
+ }
+
+ // Simulate clicking the button. The portal tab should be opened and
+ // selected and the button should hide.
+ let tab = await clickButtonAndExpectNewPortalTab();
+ testPortalTabSelectedAndButtonNotVisible();
+
+ // Close the tab. The button should become visible.
+ BrowserTestUtils.removeTab(tab);
+ ensureNoPortalTab(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+
+ // When the button is clicked, a new portal tab should be opened and
+ // selected.
+ tab = await clickButtonAndExpectNewPortalTab();
+
+ // Open another arbitrary tab. The button should become visible. When it's clicked,
+ // the portal tab should be selected.
+ let anotherTab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ testShowLoginPageButtonVisibility(notification, "visible");
+ button.click();
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be selected."
+ );
+
+ // Close the portal tab and select the arbitrary tab. The button should become
+ // visible and when it's clicked, a new portal tab should be opened.
+ BrowserTestUtils.removeTab(tab);
+ win.gBrowser.selectedTab = anotherTab;
+ testShowLoginPageButtonVisibility(notification, "visible");
+ tab = await clickButtonAndExpectNewPortalTab();
+
+ BrowserTestUtils.removeTab(anotherTab);
+ await freePortal(true);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+];
+
+for (let testcase of testcases) {
+ add_task(testcase);
+}
diff --git a/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js b/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js
new file mode 100644
index 0000000000..b630f35149
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CPS = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
+ Ci.nsICaptivePortalService
+);
+
+async function checkCaptivePortalTabReference(evt, currState) {
+ await portalDetected();
+ let errorTab = await openCaptivePortalErrorTab();
+ let portalTab = await openCaptivePortalLoginTab(errorTab);
+
+ // Release the reference held to the portal tab by sending success/abort events.
+ Services.obs.notifyObservers(null, evt);
+ await TestUtils.waitForCondition(
+ () => CPS.state == currState,
+ "Captive portal has been released"
+ );
+ gBrowser.removeTab(errorTab);
+
+ await portalDetected();
+ ok(CPS.state == CPS.LOCKED_PORTAL, "Captive portal is locked again");
+ errorTab = await openCaptivePortalErrorTab();
+ let portalTab2 = await openCaptivePortalLoginTab(errorTab);
+ ok(
+ portalTab != portalTab2,
+ "waitForNewTab in openCaptivePortalLoginTab should not have completed at this point if references were held to the old captive portal tab after login/abort."
+ );
+ gBrowser.removeTab(portalTab);
+ gBrowser.removeTab(portalTab2);
+
+ let errorTabReloaded = BrowserTestUtils.waitForErrorPage(
+ errorTab.linkedBrowser
+ );
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ await errorTabReloaded;
+
+ gBrowser.removeTab(errorTab);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["captivedetect.canonicalURL", CANONICAL_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT],
+ ],
+ });
+});
+
+let capPortalStates = [
+ {
+ evt: "captive-portal-login-success",
+ state: CPS.UNLOCKED_PORTAL,
+ },
+ {
+ evt: "captive-portal-login-abort",
+ state: CPS.UNKNOWN,
+ },
+];
+
+for (let elem of capPortalStates) {
+ add_task(checkCaptivePortalTabReference.bind(null, elem.evt, elem.state));
+}
diff --git a/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
new file mode 100644
index 0000000000..d23125a627
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["captivedetect.canonicalURL", CANONICAL_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT],
+ ],
+ });
+});
+
+// This tests the alternate cert error UI when we are behind a captive portal.
+add_task(async function checkCaptivePortalCertErrorUI() {
+ info(
+ "Checking that the alternate cert error UI is shown when we are behind a captive portal"
+ );
+
+ // Open a second window in the background. Later, we'll check that
+ // when we click the button to open the captive portal tab, the tab
+ // only opens in the active window and not in the background one.
+ let secondWindow = await openWindowAndWaitForFocus();
+ await SimpleTest.promiseFocus(window);
+
+ await portalDetected();
+
+ // Check that we didn't open anything in the background window.
+ ensureNoPortalTab(secondWindow);
+
+ let tab = await openCaptivePortalErrorTab();
+ let browser = tab.linkedBrowser;
+ let portalTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ CANONICAL_URL
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.getElementById("openPortalLoginPageButton");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.is_visible(loginButton),
+ "Captive portal error page UI is visible"
+ );
+
+ if (!Services.focus.focusedElement == loginButton) {
+ await ContentTaskUtils.waitForEvent(loginButton, "focus");
+ }
+
+ Assert.ok(true, "openPortalLoginPageButton has focus");
+ info("Clicking the Open Login Page button");
+ await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content);
+ });
+
+ let portalTab = await portalTabPromise;
+ is(
+ gBrowser.selectedTab,
+ portalTab,
+ "Login page should be open in a new foreground tab."
+ );
+
+ // Check that we didn't open anything in the background window.
+ ensureNoPortalTab(secondWindow);
+
+ // Make sure clicking the "Open Login Page" button again focuses the existing portal tab.
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ // Passing an empty function to BrowserTestUtils.switchTab lets us wait for an arbitrary
+ // tab switch.
+ portalTabPromise = BrowserTestUtils.switchTab(gBrowser, () => {});
+ await SpecialPowers.spawn(browser, [], async () => {
+ info("Clicking the Open Login Page button.");
+ let loginButton = content.document.getElementById(
+ "openPortalLoginPageButton"
+ );
+ await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content);
+ });
+
+ info("Opening captive portal login page");
+ let portalTab2 = await portalTabPromise;
+ is(portalTab2, portalTab, "The existing portal tab should be focused.");
+
+ // Check that we didn't open anything in the background window.
+ ensureNoPortalTab(secondWindow);
+
+ let portalTabClosing = BrowserTestUtils.waitForTabClosing(portalTab);
+ let errorTabReloaded = BrowserTestUtils.waitForErrorPage(browser);
+
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ await portalTabClosing;
+
+ info(
+ "Waiting for error tab to be reloaded after the captive portal was freed."
+ );
+ await errorTabReloaded;
+ await SpecialPowers.spawn(browser, [], () => {
+ let doc = content.document;
+ ok(
+ !doc.body.classList.contains("captiveportal"),
+ "Captive portal error page UI is not visible."
+ );
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(secondWindow);
+});
+
+add_task(async function testCaptivePortalAdvancedPanel() {
+ info(
+ "Checking that the advanced section of the about:certerror UI is shown when we are behind a captive portal."
+ );
+ await portalDetected();
+ let tab = await openCaptivePortalErrorTab();
+ let browser = tab.linkedBrowser;
+
+ const waitForLocationChange = (async () => {
+ await BrowserTestUtils.waitForLocationChange(gBrowser, BAD_CERT_PAGE);
+ info("(waitForLocationChange resolved)");
+ })();
+ await SpecialPowers.spawn(browser, [BAD_CERT_PAGE], async expectedURL => {
+ const doc = content.document;
+ let advancedButton = doc.getElementById("advancedButton");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.is_visible(advancedButton),
+ "Captive portal UI is visible"
+ );
+
+ info("Clicking on the advanced button");
+ const advPanel = doc.getElementById("badCertAdvancedPanel");
+ ok(
+ !ContentTaskUtils.is_visible(advPanel),
+ "Advanced panel is not yet visible"
+ );
+ await EventUtils.synthesizeMouseAtCenter(advancedButton, {}, content);
+ ok(ContentTaskUtils.is_visible(advPanel), "Advanced panel is now visible");
+
+ let advPanelContent = doc.getElementById("badCertTechnicalInfo");
+ ok(
+ ContentTaskUtils.is_visible(advPanelContent) &&
+ advPanelContent.textContent.includes("expired.example.com"),
+ "Advanced panel text content is visible"
+ );
+
+ let advPanelErrorCode = doc.getElementById("errorCode");
+ ok(
+ advPanelErrorCode.textContent,
+ "Cert error code is visible in the advanced panel"
+ );
+
+ // -
+
+ const advPanelExceptionButton = doc.getElementById("exceptionDialogButton");
+
+ function isOnCertErrorPage() {
+ return ContentTaskUtils.is_visible(advPanel);
+ }
+
+ ok(isOnCertErrorPage(), "On cert error page before adding exception");
+ ok(
+ advPanelExceptionButton.disabled,
+ "Exception button should start disabled"
+ );
+ await EventUtils.synthesizeMouseAtCenter(
+ advPanelExceptionButton,
+ {},
+ content
+ ); // Click
+ const clickTime = content.performance.now();
+ ok(
+ isOnCertErrorPage(),
+ "Still on cert error page because clicked too early"
+ );
+
+ // Now waitForCondition now that it's possible.
+ try {
+ await ContentTaskUtils.waitForCondition(
+ () => !advPanelExceptionButton.disabled,
+ "Wait for exception button enabled"
+ );
+ } catch (rejected) {
+ ok(false, rejected);
+ return;
+ }
+ ok(
+ !advPanelExceptionButton.disabled,
+ "Exception button should be enabled after waiting"
+ );
+ const msSinceClick = content.performance.now() - clickTime;
+ const expr = `${msSinceClick} > 1000`;
+ /* eslint-disable no-eval */
+ ok(eval(expr), `Exception button should stay disabled for ${expr} ms`);
+
+ await EventUtils.synthesizeMouseAtCenter(
+ advPanelExceptionButton,
+ {},
+ content
+ ); // Click
+ info("Clicked");
+ });
+ await waitForLocationChange;
+ info("Page reloaded after adding cert exception");
+
+ // Clear the certificate exception.
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("expired.example.com", -1, {});
+
+ info("After clearing cert override, asking for reload...");
+ const waitForErrorPage = BrowserTestUtils.waitForErrorPage(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ info("reload...");
+ content.location.reload();
+ });
+ info("waitForErrorPage...");
+ await waitForErrorPage;
+
+ info("removeTab...");
+ await BrowserTestUtils.removeTab(tab);
+ info("Done!");
+});
diff --git a/browser/base/content/test/captivePortal/browser_captivePortal_https_only.js b/browser/base/content/test/captivePortal/browser_captivePortal_https_only.js
new file mode 100644
index 0000000000..789d392107
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_captivePortal_https_only.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+const testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const CANONICAL_URI = Services.io.newURI(testPath);
+const PERMISSION_NAME = "https-only-load-insecure";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // That changes the canoncicalURL from "http://{server}/captive-detect/success.txt"
+ // to http://example.com
+ set: [
+ ["captivedetect.canonicalURL", testPath],
+ ["dom.security.https_only_mode", true],
+ ],
+ });
+});
+
+// This test checks if https-only exempts the canoncial uri.
+add_task(async function checkCaptivePortalExempt() {
+ await portalDetected();
+ info("Checking that the canonical uri is exempt by https-only mode");
+ let tab = await openCaptivePortalErrorTab();
+ let browser = tab.linkedBrowser;
+ let portalTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, testPath);
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.getElementById("openPortalLoginPageButton");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.is_visible(loginButton),
+ "Captive portal error page UI is visible"
+ );
+
+ if (!Services.focus.focusedElement == loginButton) {
+ await ContentTaskUtils.waitForEvent(loginButton, "focus");
+ }
+
+ Assert.ok(true, "openPortalLoginPageButton has focus");
+ info("Clicking the Open Login Page button");
+ await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content);
+ });
+ is(
+ PermissionTestUtils.testPermission(CANONICAL_URI, PERMISSION_NAME),
+ Services.perms.ALLOW_ACTION,
+ "Check permission in perm. manager if canoncial uri is set as exempt."
+ );
+ let portalTab = await portalTabPromise;
+ is(
+ gBrowser.selectedTab,
+ portalTab,
+ "Login page should be open in a new foreground tab."
+ );
+ is(
+ gBrowser.currentURI.spec,
+ testPath,
+ "Opened the right URL without upgrading it."
+ );
+ // Close all tabs
+ await BrowserTestUtils.removeTab(portalTab);
+ let tabReloaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ await tabReloaded;
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js b/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js
new file mode 100644
index 0000000000..0457dab1c0
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const LOGIN_LINK = `<html><body><a href="/unlock">login</a></body></html>`;
+const LOGIN_URL = "http://localhost:8080/login";
+const CANONICAL_SUCCESS_URL = "http://localhost:8080/success";
+const CPS = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
+ Ci.nsICaptivePortalService
+);
+
+let server;
+let loginPageShown = false;
+
+function redirectHandler(request, response) {
+ if (loginPageShown) {
+ return;
+ }
+ response.setStatusLine(request.httpVersion, 302, "captive");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Location", LOGIN_URL);
+}
+
+function loginHandler(request, response) {
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(LOGIN_LINK, LOGIN_LINK.length);
+ loginPageShown = true;
+}
+
+function unlockHandler(request, response) {
+ response.setStatusLine(request.httpVersion, 302, "login complete");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Location", CANONICAL_SUCCESS_URL);
+}
+
+add_setup(async function () {
+ // Set up a mock server for handling captive portal redirect.
+ server = new HttpServer();
+ server.registerPathHandler("/success", redirectHandler);
+ server.registerPathHandler("/login", loginHandler);
+ server.registerPathHandler("/unlock", unlockHandler);
+ server.start(8080);
+ info("Mock server is now set up for captive portal redirect");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["captivedetect.canonicalURL", CANONICAL_SUCCESS_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT],
+ ],
+ });
+});
+
+// This test checks if the captive portal tab is removed after the
+// sucess/abort events are fired, assuming the tab has already redirected
+// to the canonical URL before they are fired.
+add_task(async function checkCaptivePortalTabCloseOnCanonicalURL_one() {
+ await portalDetected();
+ let errorTab = await openCaptivePortalErrorTab();
+ let tab = await openCaptivePortalLoginTab(errorTab, LOGIN_URL);
+ let browser = tab.linkedBrowser;
+
+ let redirectedToCanonicalURL = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ CANONICAL_SUCCESS_URL
+ );
+ let errorPageReloaded = BrowserTestUtils.waitForErrorPage(
+ errorTab.linkedBrowser
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.querySelector("a");
+ await ContentTaskUtils.waitForCondition(
+ () => loginButton,
+ "Login button on the captive portal tab is visible"
+ );
+ info("Clicking the login button on the captive portal tab page");
+ await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content);
+ });
+
+ await redirectedToCanonicalURL;
+ info(
+ "Re-direct to canonical URL in the captive portal tab was succcessful after login"
+ );
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(tab);
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ await tabClosed;
+ info(
+ "Captive portal tab was closed on re-direct to canonical URL after login as expected"
+ );
+
+ await errorPageReloaded;
+ info("Captive portal error page was reloaded");
+ gBrowser.removeTab(errorTab);
+});
+
+// This test checks if the captive portal tab is removed on location change
+// i.e. when it is re-directed to the canonical URL long after success/abort
+// event handlers are executed.
+add_task(async function checkCaptivePortalTabCloseOnCanonicalURL_two() {
+ loginPageShown = false;
+ await portalDetected();
+ let errorTab = await openCaptivePortalErrorTab();
+ let tab = await openCaptivePortalLoginTab(errorTab, LOGIN_URL);
+ let browser = tab.linkedBrowser;
+
+ let redirectedToCanonicalURL = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ CANONICAL_SUCCESS_URL
+ );
+ let errorPageReloaded = BrowserTestUtils.waitForErrorPage(
+ errorTab.linkedBrowser
+ );
+
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ await TestUtils.waitForCondition(
+ () => CPS.state == CPS.UNLOCKED_PORTAL,
+ "Captive portal is released"
+ );
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(tab);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.querySelector("a");
+ await ContentTaskUtils.waitForCondition(
+ () => loginButton,
+ "Login button on the captive portal tab is visible"
+ );
+ info("Clicking the login button on the captive portal tab page");
+ await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content);
+ });
+
+ await redirectedToCanonicalURL;
+ info(
+ "Re-direct to canonical URL in the captive portal tab was succcessful after login"
+ );
+ await tabClosed;
+ info(
+ "Captive portal tab was closed on re-direct to canonical URL after login as expected"
+ );
+
+ await errorPageReloaded;
+ info("Captive portal error page was reloaded");
+ gBrowser.removeTab(errorTab);
+
+ // Stop the server.
+ await new Promise(r => server.stop(r));
+});
diff --git a/browser/base/content/test/captivePortal/head.js b/browser/base/content/test/captivePortal/head.js
new file mode 100644
index 0000000000..4e47c3012a
--- /dev/null
+++ b/browser/base/content/test/captivePortal/head.js
@@ -0,0 +1,260 @@
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "cps",
+ "@mozilla.org/network/captive-portal-service;1",
+ "nsICaptivePortalService"
+);
+
+const CANONICAL_CONTENT = "success";
+const CANONICAL_URL = "data:text/plain;charset=utf-8," + CANONICAL_CONTENT;
+const CANONICAL_URL_REDIRECTED = "data:text/plain;charset=utf-8,redirected";
+const PORTAL_NOTIFICATION_VALUE = "captive-portal-detected";
+const BAD_CERT_PAGE = "https://expired.example.com/";
+
+async function setupPrefsAndRecentWindowBehavior() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["captivedetect.canonicalURL", CANONICAL_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT],
+ ],
+ });
+ // We need to test behavior when a portal is detected when there is no browser
+ // window, but we can't close the default window opened by the test harness.
+ // Instead, we deactivate CaptivePortalWatcher in the default window and
+ // exclude it using an attribute to mask its presence.
+ window.CaptivePortalWatcher.uninit();
+ window.document.documentElement.setAttribute("ignorecaptiveportal", "true");
+
+ registerCleanupFunction(function cleanUp() {
+ window.CaptivePortalWatcher.init();
+ window.document.documentElement.removeAttribute("ignorecaptiveportal");
+ });
+}
+
+async function portalDetected() {
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ await TestUtils.waitForCondition(() => {
+ return cps.state == cps.LOCKED_PORTAL;
+ }, "Waiting for Captive Portal Service to update state after portal detected.");
+}
+
+async function freePortal(aSuccess) {
+ Services.obs.notifyObservers(
+ null,
+ "captive-portal-login-" + (aSuccess ? "success" : "abort")
+ );
+ await TestUtils.waitForCondition(() => {
+ return cps.state != cps.LOCKED_PORTAL;
+ }, "Waiting for Captive Portal Service to update state after portal freed.");
+}
+
+// If a window is provided, it will be focused. Otherwise, a new window
+// will be opened and focused.
+async function focusWindowAndWaitForPortalUI(aLongRecheck, win) {
+ // CaptivePortalWatcher triggers a recheck when a window gains focus. If
+ // the time taken for the check to complete is under PORTAL_RECHECK_DELAY_MS,
+ // a tab with the login page is opened and selected. If it took longer,
+ // no tab is opened. It's not reliable to time things in an async test,
+ // so use a delay threshold of -1 to simulate a long recheck (so that any
+ // amount of time is considered excessive), and a very large threshold to
+ // simulate a short recheck.
+ Services.prefs.setIntPref(
+ "captivedetect.portalRecheckDelayMS",
+ aLongRecheck ? -1 : 1000000
+ );
+
+ if (!win) {
+ win = await BrowserTestUtils.openNewBrowserWindow();
+ }
+ let windowActivePromise = waitForBrowserWindowActive(win);
+ win.focus();
+ await windowActivePromise;
+
+ // After a new window is opened, CaptivePortalWatcher asks for a recheck, and
+ // waits for it to complete. We need to manually tell it a recheck completed.
+ await TestUtils.waitForCondition(() => {
+ return win.CaptivePortalWatcher._waitingForRecheck;
+ }, "Waiting for CaptivePortalWatcher to trigger a recheck.");
+ Services.obs.notifyObservers(null, "captive-portal-check-complete");
+
+ let notification = ensurePortalNotification(win);
+
+ if (aLongRecheck) {
+ ensureNoPortalTab(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+ return win;
+ }
+
+ let tab = win.gBrowser.tabs[1];
+ if (tab.linkedBrowser.currentURI.spec != CANONICAL_URL) {
+ // The tab should load the canonical URL, wait for it.
+ await BrowserTestUtils.waitForLocationChange(win.gBrowser, CANONICAL_URL);
+ }
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be open and selected in the new window."
+ );
+ testShowLoginPageButtonVisibility(notification, "hidden");
+ return win;
+}
+
+function ensurePortalTab(win) {
+ // For the tests that call this function, it's enough to ensure there
+ // are two tabs in the window - the default tab and the portal tab.
+ is(
+ win.gBrowser.tabs.length,
+ 2,
+ "There should be a captive portal tab in the window."
+ );
+}
+
+function ensurePortalNotification(win) {
+ let notification = win.gNotificationBox.getNotificationWithValue(
+ PORTAL_NOTIFICATION_VALUE
+ );
+ isnot(
+ notification,
+ null,
+ "There should be a captive portal notification in the window."
+ );
+ return notification;
+}
+
+// Helper to test whether the "Show Login Page" is visible in the captive portal
+// notification (it should be hidden when the portal tab is selected).
+function testShowLoginPageButtonVisibility(notification, visibility) {
+ let showLoginPageButton = notification.buttonContainer.querySelector(
+ "button.notification-button"
+ );
+ // If the visibility property was never changed from default, it will be
+ // an empty string, so we pretend it's "visible" (effectively the same).
+ is(
+ showLoginPageButton.style.visibility || "visible",
+ visibility,
+ 'The "Show Login Page" button should be ' + visibility + "."
+ );
+}
+
+function ensureNoPortalTab(win) {
+ is(
+ win.gBrowser.tabs.length,
+ 1,
+ "There should be no captive portal tab in the window."
+ );
+}
+
+function ensureNoPortalNotification(win) {
+ is(
+ win.gNotificationBox.getNotificationWithValue(PORTAL_NOTIFICATION_VALUE),
+ null,
+ "There should be no captive portal notification in the window."
+ );
+}
+
+/**
+ * Some tests open a new window and close it later. When the window is closed,
+ * the original window opened by mochitest gains focus, generating an
+ * activate event. If the next test also opens a new window
+ * before this event has a chance to fire, CaptivePortalWatcher picks
+ * up the first one instead of the one from the new window. To avoid this
+ * unfortunate intermittent timing issue, we wait for the event from
+ * the original window every time we close a window that we opened.
+ */
+function waitForBrowserWindowActive(win) {
+ return new Promise(resolve => {
+ if (Services.focus.activeWindow == win) {
+ resolve();
+ } else {
+ win.addEventListener(
+ "activate",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+}
+
+async function closeWindowAndWaitForWindowActivate(win) {
+ let activationPromises = [];
+ for (let w of BrowserWindowTracker.orderedWindows) {
+ if (
+ w != win &&
+ !win.document.documentElement.getAttribute("ignorecaptiveportal")
+ ) {
+ activationPromises.push(waitForBrowserWindowActive(win));
+ }
+ }
+ await BrowserTestUtils.closeWindow(win);
+ await Promise.race(activationPromises);
+}
+
+/**
+ * BrowserTestUtils.openNewBrowserWindow() does not guarantee the newly
+ * opened window has received focus when the promise resolves, so we
+ * have to manually wait every time.
+ */
+async function openWindowAndWaitForFocus() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await waitForBrowserWindowActive(win);
+ return win;
+}
+
+async function openCaptivePortalErrorTab() {
+ // Open a page with a cert error.
+ let browser;
+ let certErrorLoaded;
+ let errorTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ let tab = BrowserTestUtils.addTab(gBrowser, BAD_CERT_PAGE);
+ gBrowser.selectedTab = tab;
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ return tab;
+ },
+ false
+ );
+ await certErrorLoaded;
+ info("A cert error page was opened");
+ await SpecialPowers.spawn(errorTab.linkedBrowser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.getElementById("openPortalLoginPageButton");
+ await ContentTaskUtils.waitForCondition(
+ () => loginButton && doc.body.className == "captiveportal",
+ "Captive portal error page UI is visible"
+ );
+ });
+ info("Captive portal error page UI is visible");
+
+ return errorTab;
+}
+
+async function openCaptivePortalLoginTab(
+ errorTab,
+ LOGIN_PAGE_URL = CANONICAL_URL
+) {
+ let portalTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ LOGIN_PAGE_URL,
+ true
+ );
+
+ await SpecialPowers.spawn(errorTab.linkedBrowser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.getElementById("openPortalLoginPageButton");
+ info("Click on the login button on the captive portal error page");
+ await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content);
+ });
+
+ let portalTab = await portalTabPromise;
+ is(
+ gBrowser.selectedTab,
+ portalTab,
+ "Captive Portal login page is now open in a new foreground tab."
+ );
+
+ return portalTab;
+}
diff --git a/browser/base/content/test/chrome/chrome.ini b/browser/base/content/test/chrome/chrome.ini
new file mode 100644
index 0000000000..9882f4b647
--- /dev/null
+++ b/browser/base/content/test/chrome/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_aboutCrashed.xhtml]
+[test_aboutRestartRequired.xhtml]
diff --git a/browser/base/content/test/chrome/test_aboutCrashed.xhtml b/browser/base/content/test/chrome/test_aboutCrashed.xhtml
new file mode 100644
index 0000000000..0e2ef64f9b
--- /dev/null
+++ b/browser/base/content/test/chrome/test_aboutCrashed.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <iframe type="content" id="frame1"/>
+ <iframe type="content" id="frame2" onload="doTest()"/>
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ // Load error pages do not fire "load" events, so let's use a progressListener.
+ function waitForErrorPage(frame) {
+ return new Promise(resolve => {
+ let progressListener = {
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress)
+ .removeProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+
+ resolve();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
+ "nsISupportsWeakReference"])
+ };
+
+ frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress)
+ .addProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+ });
+ }
+
+ function doTest() {
+ (async function testBody() {
+ let frame1 = document.getElementById("frame1");
+ let frame2 = document.getElementById("frame2");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let uri1 = Services.io.newURI("http://www.example.com/1");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let uri2 = Services.io.newURI("http://www.example.com/2");
+
+ let errorPageReady = waitForErrorPage(frame1);
+ frame1.docShell.chromeEventHandler.setAttribute("crashedPageTitle", "pageTitle");
+ frame1.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri1, null);
+
+ await errorPageReady;
+ frame1.docShell.chromeEventHandler.removeAttribute("crashedPageTitle");
+
+ SimpleTest.is(frame1.contentDocument.documentURI,
+ "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/1&c=UTF-8&d=pageTitle",
+ "Correct about:tabcrashed displayed for page with title.");
+
+ errorPageReady = waitForErrorPage(frame2);
+ frame2.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri2, null);
+
+ await errorPageReady;
+
+ SimpleTest.is(frame2.contentDocument.documentURI,
+ "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/2&c=UTF-8&d=%20",
+ "Correct about:tabcrashed displayed for page with no title.");
+
+ SimpleTest.finish();
+ })().catch(ex => SimpleTest.ok(false, ex));
+ }
+ ]]></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" />
+</window>
diff --git a/browser/base/content/test/chrome/test_aboutRestartRequired.xhtml b/browser/base/content/test/chrome/test_aboutRestartRequired.xhtml
new file mode 100644
index 0000000000..9745e8e935
--- /dev/null
+++ b/browser/base/content/test/chrome/test_aboutRestartRequired.xhtml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <iframe type="content" id="frame1"/>
+ <iframe type="content" id="frame2" onload="doTest()"/>
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ // Load error pages do not fire "load" events, so let's use a progressListener.
+ function waitForErrorPage(frame) {
+ return new Promise(resolve => {
+ let progressListener = {
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress)
+ .removeProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+
+ resolve();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
+ "nsISupportsWeakReference"])
+ };
+
+ frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress)
+ .addProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+ });
+ }
+
+ function doTest() {
+ (async function testBody() {
+ let frame1 = document.getElementById("frame1");
+ let frame2 = document.getElementById("frame2");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let uri1 = Services.io.newURI("http://www.example.com/1");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let uri2 = Services.io.newURI("http://www.example.com/2");
+
+ let errorPageReady = waitForErrorPage(frame1);
+ frame1.docShell.displayLoadError(Cr.NS_ERROR_BUILDID_MISMATCH, uri1, null);
+
+ await errorPageReady;
+ frame1.docShell.chromeEventHandler.removeAttribute("crashedPageTitle");
+
+ SimpleTest.is(frame1.contentDocument.documentURI,
+ "about:restartrequired?e=restartrequired&u=http%3A//www.example.com/1&c=UTF-8&d=%20",
+ "Correct about:restartrequired displayed for page with title.");
+
+ errorPageReady = waitForErrorPage(frame2);
+ frame2.docShell.displayLoadError(Cr.NS_ERROR_BUILDID_MISMATCH, uri2, null);
+
+ await errorPageReady;
+
+ SimpleTest.is(frame2.contentDocument.documentURI,
+ "about:restartrequired?e=restartrequired&u=http%3A//www.example.com/2&c=UTF-8&d=%20",
+ "Correct about:restartrequired displayed for page with no title.");
+
+ SimpleTest.finish();
+ })().catch(ex => SimpleTest.ok(false, ex));
+ }
+ ]]></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" />
+</window>
diff --git a/browser/base/content/test/contentTheme/browser.ini b/browser/base/content/test/contentTheme/browser.ini
new file mode 100644
index 0000000000..a7c859a1b3
--- /dev/null
+++ b/browser/base/content/test/contentTheme/browser.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[browser_contentTheme_in_process_tab.js]
diff --git a/browser/base/content/test/contentTheme/browser_contentTheme_in_process_tab.js b/browser/base/content/test/contentTheme/browser_contentTheme_in_process_tab.js
new file mode 100644
index 0000000000..c53e178bc5
--- /dev/null
+++ b/browser/base/content/test/contentTheme/browser_contentTheme_in_process_tab.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that tabs running in the parent process can hear about updates
+ * to lightweight themes via contentTheme.js.
+ *
+ * The test loads the History Sidebar document in a tab to avoid having
+ * to create a special parent-process page for the LightweightTheme
+ * JSWindow actors.
+ */
+add_task(async function test_in_process_tab() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ const IN_PROCESS_URI = "chrome://browser/content/places/historySidebar.xhtml";
+ const SIDEBAR_BGCOLOR = "rgb(255, 0, 0)";
+ // contentTheme.js will always convert the sidebar text color to rgba, so
+ // we need to compare against that.
+ const SIDEBAR_TEXT_COLOR = "rgba(0, 255, 0, 1)";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: IN_PROCESS_URI,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [SIDEBAR_BGCOLOR, SIDEBAR_TEXT_COLOR],
+ async (bgColor, textColor) => {
+ let style = content.document.documentElement.style;
+ Assert.notEqual(
+ style.getPropertyValue("--lwt-sidebar-background-color"),
+ bgColor
+ );
+ Assert.notEqual(
+ style.getPropertyValue("--lwt-sidebar-text-color"),
+ textColor
+ );
+ }
+ );
+
+ // Now cobble together a very simple theme that sets the sidebar background
+ // and text color.
+ let lwtData = {
+ theme: {
+ sidebar: SIDEBAR_BGCOLOR,
+ sidebar_text: SIDEBAR_TEXT_COLOR,
+ },
+ darkTheme: null,
+ window: win.docShell.outerWindowID,
+ };
+
+ Services.obs.notifyObservers(lwtData, "lightweight-theme-styling-update");
+
+ await SpecialPowers.spawn(
+ browser,
+ [SIDEBAR_BGCOLOR, SIDEBAR_TEXT_COLOR],
+ async (bgColor, textColor) => {
+ let style = content.document.documentElement.style;
+ Assert.equal(
+ style.getPropertyValue("--lwt-sidebar-background-color"),
+ bgColor,
+ "The sidebar background text color should have been set by " +
+ "contentTheme.js"
+ );
+ Assert.equal(
+ style.getPropertyValue("--lwt-sidebar-text-color"),
+ textColor,
+ "The sidebar background text color should have been set by " +
+ "contentTheme.js"
+ );
+ }
+ );
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/contextMenu/browser.ini b/browser/base/content/test/contextMenu/browser.ini
new file mode 100644
index 0000000000..df0a013059
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser.ini
@@ -0,0 +1,91 @@
+[DEFAULT]
+support-files =
+ subtst_contextmenu_webext.html
+ test_contextmenu_links.html
+ subtst_contextmenu.html
+ subtst_contextmenu_input.html
+ subtst_contextmenu_keyword.html
+ subtst_contextmenu_xul.xhtml
+ ctxmenu-image.png
+ ../general/head.js
+ ../general/video.ogg
+ ../general/audio.ogg
+ ../../../../../toolkit/components/pdfjs/test/file_pdfjs_test.pdf
+ contextmenu_common.js
+ file_bug1798178.html
+ bug1798178.sjs
+
+[browser_bug1798178.js]
+[browser_contextmenu.js]
+tags = fullscreen
+skip-if =
+ os == "linux"
+ verify
+[browser_contextmenu_badiframe.js]
+https_first_disabled = true
+skip-if =
+ os == "win" # Bug 1719856
+ os == "linux" && socketprocess_networking
+[browser_contextmenu_contenteditable.js]
+[browser_contextmenu_iframe.js]
+support-files =
+ test_contextmenu_iframe.html
+skip-if =
+ os == "linux" && socketprocess_networking
+[browser_contextmenu_input.js]
+skip-if =
+ os == "linux"
+[browser_contextmenu_inspect.js]
+skip-if =
+ os == "linux" && socketprocess_networking
+[browser_contextmenu_keyword.js]
+skip-if =
+ os == "linux" # disabled on Linux due to bug 513558
+[browser_contextmenu_linkopen.js]
+skip-if =
+ os == "linux" && socketprocess_networking
+[browser_contextmenu_loadblobinnewtab.js]
+support-files = browser_contextmenu_loadblobinnewtab.html
+skip-if =
+ os == "linux" && socketprocess_networking
+[browser_contextmenu_save_blocked.js]
+skip-if =
+ os == "linux" && socketprocess_networking
+[browser_contextmenu_share_macosx.js]
+support-files =
+ browser_contextmenu_shareurl.html
+run-if =
+ os == "mac"
+[browser_contextmenu_share_win.js]
+https_first_disabled = true
+support-files =
+ browser_contextmenu_shareurl.html
+run-if =
+ os == "win"
+[browser_contextmenu_spellcheck.js]
+https_first_disabled = true
+skip-if =
+ os == "linux"
+ debug # bug 1798233 - this trips assertions that seem harmless in opt and unlikely to occur in practical use.
+[browser_contextmenu_touch.js]
+skip-if = true # Bug 1424433, disable due to very high frequency failure rate also on Windows 10
+[browser_copy_image_link.js]
+support-files =
+ doggy.png
+ firebird.png
+ firebird.png^headers^
+skip-if =
+ os == "linux" && socketprocess_networking
+[browser_strip_on_share_link.js]
+[browser_utilityOverlay.js]
+https_first_disabled = true
+skip-if =
+ os == "linux" && socketprocess_networking
+[browser_utilityOverlayPrincipal.js]
+https_first_disabled = true
+[browser_view_image.js]
+support-files =
+ test_view_image_revoked_cached_blob.html
+ test_view_image_inline_svg.html
+skip-if =
+ os == "linux" && socketprocess_networking
diff --git a/browser/base/content/test/contextMenu/browser_bug1798178.js b/browser/base/content/test/contextMenu/browser_bug1798178.js
new file mode 100644
index 0000000000..529665a6f9
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_bug1798178.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_bug1798178.html";
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+function createTemporarySaveDirectory() {
+ let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ info("create testsavedir!");
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ info("return from createTempSaveDir: " + saveDir.path);
+ return saveDir;
+}
+
+add_task(async function test_save_link_cross_origin() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.opaqueResponseBlocking", true]],
+ });
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a[href]",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShown;
+
+ let filePickerShow = new Promise(r => {
+ MockFilePicker.showCallback = function (fp) {
+ ok(true, "filepicker should be shown");
+ info("MockFilePicker showCallback");
+
+ let fileName = fp.defaultString;
+ destFile = tempDir.clone();
+ destFile.append(fileName);
+
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+
+ info("MockFilePicker showCallback done");
+ r();
+ };
+ });
+
+ info("Let's create a temporary dir");
+ let tempDir = createTemporarySaveDirectory();
+ let destFile;
+
+ MockFilePicker.displayDirectory = tempDir;
+
+ let transferCompletePromise = new Promise(resolve => {
+ function onTransferComplete(downloadSuccess) {
+ ok(downloadSuccess, "File should have been downloaded successfully");
+ resolve();
+ }
+ mockTransferCallback = onTransferComplete;
+ mockTransferRegisterer.register();
+ });
+
+ let saveLinkCommand = document.getElementById("context-savelink");
+ info("saveLinkCommand: " + saveLinkCommand);
+ saveLinkCommand.doCommand();
+
+ await filePickerShow;
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await popupHiddenPromise;
+
+ await transferCompletePromise;
+ });
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu.js b/browser/base/content/test/contextMenu/browser_contextmenu.js
new file mode 100644
index 0000000000..03a848f26d
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu.js
@@ -0,0 +1,1943 @@
+"use strict";
+
+let contextMenu;
+let LOGIN_FILL_ITEMS = ["---", null, "manage-saved-logins", true];
+let NAVIGATION_ITEMS =
+ AppConstants.platform == "macosx"
+ ? [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "---",
+ null,
+ "context-bookmarkpage",
+ true,
+ ]
+ : [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ ];
+let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
+let hasContainers =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ ContextualIdentityService.getPublicIdentities().length;
+
+const example_base =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/contextMenu/";
+const chrome_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+const head_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+
+/* import-globals-from contextmenu_common.js */
+Services.scriptloader.loadSubScript(
+ chrome_base + "contextmenu_common.js",
+ this
+);
+
+function getThisFrameSubMenu(base_menu) {
+ if (AppConstants.NIGHTLY_BUILD) {
+ let osPidItem = ["context-frameOsPid", false];
+ base_menu = base_menu.concat(osPidItem);
+ }
+ return base_menu;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["extensions.screenshots.disabled", false],
+ ["layout.forms.reveal-password-context-menu.enabled", true],
+ ],
+ });
+});
+
+// Below are test cases for XUL element
+add_task(async function test_xul_text_link_label() {
+ let url = chrome_base + "subtst_contextmenu_xul.xhtml";
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url,
+ waitForLoad: true,
+ waitForStateStop: true,
+ });
+
+ await test_contextmenu("#test-xul-text-link-label", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+
+ // Clean up so won't affect HTML element test cases.
+ lastElementSelector = null;
+ gBrowser.removeCurrentTab();
+});
+
+// Below are test cases for HTML element.
+
+add_task(async function test_setup_html() {
+ let url = example_base + "subtst_contextmenu.html";
+
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let doc = content.document;
+ let audioIframe = doc.querySelector("#test-audio-in-iframe");
+ // media documents always use a <video> tag.
+ let audio = audioIframe.contentDocument.querySelector("video");
+ let videoIframe = doc.querySelector("#test-video-in-iframe");
+ let video = videoIframe.contentDocument.querySelector("video");
+
+ audio.loop = true;
+ audio.src = "audio.ogg";
+ video.loop = true;
+ video.src = "video.ogg";
+
+ let awaitPause = ContentTaskUtils.waitForEvent(audio, "pause");
+ await ContentTaskUtils.waitForCondition(
+ () => !audio.paused,
+ "Making sure audio is playing before calling pause"
+ );
+ audio.pause();
+ await awaitPause;
+
+ awaitPause = ContentTaskUtils.waitForEvent(video, "pause");
+ await ContentTaskUtils.waitForCondition(
+ () => !video.paused,
+ "Making sure video is playing before calling pause"
+ );
+ video.pause();
+ await awaitPause;
+ });
+});
+
+let plainTextItems;
+add_task(async function test_plaintext() {
+ await test_contextmenu("#test-text", [
+ ...NAVIGATION_ITEMS,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ]);
+});
+
+const kLinkItems = [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+];
+
+add_task(async function test_link() {
+ await test_contextmenu("#test-link", kLinkItems);
+});
+
+add_task(async function test_link_in_shadow_dom() {
+ await test_contextmenu("#shadow-host", kLinkItems, {
+ offsetX: 6,
+ offsetY: 6,
+ });
+});
+
+add_task(async function test_link_over_shadow_dom() {
+ await test_contextmenu("#shadow-host-in-link", kLinkItems, {
+ offsetX: 6,
+ offsetY: 6,
+ });
+});
+
+add_task(async function test_mailto() {
+ await test_contextmenu("#test-mailto", [
+ "context-copyemail",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+});
+
+add_task(async function test_tel() {
+ await test_contextmenu("#test-tel", [
+ "context-copyphone",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+});
+
+add_task(async function test_image() {
+ for (let selector of ["#test-image", "#test-svg-image"]) {
+ await test_contextmenu(
+ selector,
+ [
+ "context-viewimage",
+ true,
+ "context-saveimage",
+ true,
+ "context-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "context-sendimage",
+ true,
+ ...getTextRecognitionItems(),
+ ...(Services.prefs.getBoolPref("browser.menu.showViewImageInfo", false)
+ ? ["context-viewimageinfo", true]
+ : []),
+ "---",
+ null,
+ "context-setDesktopBackground",
+ true,
+ ],
+ {
+ onContextMenuShown() {
+ is(
+ typeof gContextMenu.imageInfo.height,
+ "number",
+ "Should have height"
+ );
+ is(
+ typeof gContextMenu.imageInfo.width,
+ "number",
+ "Should have width"
+ );
+ },
+ }
+ );
+ }
+});
+
+add_task(async function test_canvas() {
+ await test_contextmenu("#test-canvas", [
+ "context-viewimage",
+ true,
+ "context-saveimage",
+ true,
+ "---",
+ null,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ ]);
+});
+
+add_task(async function test_video_ok() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", true]],
+ });
+
+ await test_contextmenu("#test-video-ok", [
+ "context-media-play",
+ true,
+ "context-media-mute",
+ true,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ true,
+ "context-media-playbackrate-100x",
+ true,
+ "context-media-playbackrate-125x",
+ true,
+ "context-media-playbackrate-150x",
+ true,
+ "context-media-playbackrate-200x",
+ true,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ true,
+ "context-media-hidecontrols",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-video-pictureinpicture",
+ true,
+ "---",
+ null,
+ "context-video-saveimage",
+ true,
+ "context-savevideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "context-sendvideo",
+ true,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", false]],
+ });
+
+ await test_contextmenu("#test-video-ok", [
+ "context-media-play",
+ true,
+ "context-media-mute",
+ true,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ true,
+ "context-media-playbackrate-100x",
+ true,
+ "context-media-playbackrate-125x",
+ true,
+ "context-media-playbackrate-150x",
+ true,
+ "context-media-playbackrate-200x",
+ true,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ true,
+ "context-media-hidecontrols",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "---",
+ null,
+ "context-video-saveimage",
+ true,
+ "context-savevideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "context-sendvideo",
+ true,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_audio_in_video() {
+ await test_contextmenu("#test-audio-in-video", [
+ "context-media-play",
+ true,
+ "context-media-mute",
+ true,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ true,
+ "context-media-playbackrate-100x",
+ true,
+ "context-media-playbackrate-125x",
+ true,
+ "context-media-playbackrate-150x",
+ true,
+ "context-media-playbackrate-200x",
+ true,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-media-showcontrols",
+ true,
+ "---",
+ null,
+ "context-saveaudio",
+ true,
+ "context-copyaudiourl",
+ true,
+ "context-sendaudio",
+ true,
+ ]);
+});
+
+add_task(async function test_video_bad() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", true]],
+ });
+
+ await test_contextmenu("#test-video-bad", [
+ "context-media-play",
+ false,
+ "context-media-mute",
+ false,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ false,
+ "context-media-playbackrate-100x",
+ false,
+ "context-media-playbackrate-125x",
+ false,
+ "context-media-playbackrate-150x",
+ false,
+ "context-media-playbackrate-200x",
+ false,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ false,
+ "context-media-hidecontrols",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "---",
+ null,
+ "context-video-saveimage",
+ false,
+ "context-savevideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "context-sendvideo",
+ true,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", false]],
+ });
+
+ await test_contextmenu("#test-video-bad", [
+ "context-media-play",
+ false,
+ "context-media-mute",
+ false,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ false,
+ "context-media-playbackrate-100x",
+ false,
+ "context-media-playbackrate-125x",
+ false,
+ "context-media-playbackrate-150x",
+ false,
+ "context-media-playbackrate-200x",
+ false,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ false,
+ "context-media-hidecontrols",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "---",
+ null,
+ "context-video-saveimage",
+ false,
+ "context-savevideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "context-sendvideo",
+ true,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_video_bad2() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", true]],
+ });
+
+ await test_contextmenu("#test-video-bad2", [
+ "context-media-play",
+ false,
+ "context-media-mute",
+ false,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ false,
+ "context-media-playbackrate-100x",
+ false,
+ "context-media-playbackrate-125x",
+ false,
+ "context-media-playbackrate-150x",
+ false,
+ "context-media-playbackrate-200x",
+ false,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ false,
+ "context-media-hidecontrols",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ false,
+ "---",
+ null,
+ "context-video-saveimage",
+ false,
+ "context-savevideo",
+ false,
+ "context-copyvideourl",
+ false,
+ "context-sendvideo",
+ false,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", false]],
+ });
+
+ await test_contextmenu("#test-video-bad2", [
+ "context-media-play",
+ false,
+ "context-media-mute",
+ false,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ false,
+ "context-media-playbackrate-100x",
+ false,
+ "context-media-playbackrate-125x",
+ false,
+ "context-media-playbackrate-150x",
+ false,
+ "context-media-playbackrate-200x",
+ false,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ false,
+ "context-media-hidecontrols",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ false,
+ "---",
+ null,
+ "context-video-saveimage",
+ false,
+ "context-savevideo",
+ false,
+ "context-copyvideourl",
+ false,
+ "context-sendvideo",
+ false,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_iframe() {
+ await test_contextmenu("#test-iframe", [
+ ...NAVIGATION_ITEMS,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-showonlythisframe",
+ true,
+ "context-openframeintab",
+ true,
+ "context-openframe",
+ true,
+ "---",
+ null,
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-bookmarkframe",
+ true,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-take-frame-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewframesource",
+ true,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ]);
+});
+
+add_task(async function test_video_in_iframe() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", true]],
+ });
+
+ await test_contextmenu("#test-video-in-iframe", [
+ "context-media-play",
+ true,
+ "context-media-mute",
+ true,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ true,
+ "context-media-playbackrate-100x",
+ true,
+ "context-media-playbackrate-125x",
+ true,
+ "context-media-playbackrate-150x",
+ true,
+ "context-media-playbackrate-200x",
+ true,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ true,
+ "context-media-hidecontrols",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-video-pictureinpicture",
+ true,
+ "---",
+ null,
+ "context-video-saveimage",
+ true,
+ "context-savevideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "context-sendvideo",
+ true,
+ "---",
+ null,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-showonlythisframe",
+ true,
+ "context-openframeintab",
+ true,
+ "context-openframe",
+ true,
+ "---",
+ null,
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-bookmarkframe",
+ true,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.videocontrols.picture-in-picture.enabled", false]],
+ });
+
+ await test_contextmenu("#test-video-in-iframe", [
+ "context-media-play",
+ true,
+ "context-media-mute",
+ true,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ true,
+ "context-media-playbackrate-100x",
+ true,
+ "context-media-playbackrate-125x",
+ true,
+ "context-media-playbackrate-150x",
+ true,
+ "context-media-playbackrate-200x",
+ true,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "context-video-fullscreen",
+ true,
+ "context-media-hidecontrols",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "---",
+ null,
+ "context-video-saveimage",
+ true,
+ "context-savevideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "context-sendvideo",
+ true,
+ "---",
+ null,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-showonlythisframe",
+ true,
+ "context-openframeintab",
+ true,
+ "context-openframe",
+ true,
+ "---",
+ null,
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-bookmarkframe",
+ true,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_audio_in_iframe() {
+ await test_contextmenu("#test-audio-in-iframe", [
+ "context-media-play",
+ true,
+ "context-media-mute",
+ true,
+ "context-media-playbackrate",
+ null,
+ [
+ "context-media-playbackrate-050x",
+ true,
+ "context-media-playbackrate-100x",
+ true,
+ "context-media-playbackrate-125x",
+ true,
+ "context-media-playbackrate-150x",
+ true,
+ "context-media-playbackrate-200x",
+ true,
+ ],
+ null,
+ "context-media-loop",
+ true,
+ "---",
+ null,
+ "context-saveaudio",
+ true,
+ "context-copyaudiourl",
+ true,
+ "context-sendaudio",
+ true,
+ "---",
+ null,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-showonlythisframe",
+ true,
+ "context-openframeintab",
+ true,
+ "context-openframe",
+ true,
+ "---",
+ null,
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-bookmarkframe",
+ true,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ ]);
+});
+
+add_task(async function test_image_in_iframe() {
+ await test_contextmenu("#test-image-in-iframe", [
+ "context-viewimage",
+ true,
+ "context-saveimage",
+ true,
+ "context-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "context-sendimage",
+ true,
+ ...getTextRecognitionItems(),
+ ...(Services.prefs.getBoolPref("browser.menu.showViewImageInfo", false)
+ ? ["context-viewimageinfo", true]
+ : []),
+ "---",
+ null,
+ "context-setDesktopBackground",
+ true,
+ "---",
+ null,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-showonlythisframe",
+ true,
+ "context-openframeintab",
+ true,
+ "context-openframe",
+ true,
+ "---",
+ null,
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-bookmarkframe",
+ true,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ ]);
+});
+
+add_task(async function test_pdf_viewer_in_iframe() {
+ await test_contextmenu(
+ "#test-pdf-viewer-in-frame",
+ [
+ ...NAVIGATION_ITEMS,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-showonlythisframe",
+ true,
+ "context-openframeintab",
+ true,
+ "context-openframe",
+ true,
+ "---",
+ null,
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-bookmarkframe",
+ true,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-take-frame-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ],
+ {
+ shiftkey: true,
+ }
+ );
+});
+
+add_task(async function test_textarea() {
+ // Disabled since this is seeing spell-check-enabled
+ // instead of spell-add-dictionaries-main
+ todo(false, "spell checker tests are failing, bug 1246296");
+
+ /*
+ yield test_contextmenu("#test-textarea",
+ ["context-undo", false,
+ "context-redo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null,
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-add-dictionaries-main", true,
+ ],
+ {
+ skipFocusChange: true,
+ }
+ );
+ */
+});
+
+add_task(async function test_textarea_spellcheck() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+
+ /*
+ yield test_contextmenu("#test-textarea",
+ ["*chubbiness", true, // spelling suggestion
+ "spell-add-to-dictionary", true,
+ "---", null,
+ "context-undo", false,
+ "context-redo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ],
+ {
+ waitForSpellCheck: true,
+ offsetX: 6,
+ offsetY: 6,
+ postCheckContextMenuFn() {
+ document.getElementById("spell-add-to-dictionary").doCommand();
+ }
+ }
+ );
+ */
+});
+
+add_task(async function test_plaintext2() {
+ await test_contextmenu("#test-text", plainTextItems);
+});
+
+add_task(async function test_undo_add_to_dictionary() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+
+ /*
+ yield test_contextmenu("#test-textarea",
+ ["spell-undo-add-to-dictionary", true,
+ "---", null,
+ "context-undo", false,
+ "context-redo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ],
+ {
+ waitForSpellCheck: true,
+ postCheckContextMenuFn() {
+ document.getElementById("spell-undo-add-to-dictionary")
+ .doCommand();
+ }
+ }
+ );
+ */
+});
+
+add_task(async function test_contenteditable() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+
+ /*
+ yield test_contextmenu("#test-contenteditable",
+ ["spell-no-suggestions", false,
+ "spell-add-to-dictionary", true,
+ "---", null,
+ "context-undo", false,
+ "context-redo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ],
+ {waitForSpellCheck: true}
+ );
+ */
+});
+
+add_task(async function test_copylinkcommand() {
+ await test_contextmenu("#test-link", null, {
+ async postCheckContextMenuFn() {
+ document.commandDispatcher
+ .getControllerForCommand("cmd_copyLink")
+ .doCommand("cmd_copyLink");
+
+ // The easiest way to check the clipboard is to paste the contents
+ // into a textbox.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("test-input");
+ input.focus();
+ input.value = "";
+ }
+ );
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("test-input");
+ Assert.equal(
+ input.value,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://mozilla.com/",
+ "paste for command cmd_paste"
+ );
+ // Don't keep focus, because that may affect clipboard commands in
+ // subsequently-opened menus.
+ input.blur();
+ }
+ );
+ },
+ });
+});
+
+add_task(async function test_dom_full_screen() {
+ let fullscreenItems = NAVIGATION_ITEMS.concat([
+ "context-leave-dom-fullscreen",
+ true,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ]);
+ if (AppConstants.platform == "macosx") {
+ // Put the bookmarks item next to save page:
+ const bmPageIndex = fullscreenItems.indexOf("context-bookmarkpage");
+ let bmPageItems = fullscreenItems.splice(bmPageIndex, 2);
+ fullscreenItems.splice(
+ fullscreenItems.indexOf("context-savepage"),
+ 0,
+ ...bmPageItems
+ );
+ }
+ await test_contextmenu("#test-dom-full-screen", fullscreenItems, {
+ shiftkey: true,
+ async preCheckContextMenuFn() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ ],
+ });
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let win = doc.defaultView;
+ let full_screen_element = doc.getElementById("test-dom-full-screen");
+ let awaitFullScreenChange = ContentTaskUtils.waitForEvent(
+ win,
+ "fullscreenchange"
+ );
+ full_screen_element.requestFullscreen();
+ await awaitFullScreenChange;
+ }
+ );
+ },
+ async postCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let win = content.document.defaultView;
+ let awaitFullScreenChange = ContentTaskUtils.waitForEvent(
+ win,
+ "fullscreenchange"
+ );
+ content.document.exitFullscreen();
+ await awaitFullScreenChange;
+ }
+ );
+ },
+ });
+});
+
+add_task(async function test_pagemenu2() {
+ await test_contextmenu(
+ "#test-text",
+ [
+ ...NAVIGATION_ITEMS,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ],
+ { shiftkey: true }
+ );
+});
+
+add_task(async function test_select_text() {
+ await test_contextmenu(
+ "#test-select-text",
+ [
+ "context-copy",
+ true,
+ "context-selectall",
+ true,
+ "context-print-selection",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-viewpartialsource-selection",
+ true,
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ async preCheckContextMenuFn() {
+ await selectText("#test-select-text");
+ },
+ }
+ );
+});
+
+add_task(async function test_select_text_search_service_not_initialized() {
+ // Pretend the search service is not initialised.
+ Services.search.wrappedJSObject.forceInitializationStatusForTests(
+ "not initialized"
+ );
+ await test_contextmenu(
+ "#test-select-text",
+ [
+ "context-copy",
+ true,
+ "context-selectall",
+ true,
+ "context-print-selection",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewpartialsource-selection",
+ true,
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ async preCheckContextMenuFn() {
+ await selectText("#test-select-text");
+ },
+ }
+ );
+
+ // Restore the search service initialization status
+ Services.search.wrappedJSObject.forceInitializationStatusForTests("success");
+});
+
+add_task(async function test_select_text_link() {
+ await test_contextmenu(
+ "#test-select-text-link",
+ [
+ "context-openlinkincurrent",
+ true,
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ "---",
+ null,
+ "context-copy",
+ true,
+ "context-selectall",
+ true,
+ "context-print-selection",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-viewpartialsource-selection",
+ true,
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ async preCheckContextMenuFn() {
+ await selectText("#test-select-text-link");
+ },
+ async postCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let win = content.document.defaultView;
+ win.getSelection().removeAllRanges();
+ }
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_imagelink() {
+ await test_contextmenu("#test-image-link", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-viewimage",
+ true,
+ "context-saveimage",
+ true,
+ "context-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "context-sendimage",
+ true,
+ ...getTextRecognitionItems(),
+ ...(Services.prefs.getBoolPref("browser.menu.showViewImageInfo", false)
+ ? ["context-viewimageinfo", true]
+ : []),
+ "---",
+ null,
+ "context-setDesktopBackground",
+ true,
+ ]);
+});
+
+add_task(async function test_select_input_text() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+
+ /*
+ yield test_contextmenu("#test-select-input-text",
+ ["context-undo", false,
+ "context-redo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", true,
+ "context-selectall", true,
+ "---", null,
+ "context-searchselect", true,
+ "context-searchselect-private", true,
+ "---", null,
+ "spell-check-enabled", true
+ ].concat(LOGIN_FILL_ITEMS),
+ {
+ *preCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.getSelection().removeAllRanges();
+ let element = doc.querySelector("#test-select-input-text");
+ element.select();
+ });
+ }
+ }
+ );
+ */
+});
+
+add_task(async function test_select_input_text_password() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+
+ /*
+ yield test_contextmenu("#test-select-input-text-type-password",
+ ["context-undo", false,
+ "context-redo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", true,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ // spell checker is shown on input[type="password"] on this testcase
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ].concat(LOGIN_FILL_ITEMS),
+ {
+ *preCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.getSelection().removeAllRanges();
+ let element = doc.querySelector("#test-select-input-text-type-password");
+ element.select();
+ });
+ },
+ *postCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let win = content.document.defaultView;
+ win.getSelection().removeAllRanges();
+ });
+ }
+ }
+ );
+ */
+});
+
+add_task(async function test_longdesc() {
+ await test_contextmenu("#test-longdesc", [
+ "context-viewimage",
+ true,
+ "context-saveimage",
+ true,
+ "context-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "context-sendimage",
+ true,
+ ...getTextRecognitionItems(),
+ ...(Services.prefs.getBoolPref("browser.menu.showViewImageInfo", false)
+ ? ["context-viewimageinfo", true]
+ : []),
+ "context-viewimagedesc",
+ true,
+ "---",
+ null,
+ "context-setDesktopBackground",
+ true,
+ ]);
+});
+
+add_task(async function test_srcdoc() {
+ await test_contextmenu("#test-srcdoc", [
+ ...NAVIGATION_ITEMS,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-take-frame-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewframesource",
+ true,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ]);
+});
+
+add_task(async function test_input_spell_false() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+
+ /*
+ yield test_contextmenu("#test-contenteditable-spellcheck-false",
+ ["context-undo", false,
+ "context-redo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "context-selectall", true,
+ ]
+ );
+ */
+});
+
+add_task(async function test_svg_link() {
+ await test_contextmenu("#svg-with-link > a", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+
+ await test_contextmenu("#svg-with-link2 > a", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+
+ await test_contextmenu("#svg-with-link3 > a", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+});
+
+add_task(async function test_svg_relative_link() {
+ await test_contextmenu("#svg-with-relative-link > a", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+
+ await test_contextmenu("#svg-with-relative-link2 > a", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+
+ await test_contextmenu("#svg-with-relative-link3 > a", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+});
+
+add_task(async function test_background_image() {
+ let bgImageItems = [
+ "context-viewimage",
+ true,
+ "context-copyimage",
+ true,
+ "context-sendimage",
+ true,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ];
+ if (AppConstants.platform == "macosx") {
+ // Back/fwd/(stop|reload) and their separator go before the image items,
+ // followed by the bookmark item which goes with save page - so we need
+ // to split up NAVIGATION_ITEMS and bgImageItems:
+ bgImageItems = [
+ ...NAVIGATION_ITEMS.slice(0, 8),
+ ...bgImageItems.slice(0, 8),
+ ...NAVIGATION_ITEMS.slice(8),
+ ...bgImageItems.slice(8),
+ ];
+ } else {
+ bgImageItems = NAVIGATION_ITEMS.concat(bgImageItems);
+ }
+ await test_contextmenu("#test-background-image", bgImageItems);
+
+ // Don't show image related context menu commands for links with background images.
+ await test_contextmenu("#test-background-image-link", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ ]);
+
+ // Don't show image related context menu commands when there is a selection
+ // with background images.
+ await test_contextmenu(
+ "#test-background-image",
+ [
+ "context-copy",
+ true,
+ "context-selectall",
+ true,
+ "context-print-selection",
+ true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-viewpartialsource-selection",
+ true,
+ ],
+ {
+ async preCheckContextMenuFn() {
+ await selectText("#test-background-image");
+ },
+ }
+ );
+});
+
+add_task(async function test_cleanup_html() {
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * Selects the text of the element that matches the provided `selector`
+ *
+ * @param {String} selector
+ * A selector passed to querySelector to find
+ * the element that will be referenced.
+ */
+async function selectText(selector) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [selector],
+ async function (contentSelector) {
+ info(`Selecting text of ${contentSelector}`);
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.getSelection().removeAllRanges();
+ let div = doc.createRange();
+ let element = doc.querySelector(contentSelector);
+ Assert.ok(element, "Found element to select text from");
+ div.setStartBefore(element);
+ div.setEndAfter(element);
+ win.getSelection().addRange(div);
+ }
+ );
+}
+
+/**
+ * Not all platforms support text recognition.
+ * @returns {string[]}
+ */
+function getTextRecognitionItems() {
+ return Services.prefs.getBoolPref("dom.text-recognition.enabled") &&
+ Services.appinfo.isTextRecognitionSupported
+ ? ["context-imagetext", true]
+ : [];
+}
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js b/browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js
new file mode 100644
index 0000000000..89e7fe15e0
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js
@@ -0,0 +1,182 @@
+/* Tests for proper behaviour of "Show this frame" context menu options with a valid frame and
+ a frame with an invalid url.
+ */
+
+// Two frames, one with text content, the other an error page
+var invalidPage = "http://127.0.0.1:55555/";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+var validPage = "http://example.com/";
+var testPage =
+ 'data:text/html,<frameset cols="400,400"><frame src="' +
+ validPage +
+ '"><frame src="' +
+ invalidPage +
+ '"></frameset>';
+
+async function openTestPage() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ true,
+ true
+ );
+ let browser = tab.linkedBrowser;
+
+ // The test page has a top-level document and two subframes. One of
+ // those subframes is an error page, which doesn't fire a load event.
+ // We'll use BrowserTestUtils.browserLoaded and have it wait for all
+ // 3 loads before resolving.
+ let expectedLoads = 3;
+ let pageAndIframesLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ true /* includeSubFrames */,
+ url => {
+ expectedLoads--;
+ return !expectedLoads;
+ },
+ true /* maybeErrorPage */
+ );
+ BrowserTestUtils.loadURIString(browser, testPage);
+ await pageAndIframesLoaded;
+
+ // Make sure both the top-level document and the iframe documents have
+ // had a chance to present. We need this so that the context menu event
+ // gets dispatched properly.
+ for (let bc of [
+ ...browser.browsingContext.children,
+ browser.browsingContext,
+ ]) {
+ await SpecialPowers.spawn(bc, [], async function () {
+ await new Promise(resolve => {
+ content.requestAnimationFrame(resolve);
+ });
+ });
+ }
+}
+
+async function selectFromFrameMenu(frameNumber, menuId) {
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtPoint(
+ 40,
+ 40,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ gBrowser.selectedBrowser.browsingContext.children[frameNumber]
+ );
+
+ await popupShownPromise;
+
+ let frameItem = document.getElementById("frame");
+ let framePopup = frameItem.menupopup;
+ let subPopupShownPromise = BrowserTestUtils.waitForEvent(
+ framePopup,
+ "popupshown"
+ );
+
+ frameItem.openMenu(true);
+ await subPopupShownPromise;
+
+ let subPopupHiddenPromise = BrowserTestUtils.waitForEvent(
+ framePopup,
+ "popuphidden"
+ );
+ let contextMenuHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.activateItem(document.getElementById(menuId));
+ await subPopupHiddenPromise;
+ await contextMenuHiddenPromise;
+}
+
+add_task(async function testOpenFrame() {
+ for (let frameNumber = 0; frameNumber < 2; frameNumber++) {
+ await openTestPage();
+
+ let expectedResultURI = [validPage, invalidPage][frameNumber];
+
+ info("show only this frame for " + expectedResultURI);
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedResultURI,
+ frameNumber == 1
+ );
+
+ await selectFromFrameMenu(frameNumber, "context-showonlythisframe");
+ await browserLoadedPromise;
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ expectedResultURI,
+ "Should navigate to page url, not about:neterror"
+ );
+
+ gBrowser.removeCurrentTab();
+ }
+});
+
+add_task(async function testOpenFrameInTab() {
+ for (let frameNumber = 0; frameNumber < 2; frameNumber++) {
+ await openTestPage();
+
+ let expectedResultURI = [validPage, invalidPage][frameNumber];
+
+ info("open frame in tab for " + expectedResultURI);
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ expectedResultURI,
+ false
+ );
+ await selectFromFrameMenu(frameNumber, "context-openframeintab");
+ let newTab = await newTabPromise;
+
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+
+ // We should now have the error page in a new, active tab.
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ expectedResultURI,
+ "New tab should have page url, not about:neterror"
+ );
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ }
+});
+
+add_task(async function testOpenFrameInWindow() {
+ for (let frameNumber = 0; frameNumber < 2; frameNumber++) {
+ await openTestPage();
+
+ let expectedResultURI = [validPage, invalidPage][frameNumber];
+
+ info("open frame in window for " + expectedResultURI);
+
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: frameNumber == 1 ? invalidPage : validPage,
+ maybeErrorPage: frameNumber == 1,
+ });
+ await selectFromFrameMenu(frameNumber, "context-openframe");
+ let newWindow = await newWindowPromise;
+
+ is(
+ newWindow.gBrowser.selectedBrowser.currentURI.spec,
+ expectedResultURI,
+ "New window should have page url, not about:neterror"
+ );
+
+ newWindow.close();
+ gBrowser.removeCurrentTab();
+ }
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_contenteditable.js b/browser/base/content/test/contextMenu/browser_contextmenu_contenteditable.js
new file mode 100644
index 0000000000..ccb0be8d95
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_contenteditable.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let contextMenu;
+
+const example_base =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/contextMenu/";
+const chrome_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+
+/* import-globals-from contextmenu_common.js */
+Services.scriptloader.loadSubScript(
+ chrome_base + "contextmenu_common.js",
+ this
+);
+
+async function openMenuAndPaste(browser, useFormatting) {
+ const kElementToUse = "test-contenteditable-spellcheck-false";
+ let oldText = await SpecialPowers.spawn(browser, [kElementToUse], elemID => {
+ return content.document.getElementById(elemID).textContent;
+ });
+
+ // Open context menu and paste
+ await test_contextmenu(
+ "#" + kElementToUse,
+ [
+ "context-undo",
+ null, // whether we can undo changes mid-test.
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ true,
+ "context-paste-no-formatting",
+ true,
+ "context-delete",
+ false,
+ "context-selectall",
+ true,
+ ],
+ {
+ keepMenuOpen: true,
+ }
+ );
+ let popupHidden = BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden");
+ let menuID = "context-paste" + (useFormatting ? "" : "-no-formatting");
+ contextMenu.activateItem(document.getElementById(menuID));
+ await popupHidden;
+ await SpecialPowers.spawn(
+ browser,
+ [kElementToUse, oldText, useFormatting],
+ (elemID, textToReset, expectStrong) => {
+ let node = content.document.getElementById(elemID);
+ Assert.stringContains(
+ node.textContent,
+ "Bold text",
+ "Text should have been pasted"
+ );
+ if (expectStrong) {
+ isnot(
+ node.querySelector("strong"),
+ null,
+ "Should be markup in the text."
+ );
+ } else {
+ is(
+ node.querySelector("strong"),
+ null,
+ "Should be no markup in the text."
+ );
+ }
+ node.textContent = textToReset;
+ }
+ );
+}
+
+add_task(async function test_contenteditable() {
+ // Put some HTML on the clipboard:
+ const xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ xferable.init(null);
+ xferable.addDataFlavor("text/html");
+ xferable.setTransferData(
+ "text/html",
+ PlacesUtils.toISupportsString("<strong>Bold text</strong>")
+ );
+ xferable.addDataFlavor("text/plain");
+ xferable.setTransferData(
+ "text/plain",
+ PlacesUtils.toISupportsString("Bold text")
+ );
+ Services.clipboard.setData(
+ xferable,
+ null,
+ Services.clipboard.kGlobalClipboard
+ );
+
+ let url = example_base + "subtst_contextmenu.html";
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ await openMenuAndPaste(browser, false);
+ await openMenuAndPaste(browser, true);
+ }
+ );
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_iframe.js b/browser/base/content/test/contextMenu/browser_contextmenu_iframe.js
new file mode 100644
index 0000000000..bd52862eb4
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_iframe.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_LINK = "https://example.com/";
+const RESOURCE_LINK =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "test_contextmenu_iframe.html";
+
+/* This test checks that a context menu can open up
+ * a frame into it's own tab. */
+
+add_task(async function test_open_iframe() {
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ RESOURCE_LINK
+ );
+ const selector = "#iframe";
+ const openPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_LINK,
+ false
+ );
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+ is(contextMenu.state, "closed", "checking if popup is closed");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ {
+ type: "contextmenu",
+ button: 2,
+ centered: true,
+ },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+ info("Popup Shown");
+ const awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ // Open frame submenu
+ const frameItem = contextMenu.querySelector("#frame");
+ const menuPopup = frameItem.menupopup;
+ const menuPopupPromise = BrowserTestUtils.waitForEvent(
+ menuPopup,
+ "popupshown"
+ );
+ frameItem.openMenu(true);
+ await menuPopupPromise;
+
+ let domItem = contextMenu.querySelector("#context-openframeintab");
+ info("Going to click item " + domItem.id);
+ ok(
+ BrowserTestUtils.is_visible(domItem),
+ "DOM context menu item tab should be visible"
+ );
+ ok(!domItem.disabled, "DOM context menu item tab shouldn't be disabled");
+ contextMenu.activateItem(domItem);
+
+ let openedTab = await openPromise;
+ await awaitPopupHidden;
+ await BrowserTestUtils.removeTab(openedTab);
+
+ BrowserTestUtils.removeTab(testTab);
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_input.js b/browser/base/content/test/contextMenu/browser_contextmenu_input.js
new file mode 100644
index 0000000000..c580a8184a
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_input.js
@@ -0,0 +1,387 @@
+"use strict";
+
+let contextMenu;
+let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
+
+const NAVIGATION_ITEMS =
+ AppConstants.platform == "macosx"
+ ? [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "---",
+ null,
+ "context-bookmarkpage",
+ true,
+ ]
+ : [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ ];
+
+add_task(async function test_setup() {
+ const example_base =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/contextMenu/";
+ const url = example_base + "subtst_contextmenu_input.html";
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ const chrome_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+ const contextmenu_common = chrome_base + "contextmenu_common.js";
+ /* import-globals-from contextmenu_common.js */
+ Services.scriptloader.loadSubScript(contextmenu_common, this);
+});
+
+add_task(async function test_text_input() {
+ await test_contextmenu("#input_text", [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ false,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ ]);
+});
+
+add_task(async function test_text_input_disabled() {
+ await test_contextmenu(
+ "#input_disabled",
+ [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ false,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ ],
+ { skipFocusChange: true }
+ );
+});
+
+add_task(async function test_password_input() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.generation.enabled", false],
+ ["layout.forms.reveal-password-context-menu.enabled", true],
+ ],
+ });
+ todo(
+ false,
+ "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled"
+ );
+ await test_contextmenu(
+ "#input_password",
+ [
+ "manage-saved-logins",
+ true,
+ "---",
+ null,
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ null,
+ "context-reveal-password",
+ null,
+ ],
+ {
+ skipFocusChange: true,
+ // Need to dynamically add the "password" type or LoginManager
+ // will think that the form inputs on the page are part of a login form
+ // and will add fill-login context menu items. The element needs to be
+ // re-created as type=text afterwards since it uses hasBeenTypePassword.
+ async preCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("input_password");
+ input.type = "password";
+ input.clientTop; // force layout flush
+ }
+ );
+ },
+ async postCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("input_password");
+ input.outerHTML = `<input id=\"input_password\">`;
+ input.clientTop; // force layout flush
+ }
+ );
+ },
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function firefox_relay_input() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.firefoxRelay.feature", "enabled"]],
+ });
+
+ await test_contextmenu("#input_username", [
+ "use-relay-mask",
+ true,
+ "---",
+ null,
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ false,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ ]);
+
+ await test_contextmenu(
+ "#input_email",
+ [
+ "use-relay-mask",
+ true,
+ "---",
+ null,
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ null,
+ ],
+ {
+ skipFocusChange: true,
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_tel_email_url_number_input() {
+ todo(
+ false,
+ "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled"
+ );
+ for (let selector of [
+ "#input_email",
+ "#input_url",
+ "#input_tel",
+ "#input_number",
+ ]) {
+ await test_contextmenu(
+ selector,
+ [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ null,
+ ],
+ {
+ skipFocusChange: true,
+ }
+ );
+ }
+});
+
+add_task(
+ async function test_date_time_color_range_month_week_datetimelocal_input() {
+ for (let selector of [
+ "#input_date",
+ "#input_time",
+ "#input_color",
+ "#input_range",
+ "#input_month",
+ "#input_week",
+ "#input_datetime-local",
+ ]) {
+ await test_contextmenu(
+ selector,
+ [
+ ...NAVIGATION_ITEMS,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ ],
+ {
+ skipFocusChange: true,
+ }
+ );
+ }
+ }
+);
+
+add_task(async function test_search_input() {
+ todo(
+ false,
+ "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled"
+ );
+ await test_contextmenu(
+ "#input_search",
+ [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ ],
+ { skipFocusChange: true }
+ );
+});
+
+add_task(async function test_text_input_readonly() {
+ todo(
+ false,
+ "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled"
+ );
+ todo(
+ false,
+ "spell-check should not be enabled for input[readonly]. see bug 1246296"
+ );
+ await test_contextmenu(
+ "#input_readonly",
+ [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ false,
+ "context-copy",
+ false,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "context-selectall",
+ null,
+ ],
+ {
+ skipFocusChange: true,
+ }
+ );
+});
+
+add_task(async function test_cleanup() {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_inspect.js b/browser/base/content/test/contextMenu/browser_contextmenu_inspect.js
new file mode 100644
index 0000000000..94241e9e1f
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_inspect.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that we show the inspect item(s) as appropriate.
+ */
+add_task(async function test_contextmenu_inspect() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["devtools.selfxss.count", 0],
+ ["devtools.everOpened", false],
+ ],
+ });
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ for (let [pref, value, expectation] of [
+ ["devtools.selfxss.count", 10, true],
+ ["devtools.selfxss.count", 0, false],
+ ["devtools.everOpened", false, false],
+ ["devtools.everOpened", true, true],
+ ]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["devtools.selfxss.count", value]],
+ });
+ is(contextMenu.state, "closed", "checking if popup is closed");
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ let promisePopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "body",
+ 2,
+ 2,
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await promisePopupShown;
+ let inspectItem = document.getElementById("context-inspect");
+ ok(
+ !inspectItem.hidden,
+ `Inspect should be shown (pref ${pref} is ${value}).`
+ );
+ let inspectA11y = document.getElementById("context-inspect-a11y");
+ is(
+ inspectA11y.hidden,
+ !expectation,
+ `A11y should be ${
+ expectation ? "visible" : "hidden"
+ } (pref ${pref} is ${value}).`
+ );
+ contextMenu.hidePopup();
+ await promisePopupHidden;
+ }
+ });
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_keyword.js b/browser/base/content/test/contextMenu/browser_contextmenu_keyword.js
new file mode 100644
index 0000000000..2e1253107c
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_keyword.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let contextMenu;
+
+const example_base =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/contextMenu/";
+const MAIN_URL = example_base + "subtst_contextmenu_keyword.html";
+
+add_task(async function test_setup() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, MAIN_URL);
+
+ const chrome_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+ const contextmenu_common = chrome_base + "contextmenu_common.js";
+ /* import-globals-from contextmenu_common.js */
+ Services.scriptloader.loadSubScript(contextmenu_common, this);
+});
+
+add_task(async function test_text_input_spellcheck_noform() {
+ await test_contextmenu(
+ "#input_text_no_form",
+ [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ null, // ignore the enabled/disabled states; there are race conditions
+ // in the edit commands but they're not relevant for what we're testing.
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ "spell-dictionaries",
+ true,
+ [
+ "spell-check-dictionary-en-US",
+ true,
+ "---",
+ null,
+ "spell-add-dictionaries",
+ true,
+ ],
+ null,
+ ],
+ {
+ waitForSpellCheck: true,
+ async preCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("input_text_no_form");
+ input.setAttribute("spellcheck", "true");
+ input.clientTop; // force layout flush
+ }
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_text_input_spellcheck_loginform() {
+ await test_contextmenu(
+ "#login_text",
+ [
+ "manage-saved-logins",
+ true,
+ "---",
+ null,
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ null, // ignore the enabled/disabled states; there are race conditions
+ // in the edit commands but they're not relevant for what we're testing.
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ "spell-dictionaries",
+ true,
+ [
+ "spell-check-dictionary-en-US",
+ true,
+ "---",
+ null,
+ "spell-add-dictionaries",
+ true,
+ ],
+ null,
+ ],
+ {
+ waitForSpellCheck: true,
+ async preCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("login_text");
+ input.setAttribute("spellcheck", "true");
+ input.clientTop; // force layout flush
+ }
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_text_input_spellcheck_searchform() {
+ await test_contextmenu(
+ "#search_text",
+ [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ null, // ignore the enabled/disabled states; there are race conditions
+ // in the edit commands but they're not relevant for what we're testing.
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "context-keywordfield",
+ null,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ "spell-dictionaries",
+ true,
+ [
+ "spell-check-dictionary-en-US",
+ true,
+ "---",
+ null,
+ "spell-add-dictionaries",
+ true,
+ ],
+ null,
+ ],
+ {
+ waitForSpellCheck: true,
+ async preCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("search_text");
+ input.setAttribute("spellcheck", "true");
+ input.clientTop; // force layout flush
+ }
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_cleanup() {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_linkopen.js b/browser/base/content/test/contextMenu/browser_contextmenu_linkopen.js
new file mode 100644
index 0000000000..ac793b8011
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_linkopen.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_LINK = "https://example.com/";
+const RESOURCE_LINK =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "test_contextmenu_links.html";
+
+async function activateContextAndWaitFor(selector, where) {
+ info("Starting test for " + where);
+ let contextMenuItem = "openlink";
+ let openPromise;
+ let closeMethod;
+ switch (where) {
+ case "tab":
+ contextMenuItem += "intab";
+ openPromise = BrowserTestUtils.waitForNewTab(gBrowser, TEST_LINK, false);
+ closeMethod = async tab => BrowserTestUtils.removeTab(tab);
+ break;
+ case "privatewindow":
+ contextMenuItem += "private";
+ openPromise = BrowserTestUtils.waitForNewWindow({ url: TEST_LINK }).then(
+ win => {
+ ok(
+ PrivateBrowsingUtils.isWindowPrivate(win),
+ "Should have opened a private window."
+ );
+ return win;
+ }
+ );
+ closeMethod = async win => BrowserTestUtils.closeWindow(win);
+ break;
+ case "window":
+ // No contextMenuItem suffix for normal new windows;
+ openPromise = BrowserTestUtils.waitForNewWindow({ url: TEST_LINK }).then(
+ win => {
+ ok(
+ !PrivateBrowsingUtils.isWindowPrivate(win),
+ "Should have opened a normal window."
+ );
+ return win;
+ }
+ );
+ closeMethod = async win => BrowserTestUtils.closeWindow(win);
+ break;
+ }
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ is(contextMenu.state, "closed", "checking if popup is closed");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ selector,
+ 0,
+ 0,
+ {
+ type: "contextmenu",
+ button: 2,
+ centered: true,
+ },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+ info("Popup Shown");
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ let domItem = contextMenu.querySelector("#context-" + contextMenuItem);
+ info("Going to click item " + domItem.id);
+ ok(
+ BrowserTestUtils.is_visible(domItem),
+ "DOM context menu item " + where + " should be visible"
+ );
+ ok(
+ !domItem.disabled,
+ "DOM context menu item " + where + " shouldn't be disabled"
+ );
+ contextMenu.activateItem(domItem);
+ await awaitPopupHidden;
+
+ info("Waiting for the link to open");
+ let openedThing = await openPromise;
+ info("Waiting for the opened window/tab to close");
+ await closeMethod(openedThing);
+}
+
+add_task(async function test_select_text_link() {
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ RESOURCE_LINK
+ );
+ for (let elementID of [
+ "test-link",
+ "test-image-link",
+ "svg-with-link",
+ "svg-with-relative-link",
+ ]) {
+ for (let where of ["tab", "window", "privatewindow"]) {
+ await activateContextAndWaitFor("#" + elementID, where);
+ }
+ }
+ BrowserTestUtils.removeTab(testTab);
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.html b/browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.html
new file mode 100644
index 0000000000..ca96fcfaa0
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8" />
+</head>
+
+<body onload="add_content()">
+ <p>This example creates a typed array containing the ASCII codes for the space character through the letter Z, then
+ converts it to an object URL.A link to open that object URL is created. Click the link to see the decoded object
+ URL.</p>
+ <br />
+ <br />
+ <a id='blob-url-link'>Open the array URL</a>
+ <br />
+ <br />
+ <a id='blob-url-referrer-link'>Open the URL that fetches the URL above</a>
+
+ <script>
+ function typedArrayToURL(typedArray, mimeType) {
+ return URL.createObjectURL(new Blob([typedArray.buffer], { type: mimeType }))
+ }
+
+ function add_content() {
+ const bytes = new Uint8Array(59);
+
+ for (let i = 0;i < 59;i++) {
+ bytes[i] = 32 + i;
+ }
+
+ const url = typedArrayToURL(bytes, 'text/plain');
+ document.getElementById('blob-url-link').href = url;
+
+ const ref_url = URL.createObjectURL(new Blob([`
+ <body>
+ <script>
+ fetch("${url}", {headers: {'Content-Type': 'text/plain'}})
+ .then((response) => {
+ response.text().then((textData) => {
+ var pre = document.createElement("pre");
+ pre.textContent = textData.trim();
+ document.body.insertBefore(pre, document.body.firstChild);
+ });
+ });
+ <\/script>
+ <\/body>
+ `], { type: 'text/html' }));
+
+ document.getElementById('blob-url-referrer-link').href = ref_url;
+ };
+
+ </script>
+
+</body>
+
+</html>
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.js b/browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.js
new file mode 100644
index 0000000000..cbf1b27590
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_loadblobinnewtab.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RESOURCE_LINK =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "browser_contextmenu_loadblobinnewtab.html";
+
+const blobDataAsString = `!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
+
+// Helper method to right click on the provided link (selector as id),
+// open in new tab and return the content of the first <pre> under the
+// <body> of the new tab's document.
+async function rightClickOpenInNewTabAndReturnContent(selector) {
+ const loaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ RESOURCE_LINK
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, RESOURCE_LINK);
+ await loaded;
+
+ const generatedBlobURL = await ContentTask.spawn(
+ gBrowser.selectedBrowser,
+ { selector },
+ async args => {
+ return content.document.getElementById(args.selector).href;
+ }
+ );
+
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+ is(contextMenu.state, "closed", "checking if context menu is closed");
+
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + selector,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ const openPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ generatedBlobURL,
+ false
+ );
+
+ document.getElementById("context-openlinkintab").doCommand();
+
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+
+ let openTab = await openPromise;
+
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
+
+ let blobDataFromContent = await ContentTask.spawn(
+ gBrowser.selectedBrowser,
+ null,
+ async function () {
+ while (!content.document.querySelector("body pre")) {
+ await new Promise(resolve =>
+ content.setTimeout(() => {
+ resolve();
+ }, 100)
+ );
+ }
+ return content.document.body.firstElementChild.innerText.trim();
+ }
+ );
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(openTab);
+ await BrowserTestUtils.removeTab(openTab);
+ await tabClosed;
+
+ return blobDataFromContent;
+}
+
+// Helper method to open selected link in new tab (selector as id),
+// and return the content of the first <pre> under the <body> of
+// the new tab's document.
+async function openInNewTabAndReturnContent(selector) {
+ const loaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ RESOURCE_LINK
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, RESOURCE_LINK);
+ await loaded;
+
+ const generatedBlobURL = await ContentTask.spawn(
+ gBrowser.selectedBrowser,
+ { selector },
+ async args => {
+ return content.document.getElementById(args.selector).href;
+ }
+ );
+
+ let openTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ generatedBlobURL
+ );
+
+ let blobDataFromContent = await ContentTask.spawn(
+ gBrowser.selectedBrowser,
+ null,
+ async function () {
+ while (!content.document.querySelector("body pre")) {
+ await new Promise(resolve =>
+ content.setTimeout(() => {
+ resolve();
+ }, 100)
+ );
+ }
+ return content.document.body.firstElementChild.innerText.trim();
+ }
+ );
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(openTab);
+ await BrowserTestUtils.removeTab(openTab);
+ await tabClosed;
+
+ return blobDataFromContent;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.bloburl_per_agent_cluster", false]],
+ });
+});
+
+add_task(async function test_rightclick_open_bloburl_in_new_tab() {
+ let blobDataFromLoadedPage = await rightClickOpenInNewTabAndReturnContent(
+ "blob-url-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
+
+add_task(async function test_rightclick_open_bloburl_referrer_in_new_tab() {
+ let blobDataFromLoadedPage = await rightClickOpenInNewTabAndReturnContent(
+ "blob-url-referrer-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
+
+add_task(async function test_open_bloburl_in_new_tab() {
+ let blobDataFromLoadedPage = await openInNewTabAndReturnContent(
+ "blob-url-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
+
+add_task(async function test_open_bloburl_referrer_in_new_tab() {
+ let blobDataFromLoadedPage = await openInNewTabAndReturnContent(
+ "blob-url-referrer-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js b/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js
new file mode 100644
index 0000000000..5064d9a316
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+function mockPromptService() {
+ let { prompt } = Services;
+ let promptService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ alert: () => {},
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+ return promptService;
+}
+
+add_task(async function test_save_link_blocked_by_extension() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "cancel@test" } },
+ name: "Cancel Test",
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ return { cancel: details.url === "http://example.com/" };
+ },
+ { urls: ["*://*/*"] },
+ ["blocking"]
+ );
+ },
+ });
+ await ext.startup();
+
+ await BrowserTestUtils.withNewTab(
+ `data:text/html;charset=utf-8,<a href="http://example.com">Download</a>`,
+ async browser => {
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a[href]",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShown;
+
+ await new Promise(resolve => {
+ let promptService = mockPromptService();
+ promptService.alert = (window, title, msg) => {
+ is(
+ msg,
+ "The download cannot be saved because it is blocked by Cancel Test.",
+ "prompt should be shown"
+ );
+ setTimeout(resolve, 0);
+ };
+
+ MockFilePicker.showCallback = function (fp) {
+ ok(false, "filepicker should never been shown");
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ menu.activateItem(menu.querySelector("#context-savelink"));
+ });
+ }
+ );
+
+ await ext.unload();
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js b/browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js
new file mode 100644
index 0000000000..8175b93052
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_share_macosx.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const TEST_URL = BASE + "browser_contextmenu_shareurl.html";
+
+let mockShareData = [
+ {
+ name: "Test",
+ menuItemTitle: "Sharing Service Test",
+ image:
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKE" +
+ "lEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==",
+ },
+];
+
+// Setup spies for observing function calls from MacSharingService
+let shareUrlSpy = sinon.spy();
+let openSharingPreferencesSpy = sinon.spy();
+let getSharingProvidersSpy = sinon.spy();
+
+let stub = sinon.stub(gBrowser, "MacSharingService").get(() => {
+ return {
+ getSharingProviders(url) {
+ getSharingProvidersSpy(url);
+ return mockShareData;
+ },
+ shareUrl(name, url, title) {
+ shareUrlSpy(name, url, title);
+ },
+ openSharingPreferences() {
+ openSharingPreferencesSpy();
+ },
+ };
+});
+
+registerCleanupFunction(async function () {
+ stub.restore();
+});
+
+/**
+ * Test the "Share" item menus in the tab contextmenu on MacOSX.
+ */
+add_task(async function test_contextmenu_share_macosx() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async () => {
+ let contextMenu = await openTabContextMenu(gBrowser.selectedTab);
+ await BrowserTestUtils.waitForMutationCondition(
+ contextMenu,
+ { childList: true },
+ () => contextMenu.querySelector(".share-tab-url-item")
+ );
+ ok(true, "Got Share item");
+
+ await openMenuPopup(contextMenu);
+ ok(getSharingProvidersSpy.calledOnce, "getSharingProviders called");
+
+ info(
+ "Check we have a service and one extra menu item for the More... button"
+ );
+ let popup = contextMenu.querySelector(".share-tab-url-item").menupopup;
+ let items = popup.querySelectorAll("menuitem");
+ is(items.length, 2, "There should be 2 sharing services.");
+
+ info("Click on the sharing service");
+ let menuPopupClosedPromised = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "hidden"
+ );
+ let shareButton = items[0];
+ is(
+ shareButton.label,
+ mockShareData[0].menuItemTitle,
+ "Share button's label should match the service's menu item title. "
+ );
+ is(
+ shareButton.getAttribute("share-name"),
+ mockShareData[0].name,
+ "Share button's share-name value should match the service's name. "
+ );
+
+ popup.activateItem(shareButton);
+ await menuPopupClosedPromised;
+
+ ok(shareUrlSpy.calledOnce, "shareUrl called");
+
+ info("Check the correct data was shared.");
+ let [name, url, title] = shareUrlSpy.getCall(0).args;
+ is(name, mockShareData[0].name, "Shared correct service name");
+ is(url, TEST_URL, "Shared correct URL");
+ is(title, "Sharing URL", "Shared the correct title.");
+
+ info("Test the More... button");
+ contextMenu = await openTabContextMenu(gBrowser.selectedTab);
+ await openMenuPopup(contextMenu);
+ // Since the tab context menu was collapsed previously, the popup needs to get the
+ // providers again.
+ ok(getSharingProvidersSpy.calledTwice, "getSharingProviders called again");
+ popup = contextMenu.querySelector(".share-tab-url-item").menupopup;
+ items = popup.querySelectorAll("menuitem");
+ is(items.length, 2, "There should be 2 sharing services.");
+
+ info("Click on the More Button");
+ let moreButton = items[1];
+ menuPopupClosedPromised = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "hidden"
+ );
+ popup.activateItem(moreButton);
+ await menuPopupClosedPromised;
+ ok(openSharingPreferencesSpy.calledOnce, "openSharingPreferences called");
+ });
+});
+
+/**
+ * Helper for opening the toolbar context menu.
+ */
+async function openTabContextMenu(tab) {
+ info("Opening tab context menu");
+ let contextMenu = document.getElementById("tabContextMenu");
+ let openTabContextMenuPromise = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "shown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu" });
+ await openTabContextMenuPromise;
+ return contextMenu;
+}
+
+async function openMenuPopup(contextMenu) {
+ info("Opening Share menu popup.");
+ let shareItem = contextMenu.querySelector(".share-tab-url-item");
+ shareItem.openMenu(true);
+ await BrowserTestUtils.waitForPopupEvent(shareItem.menupopup, "shown");
+}
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js b/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js
new file mode 100644
index 0000000000..716da584c5
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const TEST_URL = BASE + "browser_contextmenu_shareurl.html";
+
+// Setup spies for observing function calls from MacSharingService
+let shareUrlSpy = sinon.spy();
+
+let stub = sinon.stub(gBrowser.ownerGlobal, "WindowsUIUtils").get(() => {
+ return {
+ shareUrl(url, title) {
+ shareUrlSpy(url, title);
+ },
+ };
+});
+
+registerCleanupFunction(async function () {
+ stub.restore();
+});
+
+/**
+ * Test the "Share" item in the tab contextmenu on Windows.
+ */
+add_task(async function test_contextmenu_share_win() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async () => {
+ await openTabContextMenu(gBrowser.selectedTab);
+
+ let contextMenu = document.getElementById("tabContextMenu");
+ let contextMenuClosedPromise = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "hidden"
+ );
+ let itemCreated = contextMenu.querySelector(".share-tab-url-item");
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.4")) {
+ Assert.ok(!itemCreated, "We only expose share on windows 10 and above");
+ contextMenu.hidePopup();
+ await contextMenuClosedPromise;
+ return;
+ }
+
+ ok(itemCreated, "Got Share item on Windows 10");
+
+ info("Test the correct URL is shared when Share is selected.");
+ EventUtils.synthesizeMouseAtCenter(itemCreated, {});
+ await contextMenuClosedPromise;
+
+ ok(shareUrlSpy.calledOnce, "shareUrl called");
+ let [url, title] = shareUrlSpy.getCall(0).args;
+ is(url, TEST_URL, "Shared correct URL");
+ is(title, "Sharing URL", "Shared correct URL");
+ });
+});
+
+/**
+ * Helper for opening the toolbar context menu.
+ */
+async function openTabContextMenu(tab) {
+ info("Opening tab context menu");
+ let contextMenu = document.getElementById("tabContextMenu");
+ let openTabContextMenuPromise = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "shown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu" });
+ await openTabContextMenuPromise;
+}
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_shareurl.html b/browser/base/content/test/contextMenu/browser_contextmenu_shareurl.html
new file mode 100644
index 0000000000..c7fb193972
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_shareurl.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Sharing URL</title>
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_spellcheck.js b/browser/base/content/test/contextMenu/browser_contextmenu_spellcheck.js
new file mode 100644
index 0000000000..6f556a58dd
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_spellcheck.js
@@ -0,0 +1,334 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let contextMenu;
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const example_base =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/contextMenu/";
+const MAIN_URL = example_base + "subtst_contextmenu_input.html";
+
+add_task(async function test_setup() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, MAIN_URL);
+
+ const chrome_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+ const contextmenu_common = chrome_base + "contextmenu_common.js";
+ /* import-globals-from contextmenu_common.js */
+ Services.scriptloader.loadSubScript(contextmenu_common, this);
+});
+
+add_task(async function test_text_input_spellcheck() {
+ await test_contextmenu(
+ "#input_spellcheck_no_value",
+ [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ null, // ignore the enabled/disabled states; there are race conditions
+ // in the edit commands but they're not relevant for what we're testing.
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ "spell-dictionaries",
+ true,
+ [
+ "spell-check-dictionary-en-US",
+ true,
+ "---",
+ null,
+ "spell-add-dictionaries",
+ true,
+ ],
+ null,
+ ],
+ {
+ waitForSpellCheck: true,
+ async preCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let input = doc.getElementById("input_spellcheck_no_value");
+ input.setAttribute("spellcheck", "true");
+ input.clientTop; // force layout flush
+ }
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_text_input_spellcheckwrong() {
+ await test_contextmenu(
+ "#input_spellcheck_incorrect",
+ [
+ "*prodigality",
+ true, // spelling suggestion
+ "spell-add-to-dictionary",
+ true,
+ "---",
+ null,
+ "context-undo",
+ null,
+ "context-redo",
+ null,
+ "---",
+ null,
+ "context-cut",
+ null,
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ "spell-dictionaries",
+ true,
+ [
+ "spell-check-dictionary-en-US",
+ true,
+ "---",
+ null,
+ "spell-add-dictionaries",
+ true,
+ ],
+ null,
+ ],
+ { waitForSpellCheck: true }
+ );
+});
+
+const kCorrectItems = [
+ "context-undo",
+ false,
+ "context-redo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ null,
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ "spell-dictionaries",
+ true,
+ [
+ "spell-check-dictionary-en-US",
+ true,
+ "---",
+ null,
+ "spell-add-dictionaries",
+ true,
+ ],
+ null,
+];
+
+add_task(async function test_text_input_spellcheckcorrect() {
+ await test_contextmenu("#input_spellcheck_correct", kCorrectItems, {
+ waitForSpellCheck: true,
+ });
+});
+
+add_task(async function test_text_input_spellcheck_deadactor() {
+ await test_contextmenu("#input_spellcheck_correct", kCorrectItems, {
+ waitForSpellCheck: true,
+ keepMenuOpen: true,
+ });
+ let wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
+
+ // Now the menu is open, and spellcheck is running, switch to another tab and
+ // close the original:
+ let tab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.org");
+ BrowserTestUtils.removeTab(tab);
+ // Ensure we've invalidated the actor
+ await TestUtils.waitForCondition(
+ () => wgp.isClosed,
+ "Waiting for actor to be dead after tab closes"
+ );
+ contextMenu.hidePopup();
+
+ // Now go back to the input testcase:
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, MAIN_URL);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ MAIN_URL
+ );
+
+ // Check the menu still looks the same, keep it open again:
+ await test_contextmenu("#input_spellcheck_correct", kCorrectItems, {
+ waitForSpellCheck: true,
+ keepMenuOpen: true,
+ });
+
+ // Now navigate the tab, after ensuring there's an unload listener, so
+ // we don't end up in bfcache:
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.body.setAttribute("onunload", "");
+ });
+ wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
+
+ const NEW_URL = MAIN_URL.replace(".com", ".org");
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, NEW_URL);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ NEW_URL
+ );
+ // Ensure we've invalidated the actor
+ await TestUtils.waitForCondition(
+ () => wgp.isClosed,
+ "Waiting for actor to be dead after onunload"
+ );
+ contextMenu.hidePopup();
+
+ // Check the menu *still* looks the same (and keep it open again):
+ await test_contextmenu("#input_spellcheck_correct", kCorrectItems, {
+ waitForSpellCheck: true,
+ keepMenuOpen: true,
+ });
+
+ // Check what happens if the actor stays alive by loading the same page
+ // again; now the context menu stuff should be destroyed by the menu
+ // hiding, nothing else.
+ wgp = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, NEW_URL);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ NEW_URL
+ );
+ contextMenu.hidePopup();
+
+ // Check the menu still looks the same:
+ await test_contextmenu("#input_spellcheck_correct", kCorrectItems, {
+ waitForSpellCheck: true,
+ });
+ // And test it a last time without any navigation:
+ await test_contextmenu("#input_spellcheck_correct", kCorrectItems, {
+ waitForSpellCheck: true,
+ });
+});
+
+add_task(async function test_text_input_spellcheck_multilingual() {
+ if (AppConstants.platform == "macosx") {
+ todo(
+ false,
+ "Need macOS support for closemenu attributes in order to " +
+ "stop the spellcheck menu closing, see bug 1796007."
+ );
+ return;
+ }
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => sandbox.restore());
+
+ // We need to mock InlineSpellCheckerUI.mRemote's properties, but
+ // InlineSpellCheckerUI.mRemote won't exist until we initialize the context
+ // menu, so do that and then manually reinit the spellcheck bits so
+ // we control them:
+ await test_contextmenu("#input_spellcheck_correct", kCorrectItems, {
+ waitForSpellCheck: true,
+ keepMenuOpen: true,
+ });
+ sandbox
+ .stub(InlineSpellCheckerUI.mRemote, "dictionaryList")
+ .get(() => ["en-US", "nl-NL"]);
+ let setterSpy = sandbox.spy();
+ sandbox
+ .stub(InlineSpellCheckerUI.mRemote, "currentDictionaries")
+ .get(() => ["en-US"])
+ .set(setterSpy);
+ // Re-init the spellcheck items:
+ InlineSpellCheckerUI.clearDictionaryListFromMenu();
+ gContextMenu.initSpellingItems();
+
+ let dictionaryMenu = document.getElementById("spell-dictionaries-menu");
+ let menuOpen = BrowserTestUtils.waitForPopupEvent(dictionaryMenu, "shown");
+ dictionaryMenu.parentNode.openMenu(true);
+ await menuOpen;
+ checkMenu(dictionaryMenu, [
+ "spell-check-dictionary-nl-NL",
+ true,
+ "spell-check-dictionary-en-US",
+ true,
+ "---",
+ null,
+ "spell-add-dictionaries",
+ true,
+ ]);
+ is(
+ dictionaryMenu.children.length,
+ 4,
+ "Should have 2 dictionaries, a separator and 'add more dictionaries' item in the menu."
+ );
+
+ let dictionaryEventPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "spellcheck-changed"
+ );
+ dictionaryMenu.activateItem(
+ dictionaryMenu.querySelector("[data-locale-code*=nl]")
+ );
+ let event = await dictionaryEventPromise;
+ Assert.deepEqual(
+ event.detail?.dictionaries,
+ ["en-US", "nl-NL"],
+ "Should have sent right dictionaries with event."
+ );
+ ok(setterSpy.called, "Should have set currentDictionaries");
+ Assert.deepEqual(
+ setterSpy.firstCall?.args,
+ [["en-US", "nl-NL"]],
+ "Should have called setter with single argument array of 2 dictionaries."
+ );
+ // Allow for the menu to potentially close:
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+ // Check it hasn't:
+ is(
+ dictionaryMenu.closest("menupopup").state,
+ "open",
+ "Main menu should still be open."
+ );
+ contextMenu.hidePopup();
+});
+
+add_task(async function test_cleanup() {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_touch.js b/browser/base/content/test/contextMenu/browser_contextmenu_touch.js
new file mode 100644
index 0000000000..2f4e5a79c6
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_touch.js
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 that context menus are in touchmode
+ * when opened through a touch event (long tap). */
+
+async function openAndCheckContextMenu(contextMenu, target) {
+ is(contextMenu.state, "closed", "Context menu is initally closed.");
+
+ let popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeNativeTapAtCenter(target, true);
+ await popupshown;
+
+ is(contextMenu.state, "open", "Context menu is open.");
+ is(
+ contextMenu.getAttribute("touchmode"),
+ "true",
+ "Context menu is in touchmode."
+ );
+
+ contextMenu.hidePopup();
+
+ popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(target, { type: "contextmenu" });
+ await popupshown;
+
+ is(contextMenu.state, "open", "Context menu is open.");
+ ok(
+ !contextMenu.hasAttribute("touchmode"),
+ "Context menu is not in touchmode."
+ );
+
+ contextMenu.hidePopup();
+}
+
+// Ensure that we can run touch events properly for windows [10]
+add_setup(async function () {
+ let isWindows = AppConstants.isPlatformAndVersionAtLeast("win", "10.0");
+ await SpecialPowers.pushPrefEnv({
+ set: [["apz.test.fails_with_native_injection", isWindows]],
+ });
+});
+
+// Test the content area context menu.
+add_task(async function test_contentarea_contextmenu_touch() {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ await openAndCheckContextMenu(contextMenu, browser);
+ });
+});
+
+// Test the back and forward buttons.
+add_task(async function test_back_forward_button_contextmenu_touch() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab(
+ "http://example.com",
+ async function (browser) {
+ let contextMenu = document.getElementById("backForwardMenu");
+
+ let backbutton = document.getElementById("back-button");
+ let notDisabled = TestUtils.waitForCondition(
+ () => !backbutton.hasAttribute("disabled")
+ );
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(browser, "http://example.org");
+ await notDisabled;
+ await openAndCheckContextMenu(contextMenu, backbutton);
+
+ let forwardbutton = document.getElementById("forward-button");
+ notDisabled = TestUtils.waitForCondition(
+ () => !forwardbutton.hasAttribute("disabled")
+ );
+ backbutton.click();
+ await notDisabled;
+ await openAndCheckContextMenu(contextMenu, forwardbutton);
+ }
+ );
+});
+
+// Test the toolbar context menu.
+add_task(async function test_toolbar_contextmenu_touch() {
+ let toolbarContextMenu = document.getElementById("toolbar-context-menu");
+ let target = document.getElementById("PanelUI-menu-button");
+ await openAndCheckContextMenu(toolbarContextMenu, target);
+});
+
+// Test the urlbar input context menu.
+add_task(async function test_urlbar_contextmenu_touch() {
+ let urlbar = document.getElementById("urlbar");
+ let textBox = urlbar.querySelector("moz-input-box");
+ let menu = textBox.menupopup;
+ await openAndCheckContextMenu(menu, textBox);
+});
diff --git a/browser/base/content/test/contextMenu/browser_copy_image_link.js b/browser/base/content/test/contextMenu/browser_copy_image_link.js
new file mode 100644
index 0000000000..4853006a61
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_copy_image_link.js
@@ -0,0 +1,40 @@
+/**
+ * Testcase for bug 1719203
+ * <https://bugzilla.mozilla.org/show_bug.cgi?id=1719203>
+ *
+ * Load firebird.png, redirect it to doggy.png, and verify that "Copy Image
+ * Link" copies firebird.png.
+ */
+
+add_task(async function () {
+ // This URL will redirect to doggy.png.
+ const URL_FIREBIRD =
+ "http://mochi.test:8888/browser/browser/base/content/test/contextMenu/firebird.png";
+
+ await BrowserTestUtils.withNewTab(URL_FIREBIRD, async function (browser) {
+ // Click image to show context menu.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "img",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShownPromise;
+
+ await SimpleTest.promiseClipboardChange(URL_FIREBIRD, () => {
+ document.getElementById("context-copyimage").doCommand();
+ });
+
+ // Close context menu.
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+ });
+});
diff --git a/browser/base/content/test/contextMenu/browser_strip_on_share_link.js b/browser/base/content/test/contextMenu/browser_strip_on_share_link.js
new file mode 100644
index 0000000000..ba3fd33caa
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_strip_on_share_link.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let listService;
+
+let url =
+ "https://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.strip_list", "stripParam"]],
+ });
+
+ // 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();
+});
+
+/*
+ Tests the strip-on-share feature for in-content links
+ */
+
+// Tests that the link url is properly stripped
+add_task(async function testStrip() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.strip_on_share.enabled", true]],
+ });
+ let strippedURI = "https://www.example.com/?otherParam=1234";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ // Prepare a link
+ await SpecialPowers.spawn(browser, [], async function () {
+ let link = content.document.createElement("a");
+ link.href = "https://www.example.com/?stripParam=1234&otherParam=1234";
+ link.textContent = "link with query param";
+ link.id = "link";
+ content.document.body.appendChild(link);
+ });
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ // Open the context menu
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await awaitPopupShown;
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ let stripOnShare = contextMenu.querySelector("#context-stripOnShareLink");
+ Assert.ok(
+ BrowserTestUtils.is_visible(stripOnShare),
+ "Menu item is visible"
+ );
+
+ // Make sure the stripped link will be copied to the clipboard
+ await SimpleTest.promiseClipboardChange(strippedURI, () => {
+ contextMenu.activateItem(stripOnShare);
+ });
+ await awaitPopupHidden;
+ });
+});
+
+// Tests that the menu item does not show if the pref is disabled
+add_task(async function testPrefDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.strip_on_share.enabled", false]],
+ });
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ // Prepare a link
+ await SpecialPowers.spawn(browser, [], async function () {
+ let link = content.document.createElement("a");
+ link.href = "https://www.example.com/?stripParam=1234&otherParam=1234";
+ link.textContent = "link with query param";
+ link.id = "link";
+ content.document.body.appendChild(link);
+ });
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ // Open the context menu
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await awaitPopupShown;
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ let stripOnShare = contextMenu.querySelector("#context-stripOnShareLink");
+ Assert.ok(
+ !BrowserTestUtils.is_visible(stripOnShare),
+ "Menu item is not visible"
+ );
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+ });
+});
+
+// Tests that the menu item does not show if there is nothing to strip
+add_task(async function testUnknownQueryParam() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.strip_on_share.enabled", true]],
+ });
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ // Prepare a link
+ await SpecialPowers.spawn(browser, [], async function () {
+ let link = content.document.createElement("a");
+ link.href = "https://www.example.com/?otherParam=1234";
+ link.textContent = "link with unknown query param";
+ link.id = "link";
+ content.document.body.appendChild(link);
+ });
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ // open the context menu
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await awaitPopupShown;
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ let stripOnShare = contextMenu.querySelector("#context-stripOnShareLink");
+ Assert.ok(
+ !BrowserTestUtils.is_visible(stripOnShare),
+ "Menu item is not visible"
+ );
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+ });
+});
diff --git a/browser/base/content/test/contextMenu/browser_utilityOverlay.js b/browser/base/content/test/contextMenu/browser_utilityOverlay.js
new file mode 100644
index 0000000000..2a3b881c92
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_utilityOverlay.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_eventMatchesKey() {
+ let eventMatchResult;
+ let key;
+ let checkEvent = function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ eventMatchResult = eventMatchesKey(e, key);
+ };
+ document.addEventListener("keypress", checkEvent);
+
+ try {
+ key = document.createXULElement("key");
+ let keyset = document.getElementById("mainKeyset");
+ key.setAttribute("key", "t");
+ key.setAttribute("modifiers", "accel");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("t", { accelKey: true });
+ is(eventMatchResult, true, "eventMatchesKey: one modifier");
+ keyset.removeChild(key);
+
+ key = document.createXULElement("key");
+ key.setAttribute("key", "g");
+ key.setAttribute("modifiers", "accel,shift");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("g", { accelKey: true, shiftKey: true });
+ is(eventMatchResult, true, "eventMatchesKey: combination modifiers");
+ keyset.removeChild(key);
+
+ key = document.createXULElement("key");
+ key.setAttribute("key", "w");
+ key.setAttribute("modifiers", "accel");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ is(eventMatchResult, false, "eventMatchesKey: mismatch keys");
+ keyset.removeChild(key);
+
+ key = document.createXULElement("key");
+ key.setAttribute("keycode", "VK_DELETE");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("VK_DELETE", { accelKey: true });
+ is(eventMatchResult, false, "eventMatchesKey: mismatch modifiers");
+ keyset.removeChild(key);
+ } finally {
+ // Make sure to remove the event listener so future tests don't
+ // fail when they simulate key presses.
+ document.removeEventListener("keypress", checkEvent);
+ }
+});
+
+add_task(async function test_getTargetWindow() {
+ is(URILoadingHelper.getTargetWindow(window), window, "got top window");
+});
+
+add_task(async function test_openUILink() {
+ const kURL = "https://example.org/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ kURL
+ );
+
+ openUILink(kURL, null, {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
+ }); // defaults to "current"
+
+ await loadPromise;
+
+ is(tab.linkedBrowser.currentURI.spec, kURL, "example.org loaded");
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/contextMenu/browser_utilityOverlayPrincipal.js b/browser/base/content/test/contextMenu/browser_utilityOverlayPrincipal.js
new file mode 100644
index 0000000000..5b8252b973
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_utilityOverlayPrincipal.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 gTests = [test_openUILink_checkPrincipal];
+
+function test() {
+ waitForExplicitFinish();
+ executeSoon(runNextTest);
+}
+
+function runNextTest() {
+ if (gTests.length) {
+ let testFun = gTests.shift();
+ info("Running " + testFun.name);
+ testFun();
+ } else {
+ finish();
+ }
+}
+
+function test_openUILink_checkPrincipal() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ )); // remote tab
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(async function () {
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/",
+ "example.com loaded"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let channel = content.docShell.currentDocumentChannel;
+
+ const loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(loadingPrincipal, null, "sanity: correct loadingPrincipal");
+ const triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "sanity: correct triggeringPrincipal"
+ );
+ const principalToInherit = channel.loadInfo.principalToInherit;
+ ok(
+ principalToInherit.isNullPrincipal,
+ "sanity: correct principalToInherit"
+ );
+ ok(
+ content.document.nodePrincipal.isContentPrincipal,
+ "sanity: correct doc.nodePrincipal"
+ );
+ is(
+ content.document.nodePrincipal.asciiSpec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/",
+ "sanity: correct doc.nodePrincipal URL"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+ runNextTest();
+ });
+
+ // Ensure we get the correct default of "allowInheritPrincipal: false" from openUILink
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ openUILink("http://example.com", null, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal({}),
+ }); // defaults to "current"
+}
diff --git a/browser/base/content/test/contextMenu/browser_view_image.js b/browser/base/content/test/contextMenu/browser_view_image.js
new file mode 100644
index 0000000000..485fdf3fb2
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_view_image.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const chrome_base = getRootDirectory(gTestPath);
+
+/* import-globals-from contextmenu_common.js */
+Services.scriptloader.loadSubScript(
+ chrome_base + "contextmenu_common.js",
+ this
+);
+const http_base = chrome_base.replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+async function test_view_image_works({ page, selector, urlChecker }) {
+ let mainURL = http_base + page;
+ let accel = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
+ let tests = {
+ tab: {
+ modifiers: { [accel]: true },
+ async loadedPromise() {
+ return BrowserTestUtils.waitForNewTab(gBrowser, urlChecker, true).then(
+ t => t.linkedBrowser
+ );
+ },
+ cleanup(browser) {
+ BrowserTestUtils.removeTab(gBrowser.getTabForBrowser(browser));
+ },
+ },
+ window: {
+ modifiers: { shiftKey: true },
+ async loadedPromise() {
+ // Unfortunately we can't predict the URL so can't just pass that to waitForNewWindow
+ let w = await BrowserTestUtils.waitForNewWindow();
+ let browser = w.gBrowser.selectedBrowser;
+ let getCx = () => browser.browsingContext;
+ await TestUtils.waitForCondition(
+ () =>
+ getCx() && urlChecker(getCx().currentWindowGlobal.documentURI.spec)
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ return ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "complete"
+ );
+ });
+ return browser;
+ },
+ async cleanup(browser) {
+ return BrowserTestUtils.closeWindow(browser.ownerGlobal);
+ },
+ },
+ tab_default: {
+ modifiers: {},
+ async loadedPromise() {
+ return BrowserTestUtils.waitForNewTab(gBrowser, urlChecker, true).then(
+ t => {
+ is(t.selected, false, "Tab should not be selected.");
+ return t.linkedBrowser;
+ }
+ );
+ },
+ cleanup(browser) {
+ is(gBrowser.tabs.length, 3, "number of tabs");
+ BrowserTestUtils.removeTab(gBrowser.getTabForBrowser(browser));
+ },
+ },
+ tab_default_flip_bg_pref: {
+ prefs: [["browser.tabs.loadInBackground", false]],
+ modifiers: {},
+ async loadedPromise() {
+ return BrowserTestUtils.waitForNewTab(gBrowser, urlChecker, true).then(
+ t => {
+ is(t.selected, true, "Tab should be selected with pref flipped.");
+ return t.linkedBrowser;
+ }
+ );
+ },
+ cleanup(browser) {
+ is(gBrowser.tabs.length, 3, "number of tabs");
+ BrowserTestUtils.removeTab(gBrowser.getTabForBrowser(browser));
+ },
+ },
+ };
+ await BrowserTestUtils.withNewTab(mainURL, async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ return ContentTaskUtils.waitForCondition(
+ () => !content.document.documentElement.classList.contains("wait")
+ );
+ });
+ for (let [testLabel, test] of Object.entries(tests)) {
+ if (test.prefs) {
+ await SpecialPowers.pushPrefEnv({ set: test.prefs });
+ }
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ is(
+ contextMenu.state,
+ "closed",
+ `${testLabel} - checking if popup is closed`
+ );
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ selector,
+ 2,
+ 2,
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await promisePopupShown;
+ info(`${testLabel} - Popup Shown`);
+ let promisePopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ let browserPromise = test.loadedPromise();
+ contextMenu.activateItem(
+ document.getElementById("context-viewimage"),
+ test.modifiers
+ );
+ await promisePopupHidden;
+
+ let newBrowser = await browserPromise;
+ let { documentURI } = newBrowser.browsingContext.currentWindowGlobal;
+ if (documentURI.spec.startsWith("data:image/svg")) {
+ await SpecialPowers.spawn(newBrowser, [testLabel], msgPrefix => {
+ let svgEl = content.document.querySelector("svg");
+ ok(svgEl, `${msgPrefix} - should have loaded SVG.`);
+ is(svgEl.height.baseVal.value, 500, `${msgPrefix} - SVG has height`);
+ is(svgEl.width.baseVal.value, 500, `${msgPrefix} - SVG has height`);
+ });
+ } else {
+ await SpecialPowers.spawn(newBrowser, [testLabel], msgPrefix => {
+ let img = content.document.querySelector("img");
+ ok(
+ img instanceof Ci.nsIImageLoadingContent,
+ `${msgPrefix} - Image should have loaded content.`
+ );
+ const request = img.getRequest(
+ Ci.nsIImageLoadingContent.CURRENT_REQUEST
+ );
+ ok(
+ request.imageStatus & request.STATUS_LOAD_COMPLETE,
+ `${msgPrefix} - Should have loaded image.`
+ );
+ });
+ }
+ await test.cleanup(newBrowser);
+ if (test.prefs) {
+ await SpecialPowers.popPrefEnv();
+ }
+ }
+ });
+}
+
+/**
+ * Verify that the 'view image' context menu in a new tab for a canvas works,
+ * when opened in a new tab, a new window, or in the same tab.
+ */
+add_task(async function test_view_image_canvas_works() {
+ await test_view_image_works({
+ page: "subtst_contextmenu.html",
+ selector: "#test-canvas",
+ urlChecker: url => url.startsWith("blob:"),
+ });
+});
+
+/**
+ * Test for https://bugzilla.mozilla.org/show_bug.cgi?id=1625786
+ */
+add_task(async function test_view_image_revoked_cached_blob() {
+ await test_view_image_works({
+ page: "test_view_image_revoked_cached_blob.html",
+ selector: "#second",
+ urlChecker: url => url.startsWith("blob:"),
+ });
+});
+
+/**
+ * Test for https://bugzilla.mozilla.org/show_bug.cgi?id=1738190
+ * Inline SVG data URIs as a background image should also open.
+ */
+add_task(async function test_view_image_inline_svg_bgimage() {
+ await SpecialPowers.pushPrefEnv({
+ // This is the default but we turn it off for unit tests.
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
+ });
+ await test_view_image_works({
+ page: "test_view_image_inline_svg.html",
+ selector: "body",
+ urlChecker: url => url.startsWith("data:"),
+ });
+});
diff --git a/browser/base/content/test/contextMenu/bug1798178.sjs b/browser/base/content/test/contextMenu/bug1798178.sjs
new file mode 100644
index 0000000000..790dc2bee5
--- /dev/null
+++ b/browser/base/content/test/contextMenu/bug1798178.sjs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("X-Content-Type-Options", "nosniff");
+ response.write("Hello");
+}
diff --git a/browser/base/content/test/contextMenu/contextmenu_common.js b/browser/base/content/test/contextMenu/contextmenu_common.js
new file mode 100644
index 0000000000..ac61aa2a3a
--- /dev/null
+++ b/browser/base/content/test/contextMenu/contextmenu_common.js
@@ -0,0 +1,437 @@
+// This file expects contextMenu to be defined in the scope it is loaded into.
+/* global contextMenu:true */
+
+var lastElement;
+const FRAME_OS_PID = "context-frameOsPid";
+
+function openContextMenuFor(element, shiftkey, waitForSpellCheck) {
+ // Context menu should be closed before we open it again.
+ is(
+ SpecialPowers.wrap(contextMenu).state,
+ "closed",
+ "checking if popup is closed"
+ );
+
+ if (lastElement) {
+ lastElement.blur();
+ }
+ element.focus();
+
+ // Some elements need time to focus and spellcheck before any tests are
+ // run on them.
+ function actuallyOpenContextMenuFor() {
+ lastElement = element;
+ var eventDetails = { type: "contextmenu", button: 2, shiftKey: shiftkey };
+ synthesizeMouse(element, 2, 2, eventDetails, element.ownerGlobal);
+ }
+
+ if (waitForSpellCheck) {
+ var { onSpellCheck } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
+ );
+ onSpellCheck(element, actuallyOpenContextMenuFor);
+ } else {
+ actuallyOpenContextMenuFor();
+ }
+}
+
+function closeContextMenu() {
+ contextMenu.hidePopup();
+}
+
+function getVisibleMenuItems(aMenu, aData) {
+ var items = [];
+ var accessKeys = {};
+ for (var i = 0; i < aMenu.children.length; i++) {
+ var item = aMenu.children[i];
+ if (item.hidden) {
+ continue;
+ }
+
+ var key = item.accessKey;
+ if (key) {
+ key = key.toLowerCase();
+ }
+
+ if (item.nodeName == "menuitem") {
+ var isGenerated =
+ item.classList.contains("spell-suggestion") ||
+ item.classList.contains("sendtab-target");
+ if (isGenerated) {
+ is(item.id, "", "child menuitem #" + i + " is generated");
+ } else {
+ ok(item.id, "child menuitem #" + i + " has an ID");
+ }
+ var label = item.getAttribute("label");
+ ok(label.length, "menuitem " + item.id + " has a label");
+ if (isGenerated) {
+ is(key, "", "Generated items shouldn't have an access key");
+ items.push("*" + label);
+ } else if (
+ item.id.indexOf("spell-check-dictionary-") != 0 &&
+ item.id != "spell-no-suggestions" &&
+ item.id != "spell-add-dictionaries-main" &&
+ item.id != "context-savelinktopocket" &&
+ item.id != "fill-login-no-logins" &&
+ // Inspect accessibility properties does not have an access key. See
+ // bug 1630717 for more details.
+ item.id != "context-inspect-a11y" &&
+ !item.id.includes("context-media-playbackrate")
+ ) {
+ if (item.id != FRAME_OS_PID) {
+ ok(key, "menuitem " + item.id + " has an access key");
+ }
+ if (accessKeys[key]) {
+ ok(
+ false,
+ "menuitem " + item.id + " has same accesskey as " + accessKeys[key]
+ );
+ } else {
+ accessKeys[key] = item.id;
+ }
+ }
+ if (!isGenerated) {
+ items.push(item.id);
+ }
+ items.push(!item.disabled);
+ } else if (item.nodeName == "menuseparator") {
+ ok(true, "--- seperator id is " + item.id);
+ items.push("---");
+ items.push(null);
+ } else if (item.nodeName == "menu") {
+ ok(item.id, "child menu #" + i + " has an ID");
+ ok(key, "menu has an access key");
+ if (accessKeys[key]) {
+ ok(
+ false,
+ "menu " + item.id + " has same accesskey as " + accessKeys[key]
+ );
+ } else {
+ accessKeys[key] = item.id;
+ }
+ items.push(item.id);
+ items.push(!item.disabled);
+ // Add a dummy item so that the indexes in checkMenu are the same
+ // for expectedItems and actualItems.
+ items.push([]);
+ items.push(null);
+ } else if (item.nodeName == "menugroup") {
+ ok(item.id, "child menugroup #" + i + " has an ID");
+ items.push(item.id);
+ items.push(!item.disabled);
+ var menugroupChildren = [];
+ for (var child of item.children) {
+ if (child.hidden) {
+ continue;
+ }
+
+ menugroupChildren.push([child.id, !child.disabled]);
+ }
+ items.push(menugroupChildren);
+ items.push(null);
+ } else {
+ ok(
+ false,
+ "child #" +
+ i +
+ " of menu ID " +
+ aMenu.id +
+ " has an unknown type (" +
+ item.nodeName +
+ ")"
+ );
+ }
+ }
+ return items;
+}
+
+function checkContextMenu(expectedItems) {
+ is(contextMenu.state, "open", "checking if popup is open");
+ var data = { generatedSubmenuId: 1 };
+ checkMenu(contextMenu, expectedItems, data);
+}
+
+function checkMenuItem(
+ actualItem,
+ actualEnabled,
+ expectedItem,
+ expectedEnabled,
+ index
+) {
+ is(
+ `${actualItem}`,
+ expectedItem,
+ "checking item #" + index / 2 + " (" + expectedItem + ") name"
+ );
+
+ if (
+ (typeof expectedEnabled == "object" && expectedEnabled != null) ||
+ (typeof actualEnabled == "object" && actualEnabled != null)
+ ) {
+ ok(!(actualEnabled == null), "actualEnabled is not null");
+ ok(!(expectedEnabled == null), "expectedEnabled is not null");
+ is(typeof actualEnabled, typeof expectedEnabled, "checking types");
+
+ if (
+ typeof actualEnabled != typeof expectedEnabled ||
+ actualEnabled == null ||
+ expectedEnabled == null
+ ) {
+ return;
+ }
+
+ is(
+ actualEnabled.type,
+ expectedEnabled.type,
+ "checking item #" + index / 2 + " (" + expectedItem + ") type attr value"
+ );
+ var icon = actualEnabled.icon;
+ if (icon) {
+ var tmp = "";
+ var j = icon.length - 1;
+ while (j && icon[j] != "/") {
+ tmp = icon[j--] + tmp;
+ }
+ icon = tmp;
+ }
+ is(
+ icon,
+ expectedEnabled.icon,
+ "checking item #" + index / 2 + " (" + expectedItem + ") icon attr value"
+ );
+ is(
+ actualEnabled.checked,
+ expectedEnabled.checked,
+ "checking item #" + index / 2 + " (" + expectedItem + ") has checked attr"
+ );
+ is(
+ actualEnabled.disabled,
+ expectedEnabled.disabled,
+ "checking item #" +
+ index / 2 +
+ " (" +
+ expectedItem +
+ ") has disabled attr"
+ );
+ } else if (expectedEnabled != null) {
+ is(
+ actualEnabled,
+ expectedEnabled,
+ "checking item #" + index / 2 + " (" + expectedItem + ") enabled state"
+ );
+ }
+}
+
+/*
+ * checkMenu - checks to see if the specified <menupopup> contains the
+ * expected items and state.
+ * expectedItems is a array of (1) item IDs and (2) a boolean specifying if
+ * the item is enabled or not (or null to ignore it). Submenus can be checked
+ * by providing a nested array entry after the expected <menu> ID.
+ * For example: ["blah", true, // item enabled
+ * "submenu", null, // submenu
+ * ["sub1", true, // submenu contents
+ * "sub2", false], null, // submenu contents
+ * "lol", false] // item disabled
+ *
+ */
+function checkMenu(menu, expectedItems, data) {
+ var actualItems = getVisibleMenuItems(menu, data);
+ // ok(false, "Items are: " + actualItems);
+ for (var i = 0; i < expectedItems.length; i += 2) {
+ var actualItem = actualItems[i];
+ var actualEnabled = actualItems[i + 1];
+ var expectedItem = expectedItems[i];
+ var expectedEnabled = expectedItems[i + 1];
+ if (expectedItem instanceof Array) {
+ ok(true, "Checking submenu/menugroup...");
+ var previousId = expectedItems[i - 2]; // The last item was the menu ID.
+ var previousItem = menu.getElementsByAttribute("id", previousId)[0];
+ ok(
+ previousItem,
+ (previousItem ? previousItem.nodeName : "item") +
+ " with previous id (" +
+ previousId +
+ ") found"
+ );
+ if (previousItem && previousItem.nodeName == "menu") {
+ ok(previousItem, "got a submenu element of id='" + previousId + "'");
+ is(
+ previousItem.nodeName,
+ "menu",
+ "submenu element of id='" + previousId + "' has expected nodeName"
+ );
+ checkMenu(previousItem.menupopup, expectedItem, data, i);
+ } else if (previousItem && previousItem.nodeName == "menugroup") {
+ ok(expectedItem.length, "menugroup must not be empty");
+ for (var j = 0; j < expectedItem.length / 2; j++) {
+ checkMenuItem(
+ actualItems[i][j][0],
+ actualItems[i][j][1],
+ expectedItem[j * 2],
+ expectedItem[j * 2 + 1],
+ i + j * 2
+ );
+ }
+ i += j;
+ } else {
+ ok(false, "previous item is not a menu or menugroup");
+ }
+ } else {
+ checkMenuItem(
+ actualItem,
+ actualEnabled,
+ expectedItem,
+ expectedEnabled,
+ i
+ );
+ }
+ }
+ // Could find unexpected extra items at the end...
+ is(
+ actualItems.length,
+ expectedItems.length,
+ "checking expected number of menu entries"
+ );
+}
+
+let lastElementSelector = null;
+/**
+ * Right-clicks on the element that matches `selector` and checks the
+ * context menu that appears against the `menuItems` array.
+ *
+ * @param {String} selector
+ * A selector passed to querySelector to find
+ * the element that will be referenced.
+ * @param {Array} menuItems
+ * An array of menuitem ids and their associated enabled state. A state
+ * of null means that it will be ignored. Ids of '---' are used for
+ * menuseparators.
+ * @param {Object} options, optional
+ * skipFocusChange: don't move focus to the element before test, useful
+ * if you want to delay spell-check initialization
+ * offsetX: horizontal mouse offset from the top-left corner of
+ * the element, optional
+ * offsetY: vertical mouse offset from the top-left corner of the
+ * element, optional
+ * centered: if true, mouse position is centered in element, defaults
+ * to true if offsetX and offsetY are not provided
+ * waitForSpellCheck: wait until spellcheck is initialized before
+ * starting test
+ * preCheckContextMenuFn: callback to run before opening menu
+ * onContextMenuShown: callback to run when the context menu is shown
+ * postCheckContextMenuFn: callback to run after opening menu
+ * keepMenuOpen: if true, we do not call hidePopup, the consumer is
+ * responsible for calling it.
+ * @return {Promise} resolved after the test finishes
+ */
+async function test_contextmenu(selector, menuItems, options = {}) {
+ contextMenu = document.getElementById("contentAreaContextMenu");
+ is(contextMenu.state, "closed", "checking if popup is closed");
+
+ // Default to centered if no positioning is defined.
+ if (!options.offsetX && !options.offsetY) {
+ options.centered = true;
+ }
+
+ if (!options.skipFocusChange) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[lastElementSelector, selector]],
+ async function ([contentLastElementSelector, contentSelector]) {
+ if (contentLastElementSelector) {
+ let contentLastElement = content.document.querySelector(
+ contentLastElementSelector
+ );
+ contentLastElement.blur();
+ }
+ let element = content.document.querySelector(contentSelector);
+ element.focus();
+ }
+ );
+ lastElementSelector = selector;
+ info(`Moved focus to ${selector}`);
+ }
+
+ if (options.preCheckContextMenuFn) {
+ await options.preCheckContextMenuFn();
+ info("Completed preCheckContextMenuFn");
+ }
+
+ if (options.waitForSpellCheck) {
+ info("Waiting for spell check");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [selector],
+ async function (contentSelector) {
+ let { onSpellCheck } = ChromeUtils.importESModule(
+ "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
+ );
+ let element = content.document.querySelector(contentSelector);
+ await new Promise(resolve => onSpellCheck(element, resolve));
+ info("Spell check running");
+ }
+ );
+ }
+
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ selector,
+ options.offsetX || 0,
+ options.offsetY || 0,
+ {
+ type: "contextmenu",
+ button: 2,
+ shiftkey: options.shiftkey,
+ centered: options.centered,
+ },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+ info("Popup Shown");
+
+ if (options.onContextMenuShown) {
+ await options.onContextMenuShown();
+ info("Completed onContextMenuShown");
+ }
+
+ if (menuItems) {
+ if (Services.prefs.getBoolPref("devtools.inspector.enabled", true)) {
+ const inspectItems =
+ menuItems.includes("context-viewsource") ||
+ menuItems.includes("context-viewpartialsource-selection")
+ ? []
+ : ["---", null];
+ if (
+ Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
+ (Services.prefs.getBoolPref("devtools.everOpened", false) ||
+ Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0)
+ ) {
+ inspectItems.push("context-inspect-a11y", true);
+ }
+ inspectItems.push("context-inspect", true);
+
+ menuItems = menuItems.concat(inspectItems);
+ }
+
+ checkContextMenu(menuItems);
+ }
+
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ if (options.postCheckContextMenuFn) {
+ await options.postCheckContextMenuFn();
+ info("Completed postCheckContextMenuFn");
+ }
+
+ if (!options.keepMenuOpen) {
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+ }
+}
diff --git a/browser/base/content/test/contextMenu/ctxmenu-image.png b/browser/base/content/test/contextMenu/ctxmenu-image.png
new file mode 100644
index 0000000000..4c3be50847
--- /dev/null
+++ b/browser/base/content/test/contextMenu/ctxmenu-image.png
Binary files differ
diff --git a/browser/base/content/test/contextMenu/doggy.png b/browser/base/content/test/contextMenu/doggy.png
new file mode 100644
index 0000000000..73632d3229
--- /dev/null
+++ b/browser/base/content/test/contextMenu/doggy.png
Binary files differ
diff --git a/browser/base/content/test/contextMenu/file_bug1798178.html b/browser/base/content/test/contextMenu/file_bug1798178.html
new file mode 100644
index 0000000000..49e0092a4f
--- /dev/null
+++ b/browser/base/content/test/contextMenu/file_bug1798178.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ <a href="https://example.org/browser/browser/base/content/test/contextMenu/bug1798178.sjs">Download Link</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/contextMenu/firebird.png b/browser/base/content/test/contextMenu/firebird.png
new file mode 100644
index 0000000000..de5c22f8ce
--- /dev/null
+++ b/browser/base/content/test/contextMenu/firebird.png
Binary files differ
diff --git a/browser/base/content/test/contextMenu/firebird.png^headers^ b/browser/base/content/test/contextMenu/firebird.png^headers^
new file mode 100644
index 0000000000..2918fdbe5f
--- /dev/null
+++ b/browser/base/content/test/contextMenu/firebird.png^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Found
+Location: doggy.png
diff --git a/browser/base/content/test/contextMenu/subtst_contextmenu.html b/browser/base/content/test/contextMenu/subtst_contextmenu.html
new file mode 100644
index 0000000000..2c263fbce4
--- /dev/null
+++ b/browser/base/content/test/contextMenu/subtst_contextmenu.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Subtest for browser context menu</title>
+</head>
+<body>
+Browser context menu subtest.
+
+<div id="test-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
+<a id="test-link" href="http://mozilla.com">Click the monkey!</a>
+<div id="shadow-host"></div>
+<a href="http://mozilla.com" style="display: block">
+ <span id="shadow-host-in-link"></span>
+</a>
+<script>
+document.getElementById("shadow-host").attachShadow({ mode: "closed" }).innerHTML =
+ "<a href='http://mozilla.com'>Click the monkey!</a>";
+document.getElementById("shadow-host-in-link").attachShadow({ mode: "closed" }).innerHTML =
+ "<span>Click the monkey!</span>";
+</script>
+<a id="test-mailto" href="mailto:codemonkey@mozilla.com">Mail the monkey!</a><br>
+<a id="test-tel" href="tel:555-123-4567">Call random number!</a><br>
+<input id="test-input"><br>
+<img id="test-image" src="ctxmenu-image.png">
+<svg>
+ <image id="test-svg-image" href="ctxmenu-image.png"/>
+</svg>
+<canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas>
+<video controls id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video>
+<video id="test-audio-in-video" src="audio.ogg" width="100" height="100" style="background-color: red"></video>
+<video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video>
+<video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow">
+ <source src="bogus.duh" type="video/durrrr;">
+</video>
+<iframe id="test-iframe" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-audio-in-iframe" src="audio.ogg" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-pdf-viewer-in-frame" src="file_pdfjs_test.pdf" width="100" height="100" style="border: 1px solid black"></iframe>
+<textarea id="test-textarea">chssseesbbbie</textarea> <!-- a weird word which generates only one suggestion -->
+<div id="test-contenteditable" contenteditable="true">chssseefsbbbie</div> <!-- a more weird word which generates no suggestions -->
+<div id="test-contenteditable-spellcheck-false" contenteditable="true" spellcheck="false">test</div> <!-- No Check Spelling menu item -->
+<div id="test-dom-full-screen">DOM full screen FTW</div>
+<div id="test-select-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
+<div id="test-select-text-link">http://mozilla.com</div>
+<a id="test-image-link" href="#"><img src="ctxmenu-image.png"></a>
+<input id="test-select-input-text" type="text" value="input">
+<input id="test-select-input-text-type-password" type="password" value="password">
+<img id="test-longdesc" src="ctxmenu-image.png" longdesc="http://www.mozilla.org"></embed>
+<iframe id="test-srcdoc" width="98" height="98" srcdoc="Hello World" style="border: 1px solid black"></iframe>
+<svg id="svg-with-link" width=10 height=10><a xlink:href="http://example.com/"><circle cx="50%" cy="50%" r="50%" fill="blue"/></a></svg>
+<svg id="svg-with-link2" width=10 height=10><a xlink:href="http://example.com/" xlink:type="simple"><circle cx="50%" cy="50%" r="50%" fill="green"/></a></svg>
+<svg id="svg-with-link3" width=10 height=10><a href="http://example.com/"><circle cx="50%" cy="50%" r="50%" fill="red"/></a></svg>
+<svg id="svg-with-relative-link" width=10 height=10><a xlink:href="/"><circle cx="50%" cy="50%" r="50%" fill="blue"/></a></svg>
+<svg id="svg-with-relative-link2" width=10 height=10><a xlink:href="/" xlink:type="simple"><circle cx="50%" cy="50%" r="50%" fill="green"/></a></svg>
+<svg id="svg-with-relative-link3" width=10 height=10><a href="/"><circle cx="50%" cy="50%" r="50%" fill="red"/></a></svg>
+<span id="test-background-image" style="background-image: url('ctxmenu-image.png')">Text with background
+ <a id='test-background-image-link' href="about:blank">image</a>
+ .
+</span></body>
+</html>
diff --git a/browser/base/content/test/contextMenu/subtst_contextmenu_input.html b/browser/base/content/test/contextMenu/subtst_contextmenu_input.html
new file mode 100644
index 0000000000..a34cbbe122
--- /dev/null
+++ b/browser/base/content/test/contextMenu/subtst_contextmenu_input.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Subtest for browser context menu</title>
+</head>
+<body>
+ Browser context menu subtest.
+ <input id="input_text">
+ <input id="input_spellcheck_no_value">
+ <input id="input_spellcheck_incorrect" spellcheck="true" value="prodkjfgigrty">
+ <input id="input_spellcheck_correct" spellcheck="true" value="foo">
+ <input id="input_disabled" disabled="true">
+ <input id="input_password">
+ <input id="input_email" type="email">
+ <input id="input_tel" type="tel">
+ <input id="input_url" type="url">
+ <input id="input_number" type="number">
+ <input id="input_date" type="date">
+ <input id="input_time" type="time">
+ <input id="input_color" type="color">
+ <input id="input_range" type="range">
+ <input id="input_search" type="search">
+ <input id="input_datetime" type="datetime">
+ <input id="input_month" type="month">
+ <input id="input_week" type="week">
+ <input id="input_datetime-local" type="datetime-local">
+ <input id="input_readonly" readonly="true">
+ <input id="input_username" name="username">
+</body>
+</html>
diff --git a/browser/base/content/test/contextMenu/subtst_contextmenu_keyword.html b/browser/base/content/test/contextMenu/subtst_contextmenu_keyword.html
new file mode 100644
index 0000000000..a0f2b0584f
--- /dev/null
+++ b/browser/base/content/test/contextMenu/subtst_contextmenu_keyword.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Subtest for browser context menu</title>
+</head>
+<body>
+ Browser context menu subtest.
+ <input id="input_text_no_form">
+ <form id="form_with_password">
+ <input id="login_text">
+ <input id="input_password" type="password">
+ </form>
+ <form id="form_without_password">
+ <input id="search_text">
+ </form>
+</body>
+</html>
diff --git a/browser/base/content/test/contextMenu/subtst_contextmenu_webext.html b/browser/base/content/test/contextMenu/subtst_contextmenu_webext.html
new file mode 100644
index 0000000000..ac3b5415dd
--- /dev/null
+++ b/browser/base/content/test/contextMenu/subtst_contextmenu_webext.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for browser context menu</title>
+</head>
+<body>
+ Browser context menu subtest.
+ <a href="moz-extension://foo-bar/tab.html" id="link">Link to an extension resource</a>
+ <video src="moz-extension://foo-bar/video.ogg" id="video"></video>
+</body>
+</html>
diff --git a/browser/base/content/test/contextMenu/subtst_contextmenu_xul.xhtml b/browser/base/content/test/contextMenu/subtst_contextmenu_xul.xhtml
new file mode 100644
index 0000000000..c8ff92a76c
--- /dev/null
+++ b/browser/base/content/test/contextMenu/subtst_contextmenu_xul.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <label id="test-xul-text-link-label" is="text-link" value="XUL text-link label" href="https://www.mozilla.com"/>
+</window>
diff --git a/browser/base/content/test/contextMenu/test_contextmenu_iframe.html b/browser/base/content/test/contextMenu/test_contextmenu_iframe.html
new file mode 100644
index 0000000000..cf5b871ecd
--- /dev/null
+++ b/browser/base/content/test/contextMenu/test_contextmenu_iframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Subtest for browser context menu iframes</title>
+</head>
+<body>
+Browser context menu iframe subtest.
+
+<iframe src="https://example.com/" id="iframe"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/contextMenu/test_contextmenu_links.html b/browser/base/content/test/contextMenu/test_contextmenu_links.html
new file mode 100644
index 0000000000..650c136f99
--- /dev/null
+++ b/browser/base/content/test/contextMenu/test_contextmenu_links.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Subtest for browser context menu links</title>
+</head>
+<body>
+Browser context menu link subtest.
+
+<a id="test-link" href="https://example.com">Click the monkey!</a>
+<a id="test-image-link" href="/"><img src="ctxmenu-image.png"></a>
+<svg id="svg-with-link" width=10 height=10><a xlink:href="https://example.com/"><circle cx="50%" cy="50%" r="50%" fill="blue"/></a></svg>
+<svg id="svg-with-relative-link" width=10 height=10><a xlink:href="/"><circle cx="50%" cy="50%" r="50%" fill="blue"/></a></svg>
+</body>
+</html>
diff --git a/browser/base/content/test/contextMenu/test_view_image_inline_svg.html b/browser/base/content/test/contextMenu/test_view_image_inline_svg.html
new file mode 100644
index 0000000000..42a41e42cb
--- /dev/null
+++ b/browser/base/content/test/contextMenu/test_view_image_inline_svg.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html><head>
+<style>
+body {
+ background: fixed #222 url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjUwMCIgd2lkdGg9IjUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGZpbHRlciBpZD0iYSI+PGZlVHVyYnVsZW5jZSBiYXNlRnJlcXVlbmN5PSIuOSIgbnVtT2N0YXZlcz0iMTAiIHN0aXRjaFRpbGVzPSJzdGl0Y2giIHR5cGU9ImZyYWN0YWxOb2lzZSIvPjwvZmlsdGVyPjxwYXRoIGQ9Im0wIDBoNTAwdjUwMGgtNTAweiIgZmlsbD0iIzExMSIvPjxwYXRoIGQ9Im0wIDBoNTAwdjUwMGgtNTAweiIgZmlsdGVyPSJ1cmwoI2EpIiBvcGFjaXR5PSIuMiIvPjwvc3ZnPgo=");
+ background-size: cover;
+ color: #ccc;
+}
+</style>
+</head>
+<body>
+This page has an inline SVG image as a background.
+
+
+</body></html>
diff --git a/browser/base/content/test/contextMenu/test_view_image_revoked_cached_blob.html b/browser/base/content/test/contextMenu/test_view_image_revoked_cached_blob.html
new file mode 100644
index 0000000000..ba130c793a
--- /dev/null
+++ b/browser/base/content/test/contextMenu/test_view_image_revoked_cached_blob.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<html class="wait">
+<meta charset="utf-8">
+<title>currentSrc is right even if underlying image is a shared blob</title>
+<img id="first">
+<img id="second">
+<script>
+(async function() {
+ let canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+ let ctx = canvas.getContext("2d");
+ ctx.fillStyle = "green";
+ ctx.rect(0, 0, 100, 100);
+ ctx.fill();
+
+ let blob = await new Promise(resolve => canvas.toBlob(resolve));
+
+ let first = document.querySelector("#first");
+ let second = document.querySelector("#second");
+
+ let firstLoad = new Promise(resolve => {
+ first.addEventListener("load", resolve, { once: true });
+ });
+
+ let secondLoad = new Promise(resolve => {
+ second.addEventListener("load", resolve, { once: true });
+ });
+
+ let uri1 = URL.createObjectURL(blob);
+ let uri2 = URL.createObjectURL(blob);
+ first.src = uri1;
+ second.src = uri2;
+
+ await firstLoad;
+ await secondLoad;
+ URL.revokeObjectURL(uri1);
+ document.documentElement.className = "";
+}());
+</script>
diff --git a/browser/base/content/test/favicons/accept.html b/browser/base/content/test/favicons/accept.html
new file mode 100644
index 0000000000..4bb00243b3
--- /dev/null
+++ b/browser/base/content/test/favicons/accept.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for accept header</title>
+ <link rel="icon" href="accept.sjs">
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/accept.sjs b/browser/base/content/test/favicons/accept.sjs
new file mode 100644
index 0000000000..3e798ba817
--- /dev/null
+++ b/browser/base/content/test/favicons/accept.sjs
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+ // Doesn't seem any way to get the value from prefs from here. :(
+ let expected = "image/avif,image/webp,*/*";
+ if (expected != request.getHeader("Accept")) {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+ response.setHeader("Location", "moz.png");
+}
diff --git a/browser/base/content/test/favicons/auth_test.html b/browser/base/content/test/favicons/auth_test.html
new file mode 100644
index 0000000000..90b78432f8
--- /dev/null
+++ b/browser/base/content/test/favicons/auth_test.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for http auth</title>
+ <link rel="icon" type="image/png" href="auth_test.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/auth_test.png b/browser/base/content/test/favicons/auth_test.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/base/content/test/favicons/auth_test.png
diff --git a/browser/base/content/test/favicons/auth_test.png^headers^ b/browser/base/content/test/favicons/auth_test.png^headers^
new file mode 100644
index 0000000000..5024ae1c4b
--- /dev/null
+++ b/browser/base/content/test/favicons/auth_test.png^headers^
@@ -0,0 +1,2 @@
+HTTP 401 Unauthorized
+WWW-Authenticate: Basic realm="Favicon auth"
diff --git a/browser/base/content/test/favicons/blank.html b/browser/base/content/test/favicons/blank.html
new file mode 100644
index 0000000000..297eb8cd78
--- /dev/null
+++ b/browser/base/content/test/favicons/blank.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+</head>
+</html>
diff --git a/browser/base/content/test/favicons/browser.ini b/browser/base/content/test/favicons/browser.ini
new file mode 100644
index 0000000000..a5fc30ecfd
--- /dev/null
+++ b/browser/base/content/test/favicons/browser.ini
@@ -0,0 +1,113 @@
+[DEFAULT]
+support-files =
+ head.js
+ discovery.html
+ moz.png
+ rich_moz_1.png
+ rich_moz_2.png
+ file_bug970276_favicon1.ico
+ file_generic_favicon.ico
+ file_with_favicon.html
+prefs =
+ browser.chrome.guess_favicon=true
+
+[browser_bug408415.js]
+[browser_bug550565.js]
+[browser_favicon_accept.js]
+support-files =
+ accept.html
+ accept.sjs
+[browser_favicon_auth.js]
+support-files =
+ auth_test.html
+ auth_test.png
+ auth_test.png^headers^
+[browser_favicon_cache.js]
+support-files =
+ cookie_favicon.sjs
+ cookie_favicon.html
+[browser_favicon_change.js]
+support-files =
+ file_favicon_change.html
+[browser_favicon_change_not_in_document.js]
+support-files =
+ file_favicon_change_not_in_document.html
+[browser_favicon_credentials.js]
+https_first_disabled = true
+support-files =
+ credentials1.html
+ credentials2.html
+ credentials.png
+ credentials.png^headers^
+[browser_favicon_crossorigin.js]
+https_first_disabled = true
+support-files =
+ crossorigin.html
+ crossorigin.png
+ crossorigin.png^headers^
+[browser_favicon_load.js]
+https_first_disabled = true
+support-files =
+ file_favicon.html
+ file_favicon.png
+ file_favicon.png^headers^
+ file_favicon_thirdParty.html
+[browser_favicon_nostore.js]
+https_first_disabled = true
+support-files =
+ no-store.html
+ no-store.png
+ no-store.png^headers^
+[browser_favicon_referer.js]
+support-files =
+ file_favicon_no_referrer.html
+[browser_favicon_store.js]
+support-files =
+ datauri-favicon.html
+ file_favicon.html
+ file_favicon.png
+ file_favicon.png^headers^
+[browser_icon_discovery.js]
+[browser_invalid_href_fallback.js]
+https_first_disabled = true
+support-files =
+ file_invalid_href.html
+[browser_missing_favicon.js]
+support-files =
+ blank.html
+[browser_mixed_content.js]
+support-files =
+ file_insecure_favicon.html
+ file_favicon.png
+[browser_multiple_icons_in_short_timeframe.js]
+[browser_oversized.js]
+support-files =
+ large_favicon.html
+ large.png
+[browser_preferred_icons.js]
+support-files =
+ icon.svg
+[browser_redirect.js]
+support-files =
+ file_favicon_redirect.html
+ file_favicon_redirect.ico
+ file_favicon_redirect.ico^headers^
+[browser_rich_icons.js]
+support-files =
+ file_rich_icon.html
+ file_mask_icon.html
+[browser_rooticon.js]
+https_first_disabled = true
+support-files =
+ blank.html
+[browser_subframe_favicons_not_used.js]
+support-files =
+ file_bug970276_popup1.html
+ file_bug970276_popup2.html
+ file_bug970276_favicon2.ico
+[browser_title_flicker.js]
+https_first_disabled = true
+support-files =
+ file_with_slow_favicon.html
+ blank.html
+ file_favicon.png
diff --git a/browser/base/content/test/favicons/browser_bug408415.js b/browser/base/content/test/favicons/browser_bug408415.js
new file mode 100644
index 0000000000..1526477db3
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_bug408415.js
@@ -0,0 +1,34 @@
+add_task(async function test() {
+ let testPath = getRootDirectory(gTestPath);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (tabBrowser) {
+ const URI = testPath + "file_with_favicon.html";
+ const expectedIcon = testPath + "file_generic_favicon.ico";
+ let faviconPromise = waitForLinkAvailable(tabBrowser);
+
+ BrowserTestUtils.loadURIString(tabBrowser, URI);
+
+ let iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Correct icon before pushState.");
+
+ faviconPromise = waitForLinkAvailable(tabBrowser);
+
+ await SpecialPowers.spawn(tabBrowser, [], function () {
+ content.location.href += "#foo";
+ });
+
+ TestUtils.executeSoon(() => {
+ faviconPromise.cancel();
+ });
+
+ try {
+ await faviconPromise;
+ ok(false, "Should not have seen a new icon load.");
+ } catch (e) {
+ ok(true, "Should have been able to cancel the promise.");
+ }
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_bug550565.js b/browser/base/content/test/favicons/browser_bug550565.js
new file mode 100644
index 0000000000..32a7527bbf
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_bug550565.js
@@ -0,0 +1,35 @@
+add_task(async function test() {
+ let testPath = getRootDirectory(gTestPath);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (tabBrowser) {
+ const URI = testPath + "file_with_favicon.html";
+ const expectedIcon = testPath + "file_generic_favicon.ico";
+ let faviconPromise = waitForLinkAvailable(tabBrowser);
+
+ BrowserTestUtils.loadURIString(tabBrowser, URI);
+
+ let iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Correct icon before pushState.");
+
+ faviconPromise = waitForLinkAvailable(tabBrowser);
+
+ await SpecialPowers.spawn(tabBrowser, [], function () {
+ content.history.pushState("page2", "page2", "page2");
+ });
+
+ // We've navigated and shouldn't get a call to onLinkIconAvailable.
+ TestUtils.executeSoon(() => {
+ faviconPromise.cancel();
+ });
+
+ try {
+ await faviconPromise;
+ ok(false, "Should not have seen a new icon load.");
+ } catch (e) {
+ ok(true, "Should have been able to cancel the promise.");
+ }
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_accept.js b/browser/base/content/test/favicons/browser_favicon_accept.js
new file mode 100644
index 0000000000..dc59a406b5
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_accept.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitest/content/",
+ "http://mochi.test:8888/"
+);
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let faviconPromise = waitForFaviconMessage(true, `${ROOT}accept.sjs`);
+
+ BrowserTestUtils.loadURIString(browser, ROOT + "accept.html");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ try {
+ let result = await faviconPromise;
+ Assert.equal(
+ result.iconURL,
+ `${ROOT}accept.sjs`,
+ "Should have seen the icon"
+ );
+ } catch (e) {
+ Assert.ok(false, "Favicon load failed.");
+ }
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_auth.js b/browser/base/content/test/favicons/browser_favicon_auth.js
new file mode 100644
index 0000000000..fb0e75f2ab
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_auth.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let faviconPromise = waitForFaviconMessage(true, `${ROOT}auth_test.png`);
+
+ BrowserTestUtils.loadURIString(browser, `${ROOT}auth_test.html`);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await Assert.rejects(
+ faviconPromise,
+ result => {
+ return result.iconURL == `${ROOT}auth_test.png`;
+ },
+ "Should have failed to load the icon."
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_cache.js b/browser/base/content/test/favicons/browser_favicon_cache.js
new file mode 100644
index 0000000000..903f038d6c
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_cache.js
@@ -0,0 +1,50 @@
+add_task(async () => {
+ const testPath =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/favicons/cookie_favicon.html";
+ const resetPath =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/favicons/cookie_favicon.sjs?reset";
+
+ let tab = BrowserTestUtils.addTab(gBrowser, testPath);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+
+ let faviconPromise = waitForLinkAvailable(browser);
+ await BrowserTestUtils.browserLoaded(browser);
+ await faviconPromise;
+ let cookies = Services.cookies.getCookiesFromHost(
+ "example.com",
+ browser.contentPrincipal.originAttributes
+ );
+ let seenCookie = false;
+ for (let cookie of cookies) {
+ if (cookie.name == "faviconCookie") {
+ seenCookie = true;
+ is(cookie.value, "1", "Should have seen the right initial cookie.");
+ }
+ }
+ ok(seenCookie, "Should have seen the cookie.");
+
+ faviconPromise = waitForLinkAvailable(browser);
+ BrowserTestUtils.loadURIString(browser, testPath);
+ await BrowserTestUtils.browserLoaded(browser);
+ await faviconPromise;
+ cookies = Services.cookies.getCookiesFromHost(
+ "example.com",
+ browser.contentPrincipal.originAttributes
+ );
+ seenCookie = false;
+ for (let cookie of cookies) {
+ if (cookie.name == "faviconCookie") {
+ seenCookie = true;
+ is(cookie.value, "1", "Should have seen the cached cookie.");
+ }
+ }
+ ok(seenCookie, "Should have seen the cookie.");
+
+ // Reset the cookie so if this test is run again it will still pass.
+ await fetch(resetPath);
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_change.js b/browser/base/content/test/favicons/browser_favicon_change.js
new file mode 100644
index 0000000000..8faf266665
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_change.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+const TEST_URL = TEST_ROOT + "file_favicon_change.html";
+
+add_task(async function () {
+ let extraTab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ let haveChanged = waitForFavicon(
+ extraTab.linkedBrowser,
+ TEST_ROOT + "file_bug970276_favicon1.ico"
+ );
+
+ BrowserTestUtils.loadURIString(extraTab.linkedBrowser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(extraTab.linkedBrowser);
+ await haveChanged;
+
+ haveChanged = waitForFavicon(extraTab.linkedBrowser, TEST_ROOT + "moz.png");
+
+ SpecialPowers.spawn(extraTab.linkedBrowser, [], function () {
+ let ev = new content.CustomEvent("PleaseChangeFavicon", {});
+ content.dispatchEvent(ev);
+ });
+
+ await haveChanged;
+
+ ok(true, "Saw all the icons we expected.");
+
+ gBrowser.removeTab(extraTab);
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js b/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js
new file mode 100644
index 0000000000..34c5ae8baf
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js
@@ -0,0 +1,55 @@
+"use strict";
+
+const TEST_ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+const TEST_URL = TEST_ROOT + "file_favicon_change_not_in_document.html";
+
+/*
+ * This test tests a link element won't fire DOMLinkChanged/DOMLinkAdded unless
+ * it is added to the DOM. See more details in bug 1083895.
+ *
+ * Note that there is debounce logic in ContentLinkHandler.jsm, adding a new
+ * icon link after the icon parsing timeout will trigger a new icon extraction
+ * cycle. Hence, there should be two favicons loads in this test as it appends
+ * a new link to the DOM in the timeout callback defined in the test HTML page.
+ * However, the not-yet-added link element with href as "http://example.org/other-icon"
+ * should not fire the DOMLinkAdded event, nor should it fire the DOMLinkChanged
+ * event after its href gets updated later.
+ */
+add_task(async function () {
+ let extraTab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ROOT
+ ));
+ let domLinkAddedFired = 0;
+ let domLinkChangedFired = 0;
+ const linkAddedHandler = event => domLinkAddedFired++;
+ const linkChangedhandler = event => domLinkChangedFired++;
+ BrowserTestUtils.addContentEventListener(
+ gBrowser.selectedBrowser,
+ "DOMLinkAdded",
+ linkAddedHandler
+ );
+ BrowserTestUtils.addContentEventListener(
+ gBrowser.selectedBrowser,
+ "DOMLinkChanged",
+ linkChangedhandler
+ );
+
+ let expectedFavicon = TEST_ROOT + "file_generic_favicon.ico";
+ let faviconPromise = waitForFavicon(extraTab.linkedBrowser, expectedFavicon);
+
+ BrowserTestUtils.loadURIString(extraTab.linkedBrowser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(extraTab.linkedBrowser);
+
+ await faviconPromise;
+
+ is(
+ domLinkAddedFired,
+ 2,
+ "Should fire the correct number of DOMLinkAdded event."
+ );
+ is(domLinkChangedFired, 0, "Should not fire any DOMLinkChanged event.");
+
+ gBrowser.removeTab(extraTab);
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_credentials.js b/browser/base/content/test/favicons/browser_favicon_credentials.js
new file mode 100644
index 0000000000..405c620c8a
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_credentials.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT_DIR = getRootDirectory(gTestPath);
+
+const EXAMPLE_NET_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "https://example.net/"
+);
+
+const EXAMPLE_COM_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+
+const FAVICON_URL = EXAMPLE_COM_ROOT + "credentials.png";
+
+// Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+// All instances of addPermission and removePermission set up 3rd-party storage
+// access in a way that allows the test to proceed with TCP enabled.
+
+function run_test(url, shouldHaveCookies, description) {
+ add_task(async () => {
+ await SpecialPowers.addPermission(
+ "3rdPartyStorage^https://example.com",
+ true,
+ url
+ );
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ const faviconPromise = waitForFaviconMessage(true, FAVICON_URL);
+
+ BrowserTestUtils.loadURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await faviconPromise;
+
+ const seenCookie = Services.cookies
+ .getCookiesFromHost(
+ "example.com", // the icon's host, not the page's
+ browser.contentPrincipal.originAttributes
+ )
+ .some(cookie => cookie.name == "faviconCookie2");
+
+ // Clean up.
+ Services.cookies.removeAll();
+ Services.cache2.clear();
+
+ if (shouldHaveCookies) {
+ Assert.ok(
+ seenCookie,
+ `Should have seen the cookie (${description}).`
+ );
+ } else {
+ Assert.ok(
+ !seenCookie,
+ `Should have not seen the cookie (${description}).`
+ );
+ }
+ }
+ );
+ await SpecialPowers.removePermission(
+ "3rdPartyStorage^https://example.com",
+ url
+ );
+ });
+}
+
+// crossorigin="" only has credentials in the same-origin case
+run_test(`${EXAMPLE_NET_ROOT}credentials1.html`, false, "anonymous, remote");
+run_test(
+ `${EXAMPLE_COM_ROOT}credentials1.html`,
+ true,
+ "anonymous, same-origin"
+);
+
+// crossorigin="use-credentials" always has them
+run_test(
+ `${EXAMPLE_NET_ROOT}credentials2.html`,
+ true,
+ "use-credentials, remote"
+);
+run_test(
+ `${EXAMPLE_COM_ROOT}credentials2.html`,
+ true,
+ "use-credentials, same-origin"
+);
diff --git a/browser/base/content/test/favicons/browser_favicon_crossorigin.js b/browser/base/content/test/favicons/browser_favicon_crossorigin.js
new file mode 100644
index 0000000000..c1ae18f765
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_crossorigin.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT_DIR = getRootDirectory(gTestPath);
+
+const MOCHI_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+const EXAMPLE_NET_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "http://example.net/"
+);
+
+const EXAMPLE_COM_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+);
+
+const FAVICON_URL = EXAMPLE_COM_ROOT + "crossorigin.png";
+
+function run_test(root, shouldSucceed, description) {
+ add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ const faviconPromise = waitForFaviconMessage(true, FAVICON_URL);
+
+ BrowserTestUtils.loadURIString(browser, `${root}crossorigin.html`);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ if (shouldSucceed) {
+ try {
+ const result = await faviconPromise;
+ Assert.equal(
+ result.iconURL,
+ FAVICON_URL,
+ `Should have seen the icon (${description}).`
+ );
+ } catch (e) {
+ Assert.ok(false, `Favicon load failed (${description}).`);
+ }
+ } else {
+ await Assert.rejects(
+ faviconPromise,
+ result => result.iconURL == FAVICON_URL,
+ `Should have failed to load the icon (${description}).`
+ );
+ }
+ }
+ );
+ });
+}
+
+// crossorigin.png only allows CORS for MOCHI_ROOT.
+run_test(EXAMPLE_NET_ROOT, false, "remote origin not allowed");
+run_test(MOCHI_ROOT, true, "remote origin allowed");
+
+// Same-origin but with the crossorigin attribute.
+run_test(EXAMPLE_COM_ROOT, true, "same-origin");
diff --git a/browser/base/content/test/favicons/browser_favicon_load.js b/browser/base/content/test/favicons/browser_favicon_load.js
new file mode 100644
index 0000000000..f948b681b0
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_load.js
@@ -0,0 +1,168 @@
+/**
+ * Bug 1247843 - A test case for testing whether the channel used to load favicon
+ * has correct classFlags.
+ * Note that this test is modified based on browser_favicon_userContextId.js.
+ */
+
+const CC = Components.Constructor;
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_SITE = "http://example.net";
+const TEST_THIRD_PARTY_SITE = "http://mochi.test:8888";
+
+const TEST_PAGE =
+ TEST_SITE + "/browser/browser/base/content/test/favicons/file_favicon.html";
+const FAVICON_URI =
+ TEST_SITE + "/browser/browser/base/content/test/favicons/file_favicon.png";
+const TEST_THIRD_PARTY_PAGE =
+ TEST_SITE +
+ "/browser/browser/base/content/test/favicons/file_favicon_thirdParty.html";
+const THIRD_PARTY_FAVICON_URI =
+ TEST_THIRD_PARTY_SITE +
+ "/browser/browser/base/content/test/favicons/file_favicon.png";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+});
+
+let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+function clearAllImageCaches() {
+ var tools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
+ var imageCache = tools.getImgCacheForDocument(window.document);
+ imageCache.clearCache(true); // true=chrome
+ imageCache.clearCache(false); // false=content
+}
+
+function clearAllPlacesFavicons() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "places-favicons-expired");
+ resolve();
+ }, "places-favicons-expired");
+
+ PlacesUtils.favicons.expireAllFavicons();
+ });
+}
+
+function FaviconObserver(aPageURI, aFaviconURL, aTailingEnabled) {
+ this.reset(aPageURI, aFaviconURL, aTailingEnabled);
+}
+
+FaviconObserver.prototype = {
+ observe(aSubject, aTopic, aData) {
+ // Make sure that the topic is 'http-on-modify-request'.
+ if (aTopic === "http-on-modify-request") {
+ let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
+ let reqLoadInfo = httpChannel.loadInfo;
+ // Make sure this is a favicon request.
+ if (httpChannel.URI.spec !== this._faviconURL) {
+ return;
+ }
+
+ let cos = aSubject.QueryInterface(Ci.nsIClassOfService);
+ if (!cos) {
+ ok(false, "Http channel should implement nsIClassOfService.");
+ return;
+ }
+
+ if (!reqLoadInfo) {
+ ok(false, "Should have load info.");
+ return;
+ }
+
+ let haveTailFlag = !!(cos.classFlags & Ci.nsIClassOfService.Tail);
+ info("classFlags=" + cos.classFlags);
+ is(haveTailFlag, this._tailingEnabled, "Should have correct cos flag.");
+ } else {
+ ok(false, "Received unexpected topic: ", aTopic);
+ }
+
+ this._faviconLoaded.resolve();
+ },
+
+ reset(aPageURI, aFaviconURL, aTailingEnabled) {
+ this._faviconURL = aFaviconURL;
+ this._faviconLoaded = PromiseUtils.defer();
+ this._tailingEnabled = aTailingEnabled;
+ },
+
+ get promise() {
+ return this._faviconLoaded.promise;
+ },
+};
+
+function waitOnFaviconLoaded(aFaviconURL) {
+ return PlacesTestUtils.waitForNotification("favicon-changed", events =>
+ events.some(e => e.faviconUrl == aFaviconURL)
+ );
+}
+
+async function doTest(aTestPage, aFaviconURL, aTailingEnabled) {
+ let pageURI = Services.io.newURI(aTestPage);
+
+ // Create the observer object for observing favion channels.
+ let observer = new FaviconObserver(pageURI, aFaviconURL, aTailingEnabled);
+
+ let promiseWaitOnFaviconLoaded = waitOnFaviconLoaded(aFaviconURL);
+
+ // Add the observer earlier in case we miss it.
+ Services.obs.addObserver(observer, "http-on-modify-request");
+
+ // Open the tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, aTestPage);
+ // Waiting for favicon requests are all made.
+ await observer.promise;
+ // Waiting for favicon loaded.
+ await promiseWaitOnFaviconLoaded;
+
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+
+ // Close the tab.
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function setupTailingPreference(aTailingEnabled) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.tailing.enabled", aTailingEnabled]],
+ });
+}
+
+async function cleanup() {
+ // Clear all cookies.
+ Services.cookies.removeAll();
+ // Clear cache.
+ Services.cache2.clear();
+ // Clear Places favicon caches.
+ await clearAllPlacesFavicons();
+ // Clear all image caches and network caches.
+ clearAllImageCaches();
+ // Clear Places history.
+ await PlacesUtils.history.clear();
+}
+
+// A clean up function to prevent affecting other tests.
+registerCleanupFunction(async () => {
+ await cleanup();
+});
+
+add_task(async function test_favicon_with_tailing_enabled() {
+ await cleanup();
+
+ let tailingEnabled = true;
+
+ await setupTailingPreference(tailingEnabled);
+
+ await doTest(TEST_PAGE, FAVICON_URI, tailingEnabled);
+});
+
+add_task(async function test_favicon_with_tailing_disabled() {
+ await cleanup();
+
+ let tailingEnabled = false;
+
+ await setupTailingPreference(tailingEnabled);
+
+ await doTest(TEST_THIRD_PARTY_PAGE, THIRD_PARTY_FAVICON_URI, tailingEnabled);
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_nostore.js b/browser/base/content/test/favicons/browser_favicon_nostore.js
new file mode 100644
index 0000000000..3fec666bbe
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_nostore.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that a favicon with Cache-Control: no-store is not stored in Places.
+// Also tests that favicons added after pageshow are not stored.
+
+const TEST_SITE = "http://example.net";
+const ICON_URL =
+ TEST_SITE + "/browser/browser/base/content/test/favicons/no-store.png";
+const PAGE_URL =
+ TEST_SITE + "/browser/browser/base/content/test/favicons/no-store.html";
+
+async function cleanup() {
+ Services.cache2.clear();
+ await PlacesTestUtils.clearFavicons();
+ await PlacesUtils.history.clear();
+}
+
+add_task(async function browser_loader() {
+ await cleanup();
+ let iconPromise = waitForFaviconMessage(true, ICON_URL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+ registerCleanupFunction(async () => {
+ await cleanup();
+ });
+
+ let { iconURL } = await iconPromise;
+ is(iconURL, ICON_URL, "Should have seen the expected icon.");
+
+ // Ensure the favicon has not been stored.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ await new Promise((resolve, reject) => {
+ PlacesUtils.favicons.getFaviconURLForPage(
+ Services.io.newURI(PAGE_URL),
+ foundIconURI => {
+ if (foundIconURI) {
+ reject(new Error("An icon has been stored " + foundIconURI.spec));
+ }
+ resolve();
+ }
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function places_loader() {
+ await cleanup();
+
+ // Ensure the favicon is not stored even if Places is directly invoked.
+ await PlacesTestUtils.addVisits(PAGE_URL);
+ let faviconData = new Map();
+ faviconData.set(PAGE_URL, ICON_URL);
+ // We can't wait for the promise due to bug 740457, so we race with a timer.
+ await Promise.race([
+ PlacesTestUtils.addFavicons(faviconData),
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ new Promise(resolve => setTimeout(resolve, 1000)),
+ ]);
+ await new Promise((resolve, reject) => {
+ PlacesUtils.favicons.getFaviconURLForPage(
+ Services.io.newURI(PAGE_URL),
+ foundIconURI => {
+ if (foundIconURI) {
+ reject(new Error("An icon has been stored " + foundIconURI.spec));
+ }
+ resolve();
+ }
+ );
+ });
+});
+
+async function later_addition(iconUrl) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+ registerCleanupFunction(async () => {
+ await cleanup();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let iconPromise = waitForFaviconMessage(true, iconUrl);
+ await ContentTask.spawn(gBrowser.selectedBrowser, iconUrl, href => {
+ let doc = content.document;
+ let head = doc.head;
+ let link = doc.createElement("link");
+ link.rel = "icon";
+ link.href = href;
+ link.type = "image/png";
+ head.appendChild(link);
+ });
+ let { iconURL } = await iconPromise;
+ is(iconURL, iconUrl, "Should have seen the expected icon.");
+
+ // Ensure the favicon has not been stored.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ await new Promise((resolve, reject) => {
+ PlacesUtils.favicons.getFaviconURLForPage(
+ Services.io.newURI(PAGE_URL),
+ foundIconURI => {
+ if (foundIconURI) {
+ reject(new Error("An icon has been stored " + foundIconURI.spec));
+ }
+ resolve();
+ }
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_later_addition() {
+ for (let iconUrl of [
+ TEST_SITE + "/browser/browser/base/content/test/favicons/moz.png",
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABH0lEQVRYw2P8////f4YBBEwMAwxGHcBCUMX/91DGOSj/BpT/DkpzQChGBSjfBErLQsVZhmoI/L8LpRdD6X1QietQGhYy7FB5aAgwmkLpBKi4BZTPMThDgBGjHIDF+f9mKD0fKvGBRKNdoF7sgPL1saaJwZgGDkJ9vpZMn8PAHqg5G9FyifBgD4H/W9HyOWrU/f+DIzHhkoeZxxgzZEIAVtJ9RxX+Q6DAxCmP3byhXxkxshAs5odqbcioAY3UC1CBLyTGOTqAmsfAOWRCwBvqxV0oIUB2OQAzDy3/D+a6wB7q8mCU2vD/nw94GziYIQOtDRn9oXz+IZMGBKGMbCjNh9Ii+v8HR4uIAUeLiEEbb9twELaIRlqrmHG0bzjiHQAA1LVfww8jwM4AAAAASUVORK5CYII=",
+ ]) {
+ await later_addition(iconUrl);
+ }
+});
+
+add_task(async function root_icon_stored() {
+ XPCShellContentUtils.ensureInitialized(this);
+ let server = XPCShellContentUtils.createHttpServer({
+ hosts: ["www.nostore.com"],
+ });
+ server.registerFile(
+ "/favicon.ico",
+ new FileUtils.File(
+ PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ "browser",
+ "browser",
+ "base",
+ "content",
+ "test",
+ "favicons",
+ "no-store.png"
+ )
+ )
+ );
+ server.registerPathHandler("/page", (request, response) => {
+ response.write("<html>A page without icon</html>");
+ });
+
+ let noStorePromise = TestUtils.topicObserved(
+ "http-on-stop-request",
+ (s, t, d) => {
+ let chan = s.QueryInterface(Ci.nsIHttpChannel);
+ return chan?.URI.spec == "http://www.nostore.com/favicon.ico";
+ }
+ ).then(([chan]) => chan.isNoStoreResponse());
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://www.nostore.com/page",
+ },
+ async function (browser) {
+ await TestUtils.waitForCondition(async () => {
+ let uri = await new Promise(resolve =>
+ PlacesUtils.favicons.getFaviconURLForPage(
+ Services.io.newURI("http://www.nostore.com/page"),
+ resolve
+ )
+ );
+ return uri?.spec == "http://www.nostore.com/favicon.ico";
+ }, "wait for the favicon to be stored");
+ Assert.ok(await noStorePromise, "Should have received no-store header");
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_referer.js b/browser/base/content/test/favicons/browser_favicon_referer.js
new file mode 100644
index 0000000000..ad1cb5d9b1
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_referer.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const FOLDER = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+add_task(async function test_check_referrer_for_discovered_favicon() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let referrerPromise = TestUtils.topicObserved(
+ "http-on-modify-request",
+ (s, t, d) => {
+ let chan = s.QueryInterface(Ci.nsIHttpChannel);
+ return chan.URI.spec == "http://mochi.test:8888/favicon.ico";
+ }
+ ).then(([chan]) => chan.getRequestHeader("Referer"));
+
+ BrowserTestUtils.loadURIString(browser, `${FOLDER}discovery.html`);
+
+ let referrer = await referrerPromise;
+ is(
+ referrer,
+ `${FOLDER}discovery.html`,
+ "Should have sent referrer for autodiscovered favicon."
+ );
+ }
+ );
+});
+
+add_task(
+ async function test_check_referrer_for_referrerpolicy_explicit_favicon() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let referrerPromise = TestUtils.topicObserved(
+ "http-on-modify-request",
+ (s, t, d) => {
+ let chan = s.QueryInterface(Ci.nsIHttpChannel);
+ return chan.URI.spec == `${FOLDER}file_favicon.png`;
+ }
+ ).then(([chan]) => chan.getRequestHeader("Referer"));
+
+ BrowserTestUtils.loadURIString(
+ browser,
+ `${FOLDER}file_favicon_no_referrer.html`
+ );
+
+ let referrer = await referrerPromise;
+ is(
+ referrer,
+ "http://mochi.test:8888/",
+ "Should have sent the origin referrer only due to the per-link referrer policy specified."
+ );
+ }
+ );
+ }
+);
diff --git a/browser/base/content/test/favicons/browser_favicon_store.js b/browser/base/content/test/favicons/browser_favicon_store.js
new file mode 100644
index 0000000000..a183effe1a
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_store.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that favicons are stored.
+
+registerCleanupFunction(async () => {
+ Services.cache2.clear();
+ await PlacesTestUtils.clearFavicons();
+ await PlacesUtils.history.clear();
+});
+
+async function test_icon(pageUrl, iconUrl) {
+ let iconPromise = waitForFaviconMessage(true, iconUrl);
+ let storedIconPromise = PlacesTestUtils.waitForNotification(
+ "favicon-changed",
+ events => events.some(e => e.url == pageUrl)
+ );
+ await BrowserTestUtils.withNewTab(pageUrl, async () => {
+ let { iconURL } = await iconPromise;
+ Assert.equal(iconURL, iconUrl, "Should have seen the expected icon.");
+
+ // Ensure the favicon has been stored.
+ await storedIconPromise;
+ await new Promise((resolve, reject) => {
+ PlacesUtils.favicons.getFaviconURLForPage(
+ Services.io.newURI(pageUrl),
+ foundIconURI => {
+ if (foundIconURI) {
+ Assert.equal(
+ foundIconURI.spec,
+ iconUrl,
+ "Should have stored the expected icon."
+ );
+ resolve();
+ }
+ reject();
+ }
+ );
+ });
+ });
+}
+
+add_task(async function test_icon_stored() {
+ for (let [pageUrl, iconUrl] of [
+ [
+ "https://example.net/browser/browser/base/content/test/favicons/datauri-favicon.html",
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYhe3SIQ4AIBADwf7/04elBAtrVlSduGnSTDJ7cuT1PQJwwO+Hl7sAGAA07gjAAfgIBeAAoHFHAA7ARygABwCNOwJwAD5CATRgAYXh+kypw86nAAAAAElFTkSuQmCC",
+ ],
+ [
+ "https://example.net/browser/browser/base/content/test/favicons/file_favicon.html",
+ "https://example.net/browser/browser/base/content/test/favicons/file_favicon.png",
+ ],
+ ]) {
+ await test_icon(pageUrl, iconUrl);
+ }
+});
diff --git a/browser/base/content/test/favicons/browser_icon_discovery.js b/browser/base/content/test/favicons/browser_icon_discovery.js
new file mode 100644
index 0000000000..6dc57b9880
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_icon_discovery.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const ROOTURI =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+const ICON = "moz.png";
+const DATAURL =
+ "data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDcwADwyqYABAQEAAgICAAMDAwAERERABYWFgAcHBwAIiIiACkpKQBVVVUATU1NAEJCQgA5OTkAgHz/AFBQ/wCTANYA/+zMAMbW7wDW5+cAkKmtAAAAMwAAAGYAAACZAAAAzAAAMwAAADMzAAAzZgAAM5kAADPMAAAz/wAAZgAAAGYzAABmZgAAZpkAAGbMAABm/wAAmQAAAJkzAACZZgAAmZkAAJnMAACZ/wAAzAAAAMwzAADMZgAAzJkAAMzMAADM/wAA/2YAAP+ZAAD/zAAzAAAAMwAzADMAZgAzAJkAMwDMADMA/wAzMwAAMzMzADMzZgAzM5kAMzPMADMz/wAzZgAAM2YzADNmZgAzZpkAM2bMADNm/wAzmQAAM5kzADOZZgAzmZkAM5nMADOZ/wAzzAAAM8wzADPMZgAzzJkAM8zMADPM/wAz/zMAM/9mADP/mQAz/8wAM///AGYAAABmADMAZgBmAGYAmQBmAMwAZgD/AGYzAABmMzMAZjNmAGYzmQBmM8wAZjP/AGZmAABmZjMAZmZmAGZmmQBmZswAZpkAAGaZMwBmmWYAZpmZAGaZzABmmf8AZswAAGbMMwBmzJkAZszMAGbM/wBm/wAAZv8zAGb/mQBm/8wAzAD/AP8AzACZmQAAmTOZAJkAmQCZAMwAmQAAAJkzMwCZAGYAmTPMAJkA/wCZZgAAmWYzAJkzZgCZZpkAmWbMAJkz/wCZmTMAmZlmAJmZmQCZmcwAmZn/AJnMAACZzDMAZsxmAJnMmQCZzMwAmcz/AJn/AACZ/zMAmcxmAJn/mQCZ/8wAmf//AMwAAACZADMAzABmAMwAmQDMAMwAmTMAAMwzMwDMM2YAzDOZAMwzzADMM/8AzGYAAMxmMwCZZmYAzGaZAMxmzACZZv8AzJkAAMyZMwDMmWYAzJmZAMyZzADMmf8AzMwAAMzMMwDMzGYAzMyZAMzMzADMzP8AzP8AAMz/MwCZ/2YAzP+ZAMz/zADM//8AzAAzAP8AZgD/AJkAzDMAAP8zMwD/M2YA/zOZAP8zzAD/M/8A/2YAAP9mMwDMZmYA/2aZAP9mzADMZv8A/5kAAP+ZMwD/mWYA/5mZAP+ZzAD/mf8A/8wAAP/MMwD/zGYA/8yZAP/MzAD/zP8A//8zAMz/ZgD//5kA///MAGZm/wBm/2YAZv//AP9mZgD/Zv8A//9mACEApQBfX18Ad3d3AIaGhgCWlpYAy8vLALKysgDX19cA3d3dAOPj4wDq6uoA8fHxAPj4+ADw+/8ApKCgAICAgAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8ACgoKCgoKCgoKCgoKCgoKCgoKCgoHAQEMbQoKCgoKCgoAAAdDH/kgHRIAAAAAAAAAAADrHfn5ASQQAAAAAAAAAArsBx0B+fkgHesAAAAAAAD/Cgwf+fn5IA4dEus/IvcACgcMAfkg+QEB+SABHushbf8QHR/5HQH5+QEdHetEHx4K7B/5+QH5+fkdDBL5+SBE/wwdJfkf+fn5AR8g+fkfEArsCh/5+QEeJR/5+SAeBwAACgoe+SAlHwFAEhAfAAAAAPcKHh8eASYBHhAMAAAAAAAA9EMdIB8gHh0dBwAAAAAAAAAA7BAdQ+wHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AADwfwAAwH8AAMB/AAAAPwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAgAcAAIAPAADADwAA8D8AAP//AAA=";
+
+let iconDiscoveryTests = [
+ {
+ text: "rel icon discovered",
+ icons: [{}],
+ },
+ {
+ text: "rel may contain additional rels separated by spaces",
+ icons: [{ rel: "abcdefg icon qwerty" }],
+ },
+ {
+ text: "rel is case insensitive",
+ icons: [{ rel: "ICON" }],
+ },
+ {
+ text: "rel shortcut-icon not discovered",
+ expectedIcon: ROOTURI + ICON,
+ icons: [
+ // We will prefer the later icon if detected
+ {},
+ { rel: "shortcut-icon", href: "nothere.png" },
+ ],
+ },
+ {
+ text: "relative href works",
+ icons: [{ href: "moz.png" }],
+ },
+ {
+ text: "404'd icon is removed properly",
+ pass: false,
+ icons: [{ href: "notthere.png" }],
+ },
+ {
+ text: "data: URIs work",
+ icons: [{ href: DATAURL, type: "image/x-icon" }],
+ },
+ {
+ text: "type may have optional parameters (RFC2046)",
+ icons: [{ type: "image/png; charset=utf-8" }],
+ },
+ {
+ text: "apple-touch-icon discovered",
+ richIcon: true,
+ icons: [{ rel: "apple-touch-icon" }],
+ },
+ {
+ text: "apple-touch-icon-precomposed discovered",
+ richIcon: true,
+ icons: [{ rel: "apple-touch-icon-precomposed" }],
+ },
+ {
+ text: "fluid-icon discovered",
+ richIcon: true,
+ icons: [{ rel: "fluid-icon" }],
+ },
+ {
+ text: "unknown icon not discovered",
+ expectedIcon: ROOTURI + ICON,
+ richIcon: true,
+ icons: [
+ // We will prefer the larger icon if detected
+ { rel: "apple-touch-icon", sizes: "32x32" },
+ { rel: "unknown-icon", sizes: "128x128", href: "notthere.png" },
+ ],
+ },
+];
+
+add_task(async function () {
+ let url = ROOTURI + "discovery.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ for (let testCase of iconDiscoveryTests) {
+ info(`Running test "${testCase.text}"`);
+
+ if (testCase.pass === undefined) {
+ testCase.pass = true;
+ }
+
+ if (testCase.icons.length > 1 && !testCase.expectedIcon) {
+ ok(false, "Invalid test data, missing expectedIcon");
+ continue;
+ }
+
+ let expectedIcon = testCase.expectedIcon || testCase.icons[0].href || ICON;
+ expectedIcon = new URL(expectedIcon, ROOTURI).href;
+
+ let iconPromise = waitForFaviconMessage(!testCase.richIcon, expectedIcon);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[testCase.icons, ROOTURI + ICON]],
+ ([icons, defaultIcon]) => {
+ let doc = content.document;
+ let head = doc.head;
+
+ for (let icon of icons) {
+ let link = doc.createElement("link");
+ link.rel = icon.rel || "icon";
+ link.href = icon.href || defaultIcon;
+ link.type = icon.type || "image/png";
+ if (icon.sizes) {
+ link.sizes = icon.sizes;
+ }
+ head.appendChild(link);
+ }
+ }
+ );
+
+ try {
+ let { iconURL } = await iconPromise;
+ ok(testCase.pass, testCase.text);
+ is(iconURL, expectedIcon, "Should have seen the expected icon.");
+ } catch (e) {
+ ok(!testCase.pass, testCase.text);
+ }
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let links = content.document.querySelectorAll("link");
+ for (let link of links) {
+ link.remove();
+ }
+ });
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_invalid_href_fallback.js b/browser/base/content/test/favicons/browser_invalid_href_fallback.js
new file mode 100644
index 0000000000..d2a36b970d
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_invalid_href_fallback.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async () => {
+ const testPath =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/favicons/";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const expectedIcon = "http://example.com/favicon.ico";
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let faviconPromise = waitForLinkAvailable(browser);
+ BrowserTestUtils.loadURIString(
+ browser,
+ testPath + "file_invalid_href.html"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let iconURI = await faviconPromise;
+ Assert.equal(
+ iconURI,
+ expectedIcon,
+ "Should have fallen back to the default site favicon for an invalid href attribute"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_missing_favicon.js b/browser/base/content/test/favicons/browser_missing_favicon.js
new file mode 100644
index 0000000000..40dce7f7a9
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_missing_favicon.js
@@ -0,0 +1,36 @@
+add_task(async () => {
+ let testPath = getRootDirectory(gTestPath);
+
+ // The default favicon would interfere with this test.
+ Services.prefs.setBoolPref("browser.chrome.guess_favicon", false);
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref("browser.chrome.guess_favicon", true);
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ const expectedIcon = testPath + "file_generic_favicon.ico";
+ let faviconPromise = waitForLinkAvailable(browser);
+
+ BrowserTestUtils.loadURIString(
+ browser,
+ testPath + "file_with_favicon.html"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Got correct icon.");
+
+ BrowserTestUtils.loadURIString(browser, testPath + "blank.html");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ is(browser.mIconURL, null, "Should have blanked the icon.");
+ is(
+ gBrowser.getTabForBrowser(browser).getAttribute("image"),
+ "",
+ "Should have blanked the tab icon."
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_mixed_content.js b/browser/base/content/test/favicons/browser_mixed_content.js
new file mode 100644
index 0000000000..37bc86f12f
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_mixed_content.js
@@ -0,0 +1,26 @@
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.mixed_content.upgrade_display_content", false]],
+ });
+
+ const testPath =
+ "https://example.com/browser/browser/base/content/test/favicons/file_insecure_favicon.html";
+ const expectedIcon =
+ "http://example.com/browser/browser/base/content/test/favicons/file_favicon.png";
+
+ let tab = BrowserTestUtils.addTab(gBrowser, testPath);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+
+ let faviconPromise = waitForLinkAvailable(browser);
+ await BrowserTestUtils.browserLoaded(browser);
+ let iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Got correct icon.");
+
+ ok(
+ gIdentityHandler._isMixedPassiveContentLoaded,
+ "Should have seen mixed content."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_multiple_icons_in_short_timeframe.js b/browser/base/content/test/favicons/browser_multiple_icons_in_short_timeframe.js
new file mode 100644
index 0000000000..80a45a9288
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_multiple_icons_in_short_timeframe.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ const ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+ const URL = ROOT + "discovery.html";
+
+ let iconPromise = waitForFaviconMessage(
+ true,
+ "http://mochi.test:8888/favicon.ico"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let icon = await iconPromise;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [ROOT], root => {
+ let doc = content.document;
+ let head = doc.head;
+ let link = doc.createElement("link");
+ link.rel = "icon";
+ link.href = root + "rich_moz_1.png";
+ link.type = "image/png";
+ head.appendChild(link);
+ let link2 = link.cloneNode(false);
+ link2.href = root + "rich_moz_2.png";
+ head.appendChild(link2);
+ });
+
+ icon = await waitForFaviconMessage();
+ Assert.equal(
+ icon.iconURL,
+ ROOT + "rich_moz_2.png",
+ "The expected icon has been set"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_oversized.js b/browser/base/content/test/favicons/browser_oversized.js
new file mode 100644
index 0000000000..4756873a30
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_oversized.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let faviconPromise = waitForFaviconMessage(true, `${ROOT}large.png`);
+
+ BrowserTestUtils.loadURIString(browser, ROOT + "large_favicon.html");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await Assert.rejects(
+ faviconPromise,
+ result => {
+ return result.iconURL == `${ROOT}large.png`;
+ },
+ "Should have failed to load the large icon."
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/browser_preferred_icons.js b/browser/base/content/test/favicons/browser_preferred_icons.js
new file mode 100644
index 0000000000..25f548c717
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_preferred_icons.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+
+async function waitIcon(url) {
+ let icon = await waitForFaviconMessage(true, url);
+ is(icon.iconURL, url, "Should have seen the right icon.");
+}
+
+function createLinks(linkInfos) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [linkInfos], links => {
+ let doc = content.document;
+ let head = doc.head;
+ for (let l of links) {
+ let link = doc.createElement("link");
+ link.rel = "icon";
+ link.href = l.href;
+ if (l.type) {
+ link.type = l.type;
+ }
+ if (l.size) {
+ link.setAttribute("sizes", `${l.size}x${l.size}`);
+ }
+ head.appendChild(link);
+ }
+ });
+}
+
+add_setup(async function () {
+ const URL = ROOT + "discovery.html";
+ let iconPromise = waitIcon("http://mochi.test:8888/favicon.ico");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ await iconPromise;
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(async function prefer_svg() {
+ let promise = waitIcon(ROOT + "icon.svg");
+ await createLinks([
+ { href: ROOT + "icon.ico", type: "image/x-icon" },
+ { href: ROOT + "icon.svg", type: "image/svg+xml" },
+ {
+ href: ROOT + "icon.png",
+ type: "image/png",
+ size: 16 * Math.ceil(window.devicePixelRatio),
+ },
+ ]);
+ await promise;
+});
+
+add_task(async function prefer_sized() {
+ let promise = waitIcon(ROOT + "moz.png");
+ await createLinks([
+ { href: ROOT + "icon.ico", type: "image/x-icon" },
+ {
+ href: ROOT + "moz.png",
+ type: "image/png",
+ size: 16 * Math.ceil(window.devicePixelRatio),
+ },
+ { href: ROOT + "icon2.ico", type: "image/x-icon" },
+ ]);
+ await promise;
+});
+
+add_task(async function prefer_last_ico() {
+ let promise = waitIcon(ROOT + "file_generic_favicon.ico");
+ await createLinks([
+ { href: ROOT + "icon.ico", type: "image/x-icon" },
+ { href: ROOT + "icon.png", type: "image/png" },
+ { href: ROOT + "file_generic_favicon.ico", type: "image/x-icon" },
+ ]);
+ await promise;
+});
+
+add_task(async function fuzzy_ico() {
+ let promise = waitIcon(ROOT + "file_generic_favicon.ico");
+ await createLinks([
+ { href: ROOT + "icon.ico", type: "image/x-icon" },
+ { href: ROOT + "icon.png", type: "image/png" },
+ {
+ href: ROOT + "file_generic_favicon.ico",
+ type: "image/vnd.microsoft.icon",
+ },
+ ]);
+ await promise;
+});
+
+add_task(async function guess_svg() {
+ let promise = waitIcon(ROOT + "icon.svg");
+ await createLinks([
+ { href: ROOT + "icon.svg" },
+ {
+ href: ROOT + "icon.png",
+ type: "image/png",
+ size: 16 * Math.ceil(window.devicePixelRatio),
+ },
+ { href: ROOT + "icon.ico", type: "image/x-icon" },
+ ]);
+ await promise;
+});
+
+add_task(async function guess_ico() {
+ let promise = waitIcon(ROOT + "file_generic_favicon.ico");
+ await createLinks([
+ { href: ROOT + "file_generic_favicon.ico" },
+ { href: ROOT + "icon.png", type: "image/png" },
+ ]);
+ await promise;
+});
+
+add_task(async function guess_invalid() {
+ let promise = waitIcon(ROOT + "icon.svg");
+ // Create strange links to make sure they don't break us
+ await createLinks([
+ { href: ROOT + "icon.svg" },
+ { href: ROOT + "icon" },
+ { href: ROOT + "icon?.svg" },
+ { href: ROOT + "icon#.svg" },
+ { href: "data:text/plain,icon" },
+ { href: "file:///icon" },
+ { href: "about:icon" },
+ ]);
+ await promise;
+});
+
+add_task(async function guess_bestSized() {
+ let preferredWidth = 16 * Math.ceil(window.devicePixelRatio);
+ let promise = waitIcon(ROOT + "moz.png");
+ await createLinks([
+ { href: ROOT + "icon.png", type: "image/png", size: preferredWidth - 1 },
+ { href: ROOT + "icon2.png", type: "image/png" },
+ { href: ROOT + "moz.png", type: "image/png", size: preferredWidth + 1 },
+ { href: ROOT + "icon4.png", type: "image/png", size: preferredWidth + 2 },
+ ]);
+ await promise;
+});
diff --git a/browser/base/content/test/favicons/browser_redirect.js b/browser/base/content/test/favicons/browser_redirect.js
new file mode 100644
index 0000000000..ea2b053be7
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_redirect.js
@@ -0,0 +1,20 @@
+const ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+
+add_task(async () => {
+ const URL = ROOT + "file_favicon_redirect.html";
+ const EXPECTED_ICON = ROOT + "file_favicon_redirect.ico";
+
+ let promise = waitForFaviconMessage(true, EXPECTED_ICON);
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let tabIcon = await promise;
+
+ is(
+ tabIcon.iconURL,
+ EXPECTED_ICON,
+ "should use the redirected icon for the tab"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_rich_icons.js b/browser/base/content/test/favicons/browser_rich_icons.js
new file mode 100644
index 0000000000..2020b7bdad
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_rich_icons.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+
+add_task(async function test_richIcons() {
+ const URL = ROOT + "file_rich_icon.html";
+ const EXPECTED_ICON = ROOT + "moz.png";
+ const EXPECTED_RICH_ICON = ROOT + "rich_moz_2.png";
+
+ let tabPromises = Promise.all([
+ waitForFaviconMessage(true, EXPECTED_ICON),
+ waitForFaviconMessage(false, EXPECTED_RICH_ICON),
+ ]);
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let [tabIcon, richIcon] = await tabPromises;
+
+ is(
+ richIcon.iconURL,
+ EXPECTED_RICH_ICON,
+ "should choose the largest rich icon"
+ );
+ is(
+ tabIcon.iconURL,
+ EXPECTED_ICON,
+ "should use the non-rich icon for the tab"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_maskIcons() {
+ const URL = ROOT + "file_mask_icon.html";
+ const EXPECTED_ICON = "http://mochi.test:8888/favicon.ico";
+
+ let promise = waitForFaviconMessage(true, EXPECTED_ICON);
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let tabIcon = await promise;
+ is(
+ tabIcon.iconURL,
+ EXPECTED_ICON,
+ "should ignore the mask icons and load the root favicon"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_rooticon.js b/browser/base/content/test/favicons/browser_rooticon.js
new file mode 100644
index 0000000000..6e642070c7
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_rooticon.js
@@ -0,0 +1,24 @@
+add_task(async () => {
+ const testPath =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/favicons/blank.html";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const expectedIcon = "http://example.com/favicon.ico";
+
+ let tab = BrowserTestUtils.addTab(gBrowser, testPath);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+
+ let faviconPromise = waitForLinkAvailable(browser);
+ await BrowserTestUtils.browserLoaded(browser);
+ let iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Got correct initial icon.");
+
+ faviconPromise = waitForLinkAvailable(browser);
+ BrowserTestUtils.loadURIString(browser, testPath);
+ await BrowserTestUtils.browserLoaded(browser);
+ iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Got correct icon on second load.");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_subframe_favicons_not_used.js b/browser/base/content/test/favicons/browser_subframe_favicons_not_used.js
new file mode 100644
index 0000000000..ff48bcd475
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_subframe_favicons_not_used.js
@@ -0,0 +1,22 @@
+/* Make sure <link rel="..."> isn't respected in sub-frames. */
+
+add_task(async function () {
+ const ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+ const URL = ROOT + "file_bug970276_popup1.html";
+
+ let promiseIcon = waitForFaviconMessage(
+ true,
+ ROOT + "file_bug970276_favicon1.ico"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let icon = await promiseIcon;
+
+ Assert.equal(
+ icon.iconURL,
+ ROOT + "file_bug970276_favicon1.ico",
+ "The expected icon has been set"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/favicons/browser_title_flicker.js b/browser/base/content/test/favicons/browser_title_flicker.js
new file mode 100644
index 0000000000..71fadce908
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_title_flicker.js
@@ -0,0 +1,185 @@
+const TEST_PATH =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/favicons/";
+
+function waitForAttributeChange(tab, attr) {
+ info(`Waiting for attribute ${attr}`);
+ return new Promise(resolve => {
+ let listener = event => {
+ if (event.detail.changed.includes(attr)) {
+ tab.removeEventListener("TabAttrModified", listener);
+ resolve();
+ }
+ };
+
+ tab.addEventListener("TabAttrModified", listener);
+ });
+}
+
+function waitForPendingIcon() {
+ return new Promise(resolve => {
+ let listener = () => {
+ LinkHandlerParent.removeListenerForTests(listener);
+ resolve();
+ };
+
+ LinkHandlerParent.addListenerForTests(listener);
+ });
+}
+
+// Verify that the title doesn't flicker if the icon takes too long to load.
+// We expect to see events in the following order:
+// "label" added to tab
+// "busy" removed from tab
+// icon available
+// In all those cases the title should be in the same position.
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let tab = gBrowser.getTabForBrowser(browser);
+ BrowserTestUtils.loadURIString(
+ browser,
+ TEST_PATH + "file_with_slow_favicon.html"
+ );
+
+ await waitForAttributeChange(tab, "label");
+ ok(tab.hasAttribute("busy"), "Should have seen the busy attribute");
+ let label = tab.textLabel;
+ let bounds = label.getBoundingClientRect();
+
+ await waitForAttributeChange(tab, "busy");
+ ok(
+ !tab.hasAttribute("busy"),
+ "Should have seen the busy attribute removed"
+ );
+ let newBounds = label.getBoundingClientRect();
+ is(
+ bounds.x,
+ newBounds.left,
+ "Should have seen the title in the same place."
+ );
+
+ await waitForFaviconMessage(true);
+ newBounds = label.getBoundingClientRect();
+ is(
+ bounds.x,
+ newBounds.left,
+ "Should have seen the title in the same place."
+ );
+ }
+ );
+});
+
+// Verify that the title doesn't flicker if a new icon is detected after load.
+add_task(async () => {
+ let iconAvailable = waitForFaviconMessage(true);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TEST_PATH + "blank.html" },
+ async browser => {
+ let icon = await iconAvailable;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(icon.iconURL, "http://example.com/favicon.ico");
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ let label = tab.textLabel;
+ let bounds = label.getBoundingClientRect();
+
+ await SpecialPowers.spawn(browser, [], () => {
+ let link = content.document.createElement("link");
+ link.setAttribute("href", "file_favicon.png");
+ link.setAttribute("rel", "icon");
+ link.setAttribute("type", "image/png");
+ content.document.head.appendChild(link);
+ });
+
+ ok(
+ !tab.hasAttribute("pendingicon"),
+ "Should not have marked a pending icon"
+ );
+ let newBounds = label.getBoundingClientRect();
+ is(
+ bounds.x,
+ newBounds.left,
+ "Should have seen the title in the same place."
+ );
+
+ await waitForPendingIcon();
+
+ ok(
+ !tab.hasAttribute("pendingicon"),
+ "Should not have marked a pending icon"
+ );
+ newBounds = label.getBoundingClientRect();
+ is(
+ bounds.x,
+ newBounds.left,
+ "Should have seen the title in the same place."
+ );
+
+ icon = await waitForFaviconMessage(true);
+ is(
+ icon.iconURL,
+ TEST_PATH + "file_favicon.png",
+ "Should have loaded the new icon."
+ );
+
+ ok(
+ !tab.hasAttribute("pendingicon"),
+ "Should not have marked a pending icon"
+ );
+ newBounds = label.getBoundingClientRect();
+ is(
+ bounds.x,
+ newBounds.left,
+ "Should have seen the title in the same place."
+ );
+ }
+ );
+});
+
+// Verify that pinned tabs don't change size when an icon is pending.
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let tab = gBrowser.getTabForBrowser(browser);
+ gBrowser.pinTab(tab);
+
+ let bounds = tab.getBoundingClientRect();
+ BrowserTestUtils.loadURIString(
+ browser,
+ TEST_PATH + "file_with_slow_favicon.html"
+ );
+
+ await waitForAttributeChange(tab, "label");
+ ok(tab.hasAttribute("busy"), "Should have seen the busy attribute");
+ let newBounds = tab.getBoundingClientRect();
+ is(
+ bounds.width,
+ newBounds.width,
+ "Should have seen tab remain the same size."
+ );
+
+ await waitForAttributeChange(tab, "busy");
+ ok(
+ !tab.hasAttribute("busy"),
+ "Should have seen the busy attribute removed"
+ );
+ newBounds = tab.getBoundingClientRect();
+ is(
+ bounds.width,
+ newBounds.width,
+ "Should have seen tab remain the same size."
+ );
+
+ await waitForFaviconMessage(true);
+ newBounds = tab.getBoundingClientRect();
+ is(
+ bounds.width,
+ newBounds.width,
+ "Should have seen tab remain the same size."
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/cookie_favicon.html b/browser/base/content/test/favicons/cookie_favicon.html
new file mode 100644
index 0000000000..618ac1850b
--- /dev/null
+++ b/browser/base/content/test/favicons/cookie_favicon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for caching</title>
+ <link rel="icon" type="image/png" href="cookie_favicon.sjs" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/cookie_favicon.sjs b/browser/base/content/test/favicons/cookie_favicon.sjs
new file mode 100644
index 0000000000..a00d48d09a
--- /dev/null
+++ b/browser/base/content/test/favicons/cookie_favicon.sjs
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+ if (request.queryString == "reset") {
+ setState("cache_cookie", "0");
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+ response.write("Reset");
+ return;
+ }
+
+ let state = getState("cache_cookie");
+ if (!state) {
+ state = 0;
+ }
+
+ response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+ response.setHeader("Set-Cookie", `faviconCookie=${++state}`);
+ response.setHeader(
+ "Location",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/favicons/moz.png"
+ );
+ setState("cache_cookie", `${state}`);
+}
diff --git a/browser/base/content/test/favicons/credentials.png b/browser/base/content/test/favicons/credentials.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/base/content/test/favicons/credentials.png
Binary files differ
diff --git a/browser/base/content/test/favicons/credentials.png^headers^ b/browser/base/content/test/favicons/credentials.png^headers^
new file mode 100644
index 0000000000..72339d67f0
--- /dev/null
+++ b/browser/base/content/test/favicons/credentials.png^headers^
@@ -0,0 +1,3 @@
+Access-Control-Allow-Origin: https://example.net
+Access-Control-Allow-Credentials: true
+Set-Cookie: faviconCookie2=test; SameSite=None; Secure;
diff --git a/browser/base/content/test/favicons/credentials1.html b/browser/base/content/test/favicons/credentials1.html
new file mode 100644
index 0000000000..2ccfd00e79
--- /dev/null
+++ b/browser/base/content/test/favicons/credentials1.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8" />
+ <title>Favicon test for cross-origin credentials</title>
+ <link rel="icon" href="https://example.com/browser/browser/base/content/test/favicons/credentials.png" crossorigin />
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/credentials2.html b/browser/base/content/test/favicons/credentials2.html
new file mode 100644
index 0000000000..cc28ca77bd
--- /dev/null
+++ b/browser/base/content/test/favicons/credentials2.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8" />
+ <title>Favicon test for cross-origin credentials</title>
+ <link rel="icon" href="https://example.com/browser/browser/base/content/test/favicons/credentials.png" crossorigin="use-credentials" />
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/crossorigin.html b/browser/base/content/test/favicons/crossorigin.html
new file mode 100644
index 0000000000..26a6a85d17
--- /dev/null
+++ b/browser/base/content/test/favicons/crossorigin.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8" />
+ <title>Favicon test for the crossorigin attribute</title>
+ <link rel="icon" href="http://example.com/browser/browser/base/content/test/favicons/crossorigin.png" crossorigin />
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/crossorigin.png b/browser/base/content/test/favicons/crossorigin.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/base/content/test/favicons/crossorigin.png
Binary files differ
diff --git a/browser/base/content/test/favicons/crossorigin.png^headers^ b/browser/base/content/test/favicons/crossorigin.png^headers^
new file mode 100644
index 0000000000..3a6a85d894
--- /dev/null
+++ b/browser/base/content/test/favicons/crossorigin.png^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
diff --git a/browser/base/content/test/favicons/datauri-favicon.html b/browser/base/content/test/favicons/datauri-favicon.html
new file mode 100644
index 0000000000..35954f67a1
--- /dev/null
+++ b/browser/base/content/test/favicons/datauri-favicon.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Favicon tab</title>
+ <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYhe3SIQ4AIBADwf7/04elBAtrVlSduGnSTDJ7cuT1PQJwwO+Hl7sAGAA07gjAAfgIBeAAoHFHAA7ARygABwCNOwJwAD5CATRgAYXh+kypw86nAAAAAElFTkSuQmCC">
+ <head>
+ <body>Some page with a favicon</body>
+</html>
diff --git a/browser/base/content/test/favicons/discovery.html b/browser/base/content/test/favicons/discovery.html
new file mode 100644
index 0000000000..2ff2aaa5f2
--- /dev/null
+++ b/browser/base/content/test/favicons/discovery.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Autodiscovery Test</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_bug970276_favicon1.ico b/browser/base/content/test/favicons/file_bug970276_favicon1.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_favicon1.ico
Binary files differ
diff --git a/browser/base/content/test/favicons/file_bug970276_favicon2.ico b/browser/base/content/test/favicons/file_bug970276_favicon2.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_favicon2.ico
Binary files differ
diff --git a/browser/base/content/test/favicons/file_bug970276_popup1.html b/browser/base/content/test/favicons/file_bug970276_popup1.html
new file mode 100644
index 0000000000..5ce7dab879
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_popup1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bug 970276.</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_bug970276_favicon1.ico">
+</head>
+<body>
+ Test file for bug 970276.
+
+ <iframe src="file_bug970276_popup2.html">
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_bug970276_popup2.html b/browser/base/content/test/favicons/file_bug970276_popup2.html
new file mode 100644
index 0000000000..0b9e5294ef
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_popup2.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bug 970276.</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_bug970276_favicon2.ico">
+</head>
+<body>
+ Test inner file for bug 970276.
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon.html b/browser/base/content/test/favicons/file_favicon.html
new file mode 100644
index 0000000000..f294b47758
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for originAttributes</title>
+ <link rel="icon" type="image/png" href="file_favicon.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon.png b/browser/base/content/test/favicons/file_favicon.png
new file mode 100644
index 0000000000..5535363c94
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon.png
Binary files differ
diff --git a/browser/base/content/test/favicons/file_favicon.png^headers^ b/browser/base/content/test/favicons/file_favicon.png^headers^
new file mode 100644
index 0000000000..9e23c73b7f
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon.png^headers^
@@ -0,0 +1 @@
+Cache-Control: no-cache
diff --git a/browser/base/content/test/favicons/file_favicon_change.html b/browser/base/content/test/favicons/file_favicon_change.html
new file mode 100644
index 0000000000..035549c5aa
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_change.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="file_bug970276_favicon1.ico" type="image/ico" id="i">
+</head>
+<body>
+ <script>
+ window.addEventListener("PleaseChangeFavicon", function() {
+ var ico = document.getElementById("i");
+ ico.setAttribute("href", "moz.png");
+ });
+ </script>
+</body></html>
diff --git a/browser/base/content/test/favicons/file_favicon_change_not_in_document.html b/browser/base/content/test/favicons/file_favicon_change_not_in_document.html
new file mode 100644
index 0000000000..c44a2f8153
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_change_not_in_document.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="file_bug970276_favicon1.ico" type="image/ico" id="i">
+</head>
+<body onload="onload()">
+ <script>
+ function onload() {
+ var ico = document.createElement("link");
+ ico.setAttribute("rel", "icon");
+ ico.setAttribute("type", "image/ico");
+ ico.setAttribute("href", "file_bug970276_favicon1.ico");
+ setTimeout(function() {
+ ico.setAttribute("href", "file_generic_favicon.ico");
+ document.getElementById("i").remove();
+ document.head.appendChild(ico);
+ }, 1000);
+ }
+ </script>
+</body></html>
diff --git a/browser/base/content/test/favicons/file_favicon_no_referrer.html b/browser/base/content/test/favicons/file_favicon_no_referrer.html
new file mode 100644
index 0000000000..4f363ffd04
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_no_referrer.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for referrer</title>
+ <link rel="icon" type="image/png" referrerpolicy="origin" href="file_favicon.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon_redirect.html b/browser/base/content/test/favicons/file_favicon_redirect.html
new file mode 100644
index 0000000000..9da4777591
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_redirect.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file with an icon that redirects</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_favicon_redirect.ico">
+</head>
+<body>
+ Test file for bugs with favicons
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon_redirect.ico b/browser/base/content/test/favicons/file_favicon_redirect.ico
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_redirect.ico
diff --git a/browser/base/content/test/favicons/file_favicon_redirect.ico^headers^ b/browser/base/content/test/favicons/file_favicon_redirect.ico^headers^
new file mode 100644
index 0000000000..380fa3d3a4
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_redirect.ico^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Found
+Location: http://example.com/browser/browser/base/content/test/favicons/file_generic_favicon.ico
diff --git a/browser/base/content/test/favicons/file_favicon_thirdParty.html b/browser/base/content/test/favicons/file_favicon_thirdParty.html
new file mode 100644
index 0000000000..7d690e5981
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_thirdParty.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for originAttributes</title>
+ <link rel="icon" type="image/png" href="http://mochi.test:8888/browser/browser/base/content/test/favicons/file_favicon.png" />
+ </head>
+ <body>
+ Third Party Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_generic_favicon.ico b/browser/base/content/test/favicons/file_generic_favicon.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/browser/base/content/test/favicons/file_generic_favicon.ico
Binary files differ
diff --git a/browser/base/content/test/favicons/file_insecure_favicon.html b/browser/base/content/test/favicons/file_insecure_favicon.html
new file mode 100644
index 0000000000..7b13b47829
--- /dev/null
+++ b/browser/base/content/test/favicons/file_insecure_favicon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for mixed content</title>
+ <link rel="icon" type="image/png" href="http://example.com/browser/browser/base/content/test/favicons/file_favicon.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_invalid_href.html b/browser/base/content/test/favicons/file_invalid_href.html
new file mode 100644
index 0000000000..087ff01403
--- /dev/null
+++ b/browser/base/content/test/favicons/file_invalid_href.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bugs with invalid hrefs for favicons</title>
+
+ <!--Empty href; that's the whole point of this file.-->
+ <link rel="icon" href="">
+</head>
+<body>
+ Test file for bugs with invalid hrefs for favicons
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_mask_icon.html b/browser/base/content/test/favicons/file_mask_icon.html
new file mode 100644
index 0000000000..5bcd9e694f
--- /dev/null
+++ b/browser/base/content/test/favicons/file_mask_icon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Mask Icon</title>
+ <link rel="icon" mask href="moz.png" type="image/png" />
+ <link rel="mask-icon" href="moz.png" type="image/png" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_rich_icon.html b/browser/base/content/test/favicons/file_rich_icon.html
new file mode 100644
index 0000000000..ce7550b611
--- /dev/null
+++ b/browser/base/content/test/favicons/file_rich_icon.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Rich Icons</title>
+ <link rel="icon" href="moz.png" type="image/png" />
+ <link rel="apple-touch-icon" sizes="96x96" href="rich_moz_1.png" type="image/png" />
+ <link rel="apple-touch-icon" sizes="256x256" href="rich_moz_2.png" type="image/png" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_with_favicon.html b/browser/base/content/test/favicons/file_with_favicon.html
new file mode 100644
index 0000000000..0702b4aaba
--- /dev/null
+++ b/browser/base/content/test/favicons/file_with_favicon.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bugs with favicons</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_generic_favicon.ico">
+</head>
+<body>
+ Test file for bugs with favicons
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_with_slow_favicon.html b/browser/base/content/test/favicons/file_with_slow_favicon.html
new file mode 100644
index 0000000000..76fb015587
--- /dev/null
+++ b/browser/base/content/test/favicons/file_with_slow_favicon.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for title flicker</title>
+</head>
+<body>
+ <!-- Putting the icon down here means we won't start loading it until the doc is fully parsed -->
+ <link rel="icon" href="file_generic_favicon.ico">
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/head.js b/browser/base/content/test/favicons/head.js
new file mode 100644
index 0000000000..ce16afd33f
--- /dev/null
+++ b/browser/base/content/test/favicons/head.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ LinkHandlerParent: "resource:///actors/LinkHandlerParent.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+
+ XPCShellContentUtils:
+ "resource://testing-common/XPCShellContentUtils.sys.mjs",
+});
+
+// Clear the network cache between every test to make sure we get a stable state
+Services.cache2.clear();
+
+function waitForFaviconMessage(isTabIcon = undefined, expectedURL = undefined) {
+ return new Promise((resolve, reject) => {
+ let listener = (name, data) => {
+ if (name != "SetIcon" && name != "SetFailedIcon") {
+ return; // Ignore unhandled messages
+ }
+
+ // If requested filter out loads of the wrong kind of icon.
+ if (isTabIcon != undefined && isTabIcon != data.canUseForTab) {
+ return;
+ }
+
+ if (expectedURL && data.originalURL != expectedURL) {
+ return;
+ }
+
+ LinkHandlerParent.removeListenerForTests(listener);
+
+ if (name == "SetIcon") {
+ resolve({
+ iconURL: data.originalURL,
+ dataURL: data.iconURL,
+ canUseForTab: data.canUseForTab,
+ });
+ } else {
+ reject({
+ iconURL: data.originalURL,
+ canUseForTab: data.canUseForTab,
+ });
+ }
+ };
+
+ LinkHandlerParent.addListenerForTests(listener);
+ });
+}
+
+function waitForFavicon(browser, url) {
+ return new Promise(resolve => {
+ let listener = {
+ onLinkIconAvailable(b, dataURI, iconURI) {
+ if (b !== browser || iconURI != url) {
+ return;
+ }
+
+ gBrowser.removeTabsProgressListener(listener);
+ resolve();
+ },
+ };
+
+ gBrowser.addTabsProgressListener(listener);
+ });
+}
+
+function waitForLinkAvailable(browser) {
+ let resolve, reject;
+
+ let listener = {
+ onLinkIconAvailable(b, dataURI, iconURI) {
+ // Ignore icons for other browsers or empty icons.
+ if (browser !== b || !iconURI) {
+ return;
+ }
+
+ gBrowser.removeTabsProgressListener(listener);
+ resolve(iconURI);
+ },
+ };
+
+ let promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+
+ gBrowser.addTabsProgressListener(listener);
+ });
+
+ promise.cancel = () => {
+ gBrowser.removeTabsProgressListener(listener);
+
+ reject();
+ };
+
+ return promise;
+}
diff --git a/browser/base/content/test/favicons/icon.svg b/browser/base/content/test/favicons/icon.svg
new file mode 100644
index 0000000000..6de9c64503
--- /dev/null
+++ b/browser/base/content/test/favicons/icon.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <circle cx="8" cy="8" r="8" fill="#8d20ae" />
+ <circle cx="8" cy="8" r="7.5" stroke="#7b149a" stroke-width="1" fill="none" />
+ <path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" stroke="#670c83" stroke-width="2" fill="none" />
+ <path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" fill="#fff" />
+</svg>
diff --git a/browser/base/content/test/favicons/large.png b/browser/base/content/test/favicons/large.png
new file mode 100644
index 0000000000..37012cf965
--- /dev/null
+++ b/browser/base/content/test/favicons/large.png
Binary files differ
diff --git a/browser/base/content/test/favicons/large_favicon.html b/browser/base/content/test/favicons/large_favicon.html
new file mode 100644
index 0000000000..48c5e8f19d
--- /dev/null
+++ b/browser/base/content/test/favicons/large_favicon.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bugs with favicons</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="large.png">
+</head>
+<body>
+ Test file for bugs with favicons
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/moz.png b/browser/base/content/test/favicons/moz.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/base/content/test/favicons/moz.png
Binary files differ
diff --git a/browser/base/content/test/favicons/no-store.html b/browser/base/content/test/favicons/no-store.html
new file mode 100644
index 0000000000..0d5bbbb475
--- /dev/null
+++ b/browser/base/content/test/favicons/no-store.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for Cache-Control: no-store</title>
+ <link rel="icon" type="image/png" href="no-store.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/no-store.png b/browser/base/content/test/favicons/no-store.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/base/content/test/favicons/no-store.png
Binary files differ
diff --git a/browser/base/content/test/favicons/no-store.png^headers^ b/browser/base/content/test/favicons/no-store.png^headers^
new file mode 100644
index 0000000000..15a2442249
--- /dev/null
+++ b/browser/base/content/test/favicons/no-store.png^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store, no-cache, must-revalidate
diff --git a/browser/base/content/test/favicons/rich_moz_1.png b/browser/base/content/test/favicons/rich_moz_1.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/base/content/test/favicons/rich_moz_1.png
Binary files differ
diff --git a/browser/base/content/test/favicons/rich_moz_2.png b/browser/base/content/test/favicons/rich_moz_2.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/base/content/test/favicons/rich_moz_2.png
Binary files differ
diff --git a/browser/base/content/test/forms/browser.ini b/browser/base/content/test/forms/browser.ini
new file mode 100644
index 0000000000..00c6cfe951
--- /dev/null
+++ b/browser/base/content/test/forms/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+prefs =
+ gfx.font_loader.delay=0
+support-files =
+ head.js
+
+[browser_selectpopup.js]
+skip-if =
+ os == "linux" # Bug 1329991
+ os == "mac" # Bug 1661132, 1775896
+ verify && os == "win"
+[browser_selectpopup_colors.js]
+skip-if = os == "linux" # Bug 1329991 - test fails intermittently on Linux builds
+[browser_selectpopup_dir.js]
+[browser_selectpopup_large.js]
+[browser_selectpopup_searchfocus.js]
+[browser_selectpopup_text_transform.js]
+[browser_selectpopup_toplevel.js]
+[browser_selectpopup_user_input.js]
+[browser_selectpopup_width.js]
+[browser_selectpopup_xhtml.js]
diff --git a/browser/base/content/test/forms/browser_selectpopup.js b/browser/base/content/test/forms/browser_selectpopup.js
new file mode 100644
index 0000000000..7645f89b4a
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -0,0 +1,913 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// This test tests <select> in a child process. This is different than
+// single-process as a <menulist> is used to implement the dropdown list.
+
+// FIXME(bug 1774835): This test should be split.
+requestLongerTimeout(2);
+
+const XHTML_DTD =
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
+
+const PAGECONTENT =
+ "<html xmlns='http://www.w3.org/1999/xhtml'>" +
+ "<body onload='gChangeEvents = 0;gInputEvents = 0; gClickEvents = 0; document.getElementById(\"select\").focus();'>" +
+ "<select id='select' oninput='gInputEvents++' onchange='gChangeEvents++' onclick='if (event.target == this) gClickEvents++'>" +
+ " <optgroup label='First Group'>" +
+ " <option value='One'>One</option>" +
+ " <option value='Two'>Two</option>" +
+ " </optgroup>" +
+ " <option value='Three'>Three</option>" +
+ " <optgroup label='Second Group' disabled='true'>" +
+ " <option value='Four'>Four</option>" +
+ " <option value='Five'>Five</option>" +
+ " </optgroup>" +
+ " <option value='Six' disabled='true'>Six</option>" +
+ " <optgroup label='Third Group'>" +
+ " <option value='Seven'> Seven </option>" +
+ " <option value='Eight'>&nbsp;&nbsp;Eight&nbsp;&nbsp;</option>" +
+ " </optgroup></select><input />Text" +
+ "</body></html>";
+
+const PAGECONTENT_XSLT =
+ "<?xml-stylesheet type='text/xml' href='#style1'?>" +
+ "<xsl:stylesheet id='style1'" +
+ " version='1.0'" +
+ " xmlns:xsl='http://www.w3.org/1999/XSL/Transform'" +
+ " xmlns:html='http://www.w3.org/1999/xhtml'>" +
+ "<xsl:template match='xsl:stylesheet'>" +
+ PAGECONTENT +
+ "</xsl:template>" +
+ "</xsl:stylesheet>";
+
+const PAGECONTENT_SMALL =
+ "<html>" +
+ "<body><select id='one'>" +
+ " <option value='One'>One</option>" +
+ " <option value='Two'>Two</option>" +
+ "</select><select id='two'>" +
+ " <option value='Three'>Three</option>" +
+ " <option value='Four'>Four</option>" +
+ "</select><select id='three'>" +
+ " <option value='Five'>Five</option>" +
+ " <option value='Six'>Six</option>" +
+ "</select></body></html>";
+
+const PAGECONTENT_GROUPS =
+ "<html>" +
+ "<body><select id='one'>" +
+ " <optgroup label='Group 1'>" +
+ " <option value='G1 O1'>G1 O1</option>" +
+ " <option value='G1 O2'>G1 O2</option>" +
+ " <option value='G1 O3'>G1 O3</option>" +
+ " </optgroup>" +
+ " <optgroup label='Group 2'>" +
+ " <option value='G2 O1'>G2 O4</option>" +
+ " <option value='G2 O2'>G2 O5</option>" +
+ " <option value='Hidden' style='display: none;'>Hidden</option>" +
+ " </optgroup>" +
+ "</select></body></html>";
+
+const PAGECONTENT_SOMEHIDDEN =
+ "<html><head><style>.hidden { display: none; }</style></head>" +
+ "<body><select id='one'>" +
+ " <option value='One' style='display: none;'>OneHidden</option>" +
+ " <option value='Two' class='hidden'>TwoHidden</option>" +
+ " <option value='Three'>ThreeVisible</option>" +
+ " <option value='Four'style='display: table;'>FourVisible</option>" +
+ " <option value='Five'>FiveVisible</option>" +
+ " <optgroup label='GroupHidden' class='hidden'>" +
+ " <option value='Four'>Six.OneHidden</option>" +
+ " <option value='Five' style='display: block;'>Six.TwoHidden</option>" +
+ " </optgroup>" +
+ " <option value='Six' class='hidden' style='display: block;'>SevenVisible</option>" +
+ "</select></body></html>";
+
+const PAGECONTENT_TRANSLATED =
+ "<html><body>" +
+ "<div id='div'>" +
+ "<iframe id='frame' width='320' height='295' style='border: none;'" +
+ " src='data:text/html,<select id=select><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'" +
+ "</iframe>" +
+ "</div></body></html>";
+
+function getInputEvents() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.gInputEvents;
+ });
+}
+
+function getChangeEvents() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.gChangeEvents;
+ });
+}
+
+function getClickEvents() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.gClickEvents;
+ });
+}
+
+async function doSelectTests(contentType, content) {
+ const pageUrl = "data:" + contentType + "," + encodeURIComponent(content);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let selectPopup = await openSelectPopup();
+ let menulist = selectPopup.parentNode;
+
+ let isWindows = navigator.platform.includes("Win");
+
+ is(menulist.selectedIndex, 1, "Initial selection");
+ is(
+ selectPopup.firstElementChild.localName,
+ "menucaption",
+ "optgroup is caption"
+ );
+ is(
+ selectPopup.firstElementChild.getAttribute("label"),
+ "First Group",
+ "optgroup label"
+ );
+ is(selectPopup.children[1].localName, "menuitem", "option is menuitem");
+ is(selectPopup.children[1].getAttribute("label"), "One", "option label");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(menulist.activeChild, menulist.getItemAtIndex(2), "Select item 2");
+ is(menulist.selectedIndex, isWindows ? 2 : 1, "Select item 2 selectedIndex");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(menulist.activeChild, menulist.getItemAtIndex(3), "Select item 3");
+ is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // On Windows, one can navigate on disabled menuitems
+ is(
+ menulist.activeChild,
+ menulist.getItemAtIndex(9),
+ "Skip optgroup header and disabled items select item 7"
+ );
+ is(
+ menulist.selectedIndex,
+ isWindows ? 9 : 1,
+ "Select or skip disabled item selectedIndex"
+ );
+
+ for (let i = 0; i < 10; i++) {
+ is(
+ menulist.getItemAtIndex(i).disabled,
+ i >= 4 && i <= 7,
+ "item " + i + " disabled"
+ );
+ }
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(menulist.activeChild, menulist.getItemAtIndex(3), "Select item 3 again");
+ is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex");
+
+ is(await getInputEvents(), 0, "Before closed - number of input events");
+ is(await getChangeEvents(), 0, "Before closed - number of change events");
+ is(await getClickEvents(), 0, "Before closed - number of click events");
+
+ EventUtils.synthesizeKey("a", { accelKey: true });
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ isWindows }],
+ function (args) {
+ Assert.equal(
+ String(content.getSelection()),
+ args.isWindows ? "Text" : "",
+ "Select all while popup is open"
+ );
+ }
+ );
+
+ // Backspace should not go back
+ let handleKeyPress = function (event) {
+ ok(false, "Should not get keypress event");
+ };
+ window.addEventListener("keypress", handleKeyPress);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ window.removeEventListener("keypress", handleKeyPress);
+
+ await hideSelectPopup();
+
+ is(menulist.selectedIndex, 3, "Item 3 still selected");
+ is(await getInputEvents(), 1, "After closed - number of input events");
+ is(await getChangeEvents(), 1, "After closed - number of change events");
+ is(await getClickEvents(), 0, "After closed - number of click events");
+
+ // Opening and closing the popup without changing the value should not fire a change event.
+ await openSelectPopup("click");
+ await hideSelectPopup("escape");
+ is(
+ await getInputEvents(),
+ 1,
+ "Open and close with no change - number of input events"
+ );
+ is(
+ await getChangeEvents(),
+ 1,
+ "Open and close with no change - number of change events"
+ );
+ is(
+ await getClickEvents(),
+ 1,
+ "Open and close with no change - number of click events"
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ is(
+ await getInputEvents(),
+ 1,
+ "Tab away from select with no change - number of input events"
+ );
+ is(
+ await getChangeEvents(),
+ 1,
+ "Tab away from select with no change - number of change events"
+ );
+ is(
+ await getClickEvents(),
+ 1,
+ "Tab away from select with no change - number of click events"
+ );
+
+ await openSelectPopup("click");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup("escape");
+ is(
+ await getInputEvents(),
+ isWindows ? 2 : 1,
+ "Open and close with change - number of input events"
+ );
+ is(
+ await getChangeEvents(),
+ isWindows ? 2 : 1,
+ "Open and close with change - number of change events"
+ );
+ is(
+ await getClickEvents(),
+ 2,
+ "Open and close with change - number of click events"
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ is(
+ await getInputEvents(),
+ isWindows ? 2 : 1,
+ "Tab away from select with change - number of input events"
+ );
+ is(
+ await getChangeEvents(),
+ isWindows ? 2 : 1,
+ "Tab away from select with change - number of change events"
+ );
+ is(
+ await getClickEvents(),
+ 2,
+ "Tab away from select with change - number of click events"
+ );
+
+ is(
+ selectPopup.lastElementChild.previousElementSibling.label,
+ "Seven",
+ "Spaces collapsed"
+ );
+ is(
+ selectPopup.lastElementChild.label,
+ "\xA0\xA0Eight\xA0\xA0",
+ "Non-breaking spaces not collapsed"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.select.customstyling", true]],
+ });
+});
+
+add_task(async function () {
+ await doSelectTests("text/html", PAGECONTENT);
+});
+
+add_task(async function () {
+ await doSelectTests("application/xhtml+xml", XHTML_DTD + "\n" + PAGECONTENT);
+});
+
+add_task(async function () {
+ await doSelectTests("application/xml", XHTML_DTD + "\n" + PAGECONTENT_XSLT);
+});
+
+// This test opens a select popup and removes the content node of a popup while
+// The popup should close if its node is removed.
+add_task(async function () {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ // First, try it when a different <select> element than the one that is open is removed
+ const selectPopup = await openSelectPopup("click", "#one");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.document.body.removeChild(content.document.getElementById("two"));
+ });
+
+ // Wait a bit just to make sure the popup won't close.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ is(selectPopup.state, "open", "Different popup did not affect open popup");
+
+ await hideSelectPopup();
+
+ // Next, try it when the same <select> element than the one that is open is removed
+ await openSelectPopup("click", "#three");
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.document.body.removeChild(content.document.getElementById("three"));
+ });
+ await popupHiddenPromise;
+
+ ok(true, "Popup hidden when select is removed");
+
+ // Finally, try it when the tab is closed while the select popup is open.
+ await openSelectPopup("click", "#one");
+
+ popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+ BrowserTestUtils.removeTab(tab);
+ await popupHiddenPromise;
+
+ ok(true, "Popup hidden when tab is closed");
+});
+
+// This test opens a select popup that is isn't a frame and has some translations applied.
+add_task(async function () {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_TRANSLATED);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ // We need to explicitly call Element.focus() since dataURL is treated as
+ // cross-origin, thus autofocus doesn't work there.
+ const iframe = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.document.querySelector("iframe").browsingContext;
+ });
+ await SpecialPowers.spawn(iframe, [], async () => {
+ const input = content.document.getElementById("select");
+ const focusPromise = new Promise(resolve => {
+ input.addEventListener("focus", resolve, { once: true });
+ });
+ input.focus();
+ await focusPromise;
+ });
+
+ // First, get the position of the select popup when no translations have been applied.
+ const selectPopup = await openSelectPopup();
+
+ let rect = selectPopup.getBoundingClientRect();
+ let expectedX = rect.left;
+ let expectedY = rect.top;
+
+ await hideSelectPopup();
+
+ // Iterate through a set of steps which each add more translation to the select's expected position.
+ let steps = [
+ ["div", "transform: translateX(7px) translateY(13px);", 7, 13],
+ [
+ "frame",
+ "border-top: 5px solid green; border-left: 10px solid red; border-right: 35px solid blue;",
+ 10,
+ 5,
+ ],
+ [
+ "frame",
+ "border: none; padding-left: 6px; padding-right: 12px; padding-top: 2px;",
+ -4,
+ -3,
+ ],
+ ["select", "margin: 9px; transform: translateY(-3px);", 9, 6],
+ ];
+
+ for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
+ let step = steps[stepIndex];
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [step],
+ async function (contentStep) {
+ return new Promise(resolve => {
+ let changedWin = content;
+
+ let elem;
+ if (contentStep[0] == "select") {
+ changedWin = content.document.getElementById("frame").contentWindow;
+ elem = changedWin.document.getElementById("select");
+ } else {
+ elem = content.document.getElementById(contentStep[0]);
+ }
+
+ changedWin.addEventListener(
+ "MozAfterPaint",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+
+ elem.style = contentStep[1];
+ elem.getBoundingClientRect();
+ });
+ }
+ );
+
+ await openSelectPopup();
+
+ expectedX += step[2];
+ expectedY += step[3];
+
+ let popupRect = selectPopup.getBoundingClientRect();
+ is(popupRect.left, expectedX, "step " + (stepIndex + 1) + " x");
+ is(popupRect.top, expectedY, "step " + (stepIndex + 1) + " y");
+
+ await hideSelectPopup();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test that we get the right events when a select popup is changed.
+add_task(async function test_event_order() {
+ const URL = "data:text/html," + escape(PAGECONTENT_SMALL);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: URL,
+ },
+ async function (browser) {
+ // According to https://html.spec.whatwg.org/#the-select-element,
+ // we want to fire input, change, and then click events on the
+ // <select> (in that order) when it has changed.
+ let expectedEnter = [
+ {
+ type: "input",
+ cancelable: false,
+ targetIsOption: false,
+ composed: true,
+ },
+ {
+ type: "change",
+ cancelable: false,
+ targetIsOption: false,
+ composed: false,
+ },
+ ];
+
+ let expectedClick = [
+ {
+ type: "mousedown",
+ cancelable: true,
+ targetIsOption: true,
+ composed: true,
+ },
+ {
+ type: "mouseup",
+ cancelable: true,
+ targetIsOption: true,
+ composed: true,
+ },
+ {
+ type: "input",
+ cancelable: false,
+ targetIsOption: false,
+ composed: true,
+ },
+ {
+ type: "change",
+ cancelable: false,
+ targetIsOption: false,
+ composed: false,
+ },
+ {
+ type: "click",
+ cancelable: true,
+ targetIsOption: true,
+ composed: true,
+ },
+ ];
+
+ for (let mode of ["enter", "click"]) {
+ let expected = mode == "enter" ? expectedEnter : expectedClick;
+ await openSelectPopup("click", mode == "enter" ? "#one" : "#two");
+
+ let eventsPromise = SpecialPowers.spawn(
+ browser,
+ [[mode, expected]],
+ async function ([contentMode, contentExpected]) {
+ return new Promise(resolve => {
+ function onEvent(event) {
+ select.removeEventListener(event.type, onEvent);
+ Assert.ok(
+ contentExpected.length,
+ "Unexpected event " + event.type
+ );
+ let expectation = contentExpected.shift();
+ Assert.equal(
+ event.type,
+ expectation.type,
+ "Expected the right event order"
+ );
+ Assert.ok(event.bubbles, "All of these events should bubble");
+ Assert.equal(
+ event.cancelable,
+ expectation.cancelable,
+ "Cancellation property should match"
+ );
+ Assert.equal(
+ event.target.localName,
+ expectation.targetIsOption ? "option" : "select",
+ "Target matches"
+ );
+ Assert.equal(
+ event.composed,
+ expectation.composed,
+ "Composed property should match"
+ );
+ if (!contentExpected.length) {
+ resolve();
+ }
+ }
+
+ let select = content.document.getElementById(
+ contentMode == "enter" ? "one" : "two"
+ );
+ for (let event of [
+ "input",
+ "change",
+ "mousedown",
+ "mouseup",
+ "click",
+ ]) {
+ select.addEventListener(event, onEvent);
+ }
+ });
+ }
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup(mode);
+ await eventsPromise;
+ }
+ }
+ );
+});
+
+async function performSelectSearchTests(win) {
+ let browser = win.gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let select = doc.getElementById("one");
+
+ for (var i = 0; i < 40; i++) {
+ select.add(new content.Option("Test" + i));
+ }
+
+ select.options[1].selected = true;
+ select.focus();
+ });
+
+ let selectPopup = await openSelectPopup(false, "select", win);
+
+ let searchElement = selectPopup.querySelector(
+ ".contentSelectDropdown-searchbox"
+ );
+ searchElement.focus();
+
+ EventUtils.synthesizeKey("O", {}, win);
+ is(selectPopup.children[2].hidden, false, "First option should be visible");
+ is(selectPopup.children[3].hidden, false, "Second option should be visible");
+
+ EventUtils.synthesizeKey("3", {}, win);
+ is(selectPopup.children[2].hidden, true, "First option should be hidden");
+ is(selectPopup.children[3].hidden, true, "Second option should be hidden");
+ is(selectPopup.children[4].hidden, false, "Third option should be visible");
+
+ EventUtils.synthesizeKey("Z", {}, win);
+ is(selectPopup.children[4].hidden, true, "Third option should be hidden");
+ is(
+ selectPopup.children[1].hidden,
+ true,
+ "First group header should be hidden"
+ );
+
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ is(selectPopup.children[4].hidden, false, "Third option should be visible");
+
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ is(
+ selectPopup.children[5].hidden,
+ false,
+ "Second group header should be visible"
+ );
+
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ EventUtils.synthesizeKey("O", {}, win);
+ EventUtils.synthesizeKey("5", {}, win);
+ is(
+ selectPopup.children[5].hidden,
+ false,
+ "Second group header should be visible"
+ );
+ is(
+ selectPopup.children[1].hidden,
+ true,
+ "First group header should be hidden"
+ );
+
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ is(
+ selectPopup.children[1].hidden,
+ false,
+ "First group header should be shown"
+ );
+
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ is(
+ selectPopup.children[8].hidden,
+ true,
+ "Option hidden by content should remain hidden"
+ );
+
+ await hideSelectPopup("escape", win);
+}
+
+// This test checks the functionality of search in select elements with groups
+// and a large number of options.
+add_task(async function test_select_search() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.selectSearch", true]],
+ });
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_GROUPS);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ await performSelectSearchTests(window);
+
+ BrowserTestUtils.removeTab(tab);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// This test checks that a mousemove event is fired correctly at the menu and
+// not at the browser, ensuring that any mouse capture has been cleared.
+add_task(async function test_mousemove_correcttarget() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ const selectPopup = await openSelectPopup("mousedown");
+
+ await new Promise(resolve => {
+ window.addEventListener(
+ "mousemove",
+ function (event) {
+ is(event.target.localName.indexOf("menu"), 0, "mouse over menu");
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+
+ EventUtils.synthesizeMouseAtCenter(selectPopup.firstElementChild, {
+ type: "mousemove",
+ buttons: 1,
+ });
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mouseup" },
+ gBrowser.selectedBrowser
+ );
+
+ await hideSelectPopup();
+
+ // The popup should be closed when fullscreen mode is entered or exited.
+ for (let steps = 0; steps < 2; steps++) {
+ await openSelectPopup("click");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+ let sizeModeChanged = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ BrowserFullScreen();
+ await sizeModeChanged;
+ await popupHiddenPromise;
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test checks when a <select> element has some options with altered display values.
+add_task(async function test_somehidden() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SOMEHIDDEN);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let selectPopup = await openSelectPopup("click");
+
+ // The exact number is not needed; just ensure the height is larger than 4 items to accommodate any popup borders.
+ ok(
+ selectPopup.getBoundingClientRect().height >=
+ selectPopup.lastElementChild.getBoundingClientRect().height * 4,
+ "Height contains at least 4 items"
+ );
+ ok(
+ selectPopup.getBoundingClientRect().height <
+ selectPopup.lastElementChild.getBoundingClientRect().height * 5,
+ "Height doesn't contain 5 items"
+ );
+
+ // The label contains the substring 'Visible' for items that are visible.
+ // Otherwise, it is expected to be display: none.
+ is(selectPopup.parentNode.itemCount, 9, "Correct number of items");
+ let child = selectPopup.firstElementChild;
+ let idx = 1;
+ while (child) {
+ is(
+ getComputedStyle(child).display,
+ child.label.indexOf("Visible") > 0 ? "flex" : "none",
+ "Item " + idx++ + " is visible"
+ );
+ child = child.nextElementSibling;
+ }
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test checks that the popup is closed when the select element is blurred.
+add_task(async function test_blur_hides_popup() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.addEventListener(
+ "blur",
+ function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ },
+ true
+ );
+
+ content.document.getElementById("one").focus();
+ });
+
+ let selectPopup = await openSelectPopup();
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.document.getElementById("one").blur();
+ });
+
+ await popupHiddenPromise;
+
+ ok(true, "Blur closed popup");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test zoom handling.
+add_task(async function test_zoom() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ info("Opening the popup");
+ const selectPopup = await openSelectPopup("click");
+
+ info("Opened the popup");
+ let nonZoomedFontSize = parseFloat(
+ getComputedStyle(selectPopup.querySelector("menuitem")).fontSize,
+ 10
+ );
+
+ info("font-size is " + nonZoomedFontSize);
+ await hideSelectPopup();
+
+ info("Hide the popup");
+
+ for (let i = 0; i < 2; ++i) {
+ info("Testing with full zoom: " + ZoomManager.useFullZoom);
+
+ // This is confusing, but does the right thing.
+ FullZoom.setZoom(2.0, tab.linkedBrowser);
+
+ info("Opening popup again");
+ await openSelectPopup("click");
+
+ let zoomedFontSize = parseFloat(
+ getComputedStyle(selectPopup.querySelector("menuitem")).fontSize,
+ 10
+ );
+ info("Zoomed font-size is " + zoomedFontSize);
+
+ ok(
+ Math.abs(zoomedFontSize - nonZoomedFontSize * 2.0) < 0.01,
+ `Zoom should affect menu popup size, got ${zoomedFontSize}, ` +
+ `expected ${nonZoomedFontSize * 2.0}`
+ );
+
+ await hideSelectPopup();
+ info("Hid the popup again");
+
+ ZoomManager.toggleZoom();
+ }
+
+ FullZoom.setZoom(1.0, tab.linkedBrowser); // make sure the zoom level is reset
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test that input and change events are dispatched consistently (bug 1561882).
+add_task(async function test_event_destroys_popup() {
+ const PAGE_CONTENT = `
+<!doctype html>
+<select>
+ <option>a</option>
+ <option>b</option>
+</select>
+<script>
+gChangeEvents = 0;
+gInputEvents = 0;
+let select = document.querySelector("select");
+ select.addEventListener("input", function() {
+ gInputEvents++;
+ this.style.display = "none";
+ this.getBoundingClientRect();
+ })
+ select.addEventListener("change", function() {
+ gChangeEvents++;
+ })
+</script>`;
+
+ const pageUrl = "data:text/html," + escape(PAGE_CONTENT);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ // Test change and input events get handled consistently
+ await openSelectPopup("click");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup();
+
+ is(
+ await getChangeEvents(),
+ 1,
+ "Should get change and input events consistently"
+ );
+ is(
+ await getInputEvents(),
+ 1,
+ "Should get change and input events consistently (input)"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_label_not_text() {
+ const PAGE_CONTENT = `
+<!doctype html>
+<select>
+ <option label="Some nifty Label">Some Element Text Instead</option>
+ <option label="">Element Text</option>
+</select>
+`;
+
+ const pageUrl = "data:text/html," + escape(PAGE_CONTENT);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ const selectPopup = await openSelectPopup("click");
+
+ is(
+ selectPopup.children[0].label,
+ "Some nifty Label",
+ "Use the label not the text."
+ );
+
+ is(
+ selectPopup.children[1].label,
+ "Element Text",
+ "Uses the text if the label is empty, like HTMLOptionElement::GetRenderedLabel."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_colors.js b/browser/base/content/test/forms/browser_selectpopup_colors.js
new file mode 100644
index 0000000000..00b399c672
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_colors.js
@@ -0,0 +1,867 @@
+const gSelects = {
+ PAGECONTENT_COLORS:
+ "<html><head><style>" +
+ " .blue { color: #fff; background-color: #00f; }" +
+ " .green { color: #800080; background-color: green; }" +
+ " .defaultColor { color: -moz-ComboboxText; }" +
+ " .defaultBackground { background-color: -moz-Combobox; }" +
+ "</style>" +
+ "<body><select id='one'>" +
+ ' <option value="One" style="color: #fff; background-color: #f00;">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(255, 0, 0)"}</option>' +
+ ' <option value="Two" class="blue">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 255)"}</option>' +
+ ' <option value="Three" class="green">{"color": "rgb(128, 0, 128)", "backgroundColor": "rgb(0, 128, 0)"}</option>' +
+ ' <option value="Four" class="defaultColor defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option value="Five" class="defaultColor">{"color": "-moz-ComboboxText", "backgroundColor": "rgba(0, 0, 0, 0)", "unstyled": "true"}</option>' +
+ ' <option value="Six" class="defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option value="Seven" selected="true">{"unstyled": "true"}</option>' +
+ "</select></body></html>",
+
+ PAGECONTENT_COLORS_ON_SELECT:
+ "<html><head><style>" +
+ " #one { background-color: #7E3A3A; color: #fff }" +
+ "</style>" +
+ "<body><select id='one'>" +
+ ' <option value="One">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option value="Two">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option value="Three">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option value="Four" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ TRANSPARENT_SELECT:
+ "<html><head><style>" +
+ " #one { background-color: transparent; }" +
+ "</style>" +
+ "<body><select id='one'>" +
+ ' <option value="One">{"unstyled": "true"}</option>' +
+ ' <option value="Two" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ OPTION_COLOR_EQUAL_TO_UABACKGROUND_COLOR_SELECT:
+ "<html><head><style>" +
+ " #one { background-color: black; color: white; }" +
+ "</style>" +
+ "<body><select id='one'>" +
+ ' <option value="One" style="background-color: white; color: black;">{"color": "rgb(0, 0, 0)", "backgroundColor": "rgb(255, 255, 255)"}</option>' +
+ ' <option value="Two" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ GENERIC_OPTION_STYLED_AS_IMPORTANT:
+ "<html><head><style>" +
+ " option { background-color: black !important; color: white !important; }" +
+ "</style>" +
+ "<body><select id='one'>" +
+ ' <option value="One">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 0)"}</option>' +
+ ' <option value="Two" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ TRANSLUCENT_SELECT_BECOMES_OPAQUE:
+ "<html><head>" +
+ "<body><select id='one' style='background-color: rgba(255,255,255,.55);'>" +
+ ' <option value="One">{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option value="Two" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ TRANSLUCENT_SELECT_APPLIES_ON_BASE_COLOR:
+ "<html><head>" +
+ "<body><select id='one' style='background-color: rgba(255,0,0,.55);'>" +
+ ' <option value="One">{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option value="Two" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ DISABLED_OPTGROUP_AND_OPTIONS:
+ "<html><head>" +
+ "<body><select id='one'>" +
+ " <optgroup label='{\"unstyled\": true}'>" +
+ ' <option disabled="">{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"unstyled": true}</option>' +
+ ' <option disabled="">{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"unstyled": true}</option>' +
+ ' <option>{"unstyled": true}</option>' +
+ ' <option>{"unstyled": true}</option>' +
+ ' <option>{"unstyled": true}</option>' +
+ " </optgroup>" +
+ ' <optgroup label=\'{"color": "GrayText", "backgroundColor": "-moz-Combobox"}\' disabled=\'\'>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' +
+ " </optgroup>" +
+ ' <option value="Two" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ SELECT_CHANGES_COLOR_ON_FOCUS:
+ "<html><head><style>" +
+ " select:focus { background-color: orange; color: black; }" +
+ "</style></head>" +
+ "<body><select id='one'>" +
+ ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS:
+ "<html><head><style>" +
+ " select { background-color: black; }" +
+ " option { color: white; }" +
+ "</style></head>" +
+ "<body><select id='one'>" +
+ ' <option>{"colorScheme": "dark", "color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 0)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT:
+ "<html><head><style>" +
+ " select:focus { background-color: #3a96dd; }" +
+ " select:focus option { background-color: #fff; }" +
+ "</style></head>" +
+ "<body><select id='one'>" +
+ ' <option>{"color": "-moz-ComboboxText", "backgroundColor": "rgb(255, 255, 255)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ SELECT_STYLE_OF_OPTION_CHANGES_AFTER_FOCUS_EVENT:
+ "<html><body><select id='one'>" +
+ ' <option>{"color": "rgb(255, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body><scr" +
+ "ipt>" +
+ " var select = document.getElementById('one');" +
+ " select.addEventListener('focus', () => select.style.color = 'red');" +
+ "</script></html>",
+
+ SELECT_COLOR_OF_OPTION_CHANGES_AFTER_TRANSITIONEND:
+ "<html><head><style>" +
+ " select { transition: all .1s; }" +
+ " select:focus { background-color: orange; }" +
+ "</style></head><body><select id='one'>" +
+ ' <option>{"color": "-moz-ComboboxText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND:
+ "<html><head><style>" +
+ " select { transition: all .1s; }" +
+ " select:focus { text-shadow: 0 0 0 #303030; }" +
+ " option { color: red; /* It gets the default otherwise, which is fine but we don't have a good way to test for */ }" +
+ "</style></head><body><select id='one'>" +
+ ' <option>{"color": "rgb(255, 0, 0)", "backgroundColor": "-moz-Combobox", "textShadow": "rgb(48, 48, 48) 0px 0px 0px"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ SELECT_TRANSPARENT_COLOR_WITH_TEXT_SHADOW:
+ "<html><head><style>" +
+ " select { color: transparent; text-shadow: 0 0 0 #303030; }" +
+ "</style></head><body><select id='one'>" +
+ ' <option>{"color": "rgba(0, 0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)", "textShadow": "rgb(48, 48, 48) 0px 0px 0px"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>",
+
+ SELECT_LONG_WITH_TRANSITION:
+ "<html><head><style>" +
+ " select { transition: all .2s linear; }" +
+ " select:focus { color: purple; }" +
+ "</style></head><body><select id='one'>" +
+ (function () {
+ let rv = "";
+ for (let i = 0; i < 75; i++) {
+ rv +=
+ ' <option>{"color": "rgb(128, 0, 128)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>';
+ }
+ rv +=
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>";
+ return rv;
+ })(),
+
+ SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT: `
+ <html><head><style>
+ select { color: blue; text-shadow: 1px 1px 2px blue; }
+ .redColor { color: red; }
+ .textShadow { text-shadow: 1px 1px 2px black; }
+ </style></head><body><select id='one'>
+ <option>{"color": "rgb(0, 0, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>
+ <option>{"color": "rgb(0, 0, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>
+ <option>{"color": "rgb(0, 0, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>
+ <option class="redColor">{"color": "rgb(255, 0, 0)", "backgroundColor": "-moz-Combobox"}</option>
+ <option class="textShadow">{"color": "rgb(0, 0, 255)", "textShadow": "rgb(0, 0, 0) 1px 1px 2px", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>
+ <option selected="true">{"end": "true"}</option>
+ </select></body></html>
+`,
+
+ SELECT_FONT_INHERITS_TO_OPTION: `
+ <html><head><style>
+ select { font-family: monospace }
+ </style></head><body><select id='one'>
+ <option>One</option>
+ <option style="font-family: sans-serif">Two</option>
+ </select></body></html>
+`,
+
+ SELECT_SCROLLBAR_PROPS: `
+ <html><head><style>
+ select { scrollbar-width: thin; scrollbar-color: red blue }
+ </style></head><body><select id='one'>
+ <option>One</option>
+ <option style="font-family: sans-serif">Two</option>
+ </select></body></html>
+`,
+ DEFAULT_DARKMODE: `
+ <html><body><select id='one'>
+ <option>{"unstyled": "true"}</option>
+ <option>{"unstyled": "true"}</option>
+ <option selected="true">{"end": "true"}</option>
+ </select></body></html>
+`,
+
+ DEFAULT_DARKMODE_DARK: `
+ <meta name=color-scheme content=dark>
+ <select id='one'>
+ <option>{"color": "MenuText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>
+ <option>{"color": "MenuText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>
+ <option selected="true">{"end": "true"}</option>
+ </select>
+`,
+
+ SPLIT_FG_BG_OPTION_DARKMODE: `
+ <html><head><style>
+ select { background-color: #fff; }
+ option { color: #2b2b2b; }
+ </style></head><body><select id='one'>
+ <option>{"color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option>
+ <option>{"color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option>
+ <option selected="true">{"end": "true"}</option>
+ </select></body></html>
+`,
+
+ IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE: `
+ <html><head><style>
+ select { background-color: #fff; }
+ option { color: #2b2b2b; background-color: #fff; }
+ </style></head><body><select id='one'>
+ <option>{"colorScheme": "light", "color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option>
+ <option>{"colorScheme": "light", "color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option>
+ <option selected="true">{"end": "true"}</option>
+ </select></body></html>
+`,
+};
+
+function rgbaToString(parsedColor) {
+ let { r, g, b, a } = parsedColor;
+ if (a == 1) {
+ return `rgb(${r}, ${g}, ${b})`;
+ }
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+}
+
+function testOptionColors(test, index, item, menulist) {
+ // The label contains a JSON string of the expected colors for
+ // `color` and `background-color`.
+ let expected = JSON.parse(item.label);
+
+ // Press Down to move the selected item to the next item in the
+ // list and check the colors of this item when it's not selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ if (expected.end) {
+ return;
+ }
+
+ if (expected.unstyled) {
+ ok(
+ !item.hasAttribute("customoptionstyling"),
+ `${test}: Item ${index} should not have any custom option styling: ${item.outerHTML}`
+ );
+ } else {
+ is(
+ getComputedStyle(item).color,
+ expected.color,
+ `${test}: Item ${index} has correct foreground color`
+ );
+ is(
+ getComputedStyle(item).backgroundColor,
+ expected.backgroundColor,
+ `${test}: Item ${index} has correct background color`
+ );
+ if (expected.textShadow) {
+ is(
+ getComputedStyle(item).textShadow,
+ expected.textShadow,
+ `${test}: Item ${index} has correct text-shadow color`
+ );
+ }
+ }
+}
+
+function computeLabels(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ function _rgbaToString(parsedColor) {
+ let { r, g, b, a } = parsedColor;
+ if (a == 1) {
+ return `rgb(${r}, ${g}, ${b})`;
+ }
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+ }
+ function computeColors(expected) {
+ let any = false;
+ for (let color of Object.keys(expected)) {
+ if (
+ color != "colorScheme" &&
+ color.toLowerCase().includes("color") &&
+ !expected[color].startsWith("rgb")
+ ) {
+ any = true;
+ expected[color] = _rgbaToString(
+ InspectorUtils.colorToRGBA(expected[color], content.document)
+ );
+ }
+ }
+ return any;
+ }
+ for (let option of content.document.querySelectorAll("option,optgroup")) {
+ if (!option.label) {
+ continue;
+ }
+ let expected;
+ try {
+ expected = JSON.parse(option.label);
+ } catch (ex) {
+ continue;
+ }
+ if (computeColors(expected)) {
+ option.label = JSON.stringify(expected);
+ }
+ }
+ });
+}
+
+async function openSelectPopup(select) {
+ const pageUrl = "data:text/html," + escape(select);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ await computeLabels(tab);
+
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mousedown" },
+ gBrowser.selectedBrowser
+ );
+ let selectPopup = await popupShownPromise;
+ let menulist = selectPopup.parentNode;
+ return { tab, menulist, selectPopup };
+}
+
+async function testSelectColors(selectID, itemCount, options) {
+ let select = gSelects[selectID];
+ let { tab, menulist, selectPopup } = await openSelectPopup(select);
+ if (options.unstyled) {
+ ok(
+ !selectPopup.hasAttribute("customoptionstyling"),
+ `Shouldn't have custom option styling for ${selectID}`
+ );
+ }
+ let arrowSB = selectPopup.shadowRoot.querySelector(
+ ".menupopup-arrowscrollbox"
+ );
+ if (options.waitForComputedStyle) {
+ let property = options.waitForComputedStyle.property;
+ let expectedValue = options.waitForComputedStyle.value;
+ await TestUtils.waitForCondition(() => {
+ let node = ["background-image", "background-color"].includes(property)
+ ? arrowSB
+ : selectPopup;
+ let value = getComputedStyle(node).getPropertyValue(property);
+ info(`<${node.localName}> has ${property}: ${value}`);
+ return value == expectedValue;
+ }, `${selectID} - Waiting for <select> to have ${property}: ${expectedValue}`);
+ }
+
+ is(selectPopup.parentNode.itemCount, itemCount, "Correct number of items");
+ let child = selectPopup.firstElementChild;
+ let idx = 1;
+
+ if (typeof options.skipSelectColorTest != "object") {
+ let skip = !!options.skipSelectColorTest;
+ options.skipSelectColorTest = {
+ color: skip,
+ background: skip,
+ };
+ }
+ if (!options.skipSelectColorTest.color) {
+ is(
+ getComputedStyle(arrowSB).color,
+ options.selectColor,
+ selectID + " popup has expected foreground color"
+ );
+ }
+
+ if (options.selectTextShadow) {
+ is(
+ getComputedStyle(selectPopup).textShadow,
+ options.selectTextShadow,
+ selectID + " popup has expected text-shadow color"
+ );
+ }
+
+ if (!options.skipSelectColorTest.background) {
+ // Combine the select popup's backgroundColor and the
+ // backgroundImage color to get the color that is seen by
+ // the user.
+ let base = getComputedStyle(arrowSB).backgroundColor;
+ if (base == "rgba(0, 0, 0, 0)") {
+ base = getComputedStyle(selectPopup).backgroundColor;
+ }
+ info("Parsing background color: " + base);
+ let [, /* unused */ bR, bG, bB] = base.match(/rgb\((\d+), (\d+), (\d+)\)/);
+ bR = parseInt(bR, 10);
+ bG = parseInt(bG, 10);
+ bB = parseInt(bB, 10);
+ let topCoat = getComputedStyle(arrowSB).backgroundImage;
+ if (topCoat == "none") {
+ is(
+ `rgb(${bR}, ${bG}, ${bB})`,
+ options.selectBgColor,
+ selectID + " popup has expected background color (top coat)"
+ );
+ } else {
+ let [, , /* unused */ /* unused */ tR, tG, tB, tA] = topCoat.match(
+ /(rgba?\((\d+), (\d+), (\d+)(?:, (0\.\d+))?\)), \1/
+ );
+ tR = parseInt(tR, 10);
+ tG = parseInt(tG, 10);
+ tB = parseInt(tB, 10);
+ tA = parseFloat(tA) || 1;
+ let actualR = Math.round(tR * tA + bR * (1 - tA));
+ let actualG = Math.round(tG * tA + bG * (1 - tA));
+ let actualB = Math.round(tB * tA + bB * (1 - tA));
+ is(
+ `rgb(${actualR}, ${actualG}, ${actualB})`,
+ options.selectBgColor,
+ selectID + " popup has expected background color (no top coat)"
+ );
+ }
+ }
+
+ ok(!child.selected, "The first child should not be selected");
+ while (child) {
+ testOptionColors(selectID, idx, child, menulist);
+ idx++;
+ child = child.nextElementSibling;
+ }
+
+ if (!options.leaveOpen) {
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+ }
+}
+
+// System colors may be different in content pages and chrome pages.
+let kDefaultSelectStyles = {};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.select.customstyling", true]],
+ });
+ kDefaultSelectStyles = await BrowserTestUtils.withNewTab(
+ `data:text/html,<select>`,
+ function (browser) {
+ return SpecialPowers.spawn(browser, [], function () {
+ let cs = content.getComputedStyle(
+ content.document.querySelector("select")
+ );
+ return {
+ backgroundColor: cs.backgroundColor,
+ };
+ });
+ }
+ );
+});
+
+// This test checks when a <select> element has styles applied to <option>s within it.
+add_task(async function test_colors_applied_to_popup_items() {
+ await testSelectColors("PAGECONTENT_COLORS", 7, {
+ skipSelectColorTest: true,
+ });
+});
+
+// This test checks when a <select> element has styles applied to itself.
+add_task(async function test_colors_applied_to_popup() {
+ let options = {
+ selectColor: "rgb(255, 255, 255)",
+ selectBgColor: "rgb(126, 58, 58)",
+ };
+ await testSelectColors("PAGECONTENT_COLORS_ON_SELECT", 4, options);
+});
+
+// This test checks when a <select> element has a transparent background applied to itself.
+add_task(async function test_transparent_applied_to_popup() {
+ let options = {
+ unstyled: true,
+ skipSelectColorTest: true,
+ };
+ await testSelectColors("TRANSPARENT_SELECT", 2, options);
+});
+
+// This test checks when a <select> element has a background set, and the
+// options have their own background set which is equal to the default
+// user-agent background color, but should be used because the select
+// background color has been changed.
+add_task(async function test_options_inverted_from_select_background() {
+ // The popup has a black background and white text, but the
+ // options inside of it have flipped the colors.
+ let options = {
+ selectColor: "rgb(255, 255, 255)",
+ selectBgColor: "rgb(0, 0, 0)",
+ };
+ await testSelectColors(
+ "OPTION_COLOR_EQUAL_TO_UABACKGROUND_COLOR_SELECT",
+ 2,
+ options
+ );
+});
+
+// This test checks when a <select> element has a background set using !important,
+// which was affecting how we calculated the user-agent styling.
+add_task(async function test_select_background_using_important() {
+ await testSelectColors("GENERIC_OPTION_STYLED_AS_IMPORTANT", 2, {
+ skipSelectColorTest: true,
+ });
+});
+
+// This test checks when a <select> element has a background set, and the
+// options have their own background set which is equal to the default
+// user-agent background color, but should be used because the select
+// background color has been changed.
+add_task(async function test_translucent_select_becomes_opaque() {
+ // The popup is requested to show a translucent background
+ // but we apply the requested background color on the system's base color.
+ let options = {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(255, 255, 255)",
+ };
+ await testSelectColors("TRANSLUCENT_SELECT_BECOMES_OPAQUE", 2, options);
+});
+
+// This test checks when a popup has a translucent background color,
+// and that the color painted to the screen of the translucent background
+// matches what the user expects.
+add_task(async function test_translucent_select_applies_on_base_color() {
+ // The popup is requested to show a translucent background
+ // but we apply the requested background color on the system's base color.
+ let options = {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(255, 115, 115)",
+ };
+ await testSelectColors(
+ "TRANSLUCENT_SELECT_APPLIES_ON_BASE_COLOR",
+ 2,
+ options
+ );
+});
+
+add_task(async function test_disabled_optgroup_and_options() {
+ await testSelectColors("DISABLED_OPTGROUP_AND_OPTIONS", 17, {
+ skipSelectColorTest: true,
+ });
+});
+
+add_task(async function test_disabled_optgroup_and_options() {
+ let options = {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(255, 165, 0)",
+ };
+
+ await testSelectColors("SELECT_CHANGES_COLOR_ON_FOCUS", 2, options);
+});
+
+add_task(async function test_bgcolor_on_select_color_on_options() {
+ let options = {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(0, 0, 0)",
+ };
+
+ await testSelectColors(
+ "SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS",
+ 2,
+ options
+ );
+});
+
+add_task(
+ async function test_style_of_options_is_dependent_on_focus_of_select() {
+ let options = {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(58, 150, 221)",
+ };
+
+ await testSelectColors(
+ "SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT",
+ 2,
+ options
+ );
+ }
+);
+
+add_task(
+ async function test_style_of_options_is_dependent_on_focus_of_select_after_event() {
+ let options = {
+ skipSelectColorTest: true,
+ waitForComputedStyle: {
+ property: "--panel-color",
+ value: "rgb(255, 0, 0)",
+ },
+ };
+ await testSelectColors(
+ "SELECT_STYLE_OF_OPTION_CHANGES_AFTER_FOCUS_EVENT",
+ 2,
+ options
+ );
+ }
+);
+
+add_task(async function test_color_of_options_is_dependent_on_transitionend() {
+ let options = {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(255, 165, 0)",
+ waitForComputedStyle: {
+ property: "background-image",
+ value: "linear-gradient(rgb(255, 165, 0), rgb(255, 165, 0))",
+ },
+ };
+
+ await testSelectColors(
+ "SELECT_COLOR_OF_OPTION_CHANGES_AFTER_TRANSITIONEND",
+ 2,
+ options
+ );
+});
+
+add_task(
+ async function test_textshadow_of_options_is_dependent_on_transitionend() {
+ let options = {
+ skipSelectColorTest: true,
+ waitForComputedStyle: {
+ property: "text-shadow",
+ value: "rgb(48, 48, 48) 0px 0px 0px",
+ },
+ };
+
+ await testSelectColors(
+ "SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND",
+ 2,
+ options
+ );
+ }
+);
+
+add_task(async function test_transparent_color_with_text_shadow() {
+ let options = {
+ selectColor: "rgba(0, 0, 0, 0)",
+ selectTextShadow: "rgb(48, 48, 48) 0px 0px 0px",
+ selectBgColor: kDefaultSelectStyles.backgroundColor,
+ };
+
+ await testSelectColors(
+ "SELECT_TRANSPARENT_COLOR_WITH_TEXT_SHADOW",
+ 2,
+ options
+ );
+});
+
+add_task(
+ async function test_select_with_transition_doesnt_lose_scroll_position() {
+ let options = {
+ selectColor: "rgb(128, 0, 128)",
+ selectBgColor: kDefaultSelectStyles.backgroundColor,
+ waitForComputedStyle: {
+ property: "--panel-color",
+ value: "rgb(128, 0, 128)",
+ },
+ leaveOpen: true,
+ };
+
+ await testSelectColors("SELECT_LONG_WITH_TRANSITION", 76, options);
+
+ let selectPopup = document.getElementById(
+ "ContentSelectDropdown"
+ ).menupopup;
+ let scrollBox = selectPopup.scrollBox;
+ is(
+ scrollBox.scrollTop,
+ scrollBox.scrollTopMax,
+ "The popup should be scrolled to the bottom of the list (where the selected item is)"
+ );
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+);
+
+add_task(
+ async function test_select_inherited_colors_on_options_dont_get_unique_rules_if_rule_set_on_select() {
+ let options = {
+ selectColor: "rgb(0, 0, 255)",
+ selectTextShadow: "rgb(0, 0, 255) 1px 1px 2px",
+ selectBgColor: kDefaultSelectStyles.backgroundColor,
+ leaveOpen: true,
+ };
+
+ await testSelectColors(
+ "SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT",
+ 6,
+ options
+ );
+
+ let stylesheetEl = document.getElementById(
+ "ContentSelectDropdownStylesheet"
+ );
+
+ let sheet = stylesheetEl.sheet;
+ /* Check that the rules are what we expect: There are three different option styles (even though there are 6 options, plus the select rules). */
+ let expectedSelectors = [
+ "#ContentSelectDropdown .ContentSelectDropdown-item-0",
+ "#ContentSelectDropdown .ContentSelectDropdown-item-1",
+ '#ContentSelectDropdown .ContentSelectDropdown-item-1:not([_moz-menuactive="true"])',
+ "#ContentSelectDropdown .ContentSelectDropdown-item-2",
+ '#ContentSelectDropdown .ContentSelectDropdown-item-2:not([_moz-menuactive="true"])',
+ '#ContentSelectDropdown > menupopup > :is(menuitem, menucaption):not([_moz-menuactive="true"])',
+ '#ContentSelectDropdown > menupopup > :is(menuitem, menucaption)[_moz-menuactive="true"]',
+ ].sort();
+
+ let actualSelectors = [...sheet.cssRules].map(r => r.selectorText).sort();
+ is(
+ actualSelectors.length,
+ expectedSelectors.length,
+ "Should have the expected number of rules"
+ );
+ for (let i = 0; i < expectedSelectors.length; ++i) {
+ is(
+ actualSelectors[i],
+ expectedSelectors[i],
+ `Selector ${i} should match`
+ );
+ }
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+);
+
+add_task(async function test_select_font_inherits_to_option() {
+ let { tab, menulist, selectPopup } = await openSelectPopup(
+ gSelects.SELECT_FONT_INHERITS_TO_OPTION
+ );
+
+ let popupFont = getComputedStyle(selectPopup).fontFamily;
+ let items = menulist.querySelectorAll("menuitem");
+ is(items.length, 2, "Should have two options");
+ let firstItemFont = getComputedStyle(items[0]).fontFamily;
+ let secondItemFont = getComputedStyle(items[1]).fontFamily;
+
+ is(
+ popupFont,
+ firstItemFont,
+ "First menuitem's font should be inherited from the select"
+ );
+ isnot(
+ popupFont,
+ secondItemFont,
+ "Second menuitem's font should be the author specified one"
+ );
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_scrollbar_props() {
+ let { tab, selectPopup } = await openSelectPopup(
+ gSelects.SELECT_SCROLLBAR_PROPS
+ );
+
+ let popupStyle = getComputedStyle(selectPopup);
+ is(popupStyle.getPropertyValue("--content-select-scrollbar-width"), "thin");
+ is(popupStyle.scrollbarColor, "rgb(255, 0, 0) rgb(0, 0, 255)");
+
+ let scrollBoxStyle = getComputedStyle(selectPopup.scrollBox.scrollbox);
+ is(scrollBoxStyle.overflow, "auto", "Should be the scrollable box");
+ is(scrollBoxStyle.scrollbarWidth, "thin");
+ is(scrollBoxStyle.scrollbarColor, "rgb(255, 0, 0) rgb(0, 0, 255)");
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+});
+
+if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ add_task(async function test_darkmode() {
+ let lightSelectColor = rgbaToString(
+ InspectorUtils.colorToRGBA("MenuText", document)
+ );
+ let lightSelectBgColor = rgbaToString(
+ InspectorUtils.colorToRGBA("Menu", document)
+ );
+
+ // Force dark mode:
+ let darkModeQuery = matchMedia("(prefers-color-scheme: dark)");
+ let darkModeChange = BrowserTestUtils.waitForEvent(darkModeQuery, "change");
+ await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] });
+ await darkModeChange;
+
+ // Determine colours from the main context menu:
+ let darkSelectColor = rgbaToString(
+ InspectorUtils.colorToRGBA("MenuText", document)
+ );
+ let darkSelectBgColor = rgbaToString(
+ InspectorUtils.colorToRGBA("Menu", document)
+ );
+
+ isnot(lightSelectColor, darkSelectColor);
+ isnot(lightSelectBgColor, darkSelectBgColor);
+
+ let { tab } = await openSelectPopup(gSelects.DEFAULT_DARKMODE);
+
+ await testSelectColors("DEFAULT_DARKMODE", 3, {
+ selectColor: lightSelectColor,
+ selectBgColor: lightSelectBgColor,
+ });
+
+ await hideSelectPopup("escape");
+
+ await testSelectColors("DEFAULT_DARKMODE_DARK", 3, {
+ selectColor: darkSelectColor,
+ selectBgColor: darkSelectBgColor,
+ });
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+
+ ({ tab } = await openSelectPopup(
+ gSelects.IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE
+ ));
+
+ // Custom styling on the options enforces using the select styling, too,
+ // even if it matched the UA style. They'll be overridden on individual
+ // options where necessary.
+ await testSelectColors("IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE", 3, {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(255, 255, 255)",
+ });
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+
+ ({ tab } = await openSelectPopup(gSelects.SPLIT_FG_BG_OPTION_DARKMODE));
+
+ // Like the previous case, but here the bg colour is defined on the
+ // select, and the fg colour on the option. The behaviour should be the
+ // same.
+ await testSelectColors("SPLIT_FG_BG_OPTION_DARKMODE", 3, {
+ selectColor: "rgb(0, 0, 0)",
+ selectBgColor: "rgb(255, 255, 255)",
+ });
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+ });
+}
diff --git a/browser/base/content/test/forms/browser_selectpopup_dir.js b/browser/base/content/test/forms/browser_selectpopup_dir.js
new file mode 100644
index 0000000000..aaf4a61fc2
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_dir.js
@@ -0,0 +1,21 @@
+const PAGE = `
+<!doctype html>
+<select style="direction: rtl">
+ <option>ABC</option>
+ <option>DEFG</option>
+</select>
+`;
+
+add_task(async function () {
+ const url = "data:text/html," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popup = await openSelectPopup("click");
+ is(popup.style.direction, "rtl", "Should be the right dir");
+ }
+ );
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_large.js b/browser/base/content/test/forms/browser_selectpopup_large.js
new file mode 100644
index 0000000000..0c88755b27
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_large.js
@@ -0,0 +1,338 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PAGECONTENT_SMALL = `
+ <!doctype html>
+ <html>
+ <body><select id='one'>
+ <option value='One'>One</option>
+ <option value='Two'>Two</option>
+ </select><select id='two'>
+ <option value='Three'>Three</option>
+ <option value='Four'>Four</option>
+ </select><select id='three'>
+ <option value='Five'>Five</option>
+ <option value='Six'>Six</option>
+ </select></body></html>
+`;
+
+async function performLargePopupTests(win) {
+ let browser = win.gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let select = doc.getElementById("one");
+ for (var i = 0; i < 180; i++) {
+ select.add(new content.Option("Test" + i));
+ }
+
+ select.options[60].selected = true;
+ select.focus();
+ });
+
+ // Check if a drag-select works and scrolls the list.
+ const selectPopup = await openSelectPopup("mousedown", "select", win);
+ const browserRect = browser.getBoundingClientRect();
+
+ let getScrollPos = () => selectPopup.scrollBox.scrollbox.scrollTop;
+ let scrollPos = getScrollPos();
+ let popupRect = selectPopup.getBoundingClientRect();
+
+ // First, check that scrolling does not occur when the mouse is moved over the
+ // anchor button but not the popup yet.
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 5,
+ popupRect.top - 10,
+ {
+ type: "mousemove",
+ buttons: 1,
+ },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position after mousemove over button should not change"
+ );
+
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.top + 10,
+ {
+ type: "mousemove",
+ buttons: 1,
+ },
+ win
+ );
+
+ // Dragging above the popup scrolls it up.
+ let scrolledPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "scroll",
+ false,
+ () => getScrollPos() < scrollPos - 5
+ );
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.top - 20,
+ {
+ type: "mousemove",
+ buttons: 1,
+ },
+ win
+ );
+ await scrolledPromise;
+ ok(true, "scroll position at drag up");
+
+ // Dragging below the popup scrolls it down.
+ scrollPos = getScrollPos();
+ scrolledPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "scroll",
+ false,
+ () => getScrollPos() > scrollPos + 5
+ );
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 20,
+ {
+ type: "mousemove",
+ buttons: 1,
+ },
+ win
+ );
+ await scrolledPromise;
+ ok(true, "scroll position at drag down");
+
+ // Releasing the mouse button and moving the mouse does not change the scroll position.
+ scrollPos = getScrollPos();
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 25,
+ { type: "mouseup" },
+ win
+ );
+ is(getScrollPos(), scrollPos, "scroll position at mouseup should not change");
+
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 20,
+ { type: "mousemove" },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position at mousemove after mouseup should not change"
+ );
+
+ // Now check dragging with a mousedown on an item
+ let menuRect = selectPopup.children[51].getBoundingClientRect();
+ EventUtils.synthesizeMouseAtPoint(
+ menuRect.left + 5,
+ menuRect.top + 5,
+ { type: "mousedown" },
+ win
+ );
+
+ // Dragging below the popup scrolls it down.
+ scrolledPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "scroll",
+ false,
+ () => getScrollPos() > scrollPos + 5
+ );
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 20,
+ {
+ type: "mousemove",
+ buttons: 1,
+ },
+ win
+ );
+ await scrolledPromise;
+ ok(true, "scroll position at drag down from option");
+
+ // Dragging above the popup scrolls it up.
+ scrolledPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "scroll",
+ false,
+ () => getScrollPos() < scrollPos - 5
+ );
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.top - 20,
+ {
+ type: "mousemove",
+ buttons: 1,
+ },
+ win
+ );
+ await scrolledPromise;
+ ok(true, "scroll position at drag up from option");
+
+ scrollPos = getScrollPos();
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 25,
+ { type: "mouseup" },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position at mouseup from option should not change"
+ );
+
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 20,
+ { type: "mousemove" },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position at mousemove after mouseup should not change"
+ );
+
+ await hideSelectPopup("escape", win);
+
+ let positions = [
+ "margin-top: 300px;",
+ "position: fixed; bottom: 200px;",
+ "width: 100%; height: 9999px;",
+ ];
+
+ let position;
+ while (positions.length) {
+ await openSelectPopup("key", "select", win);
+
+ let rect = selectPopup.getBoundingClientRect();
+ let marginBottom = parseFloat(getComputedStyle(selectPopup).marginBottom);
+ let marginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
+ ok(
+ rect.top - marginTop >= browserRect.top,
+ "Popup top position in within browser area"
+ );
+ ok(
+ rect.bottom + marginBottom <= browserRect.bottom,
+ "Popup bottom position in within browser area"
+ );
+
+ let cs = win.getComputedStyle(selectPopup);
+ let csArrow = win.getComputedStyle(selectPopup.scrollBox);
+ let bpBottom =
+ parseFloat(cs.paddingBottom) +
+ parseFloat(cs.borderBottomWidth) +
+ parseFloat(csArrow.paddingBottom) +
+ parseFloat(csArrow.borderBottomWidth);
+ let selectedOption = 60;
+
+ if (Services.prefs.getBoolPref("dom.forms.selectSearch")) {
+ // Use option 61 instead of 60, as the 60th option element is actually the
+ // 61st child, since the first child is now the search input field.
+ selectedOption = 61;
+ }
+ // Some of the styles applied to the menuitems are percentages, meaning
+ // that the final layout calculations returned by getBoundingClientRect()
+ // might return floating point values. We don't care about sub-pixel
+ // accuracy, and only care about the final pixel value, so we add a
+ // fuzz-factor of 1.
+ //
+ // FIXME(emilio): In win7 scroll position is off by 20px more, but that's
+ // not reproducible in win10 even with the win7 "native" menus enabled.
+ const fuzzFactor = matchMedia("(-moz-platform: windows-win7)").matches
+ ? 21
+ : 1;
+ SimpleTest.isfuzzy(
+ selectPopup.children[selectedOption].getBoundingClientRect().bottom,
+ selectPopup.getBoundingClientRect().bottom - bpBottom + marginBottom,
+ fuzzFactor,
+ "Popup scroll at correct position " + bpBottom
+ );
+
+ await hideSelectPopup("enter", win);
+
+ position = positions.shift();
+
+ let contentPainted = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "MozAfterPaint"
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [position],
+ async function (contentPosition) {
+ let select = content.document.getElementById("one");
+ select.setAttribute("style", contentPosition || "");
+ select.getBoundingClientRect();
+ }
+ );
+ await contentPainted;
+ }
+
+ if (navigator.platform.indexOf("Mac") == 0) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ doc.body.style = "padding-top: 400px;";
+
+ let select = doc.getElementById("one");
+ select.options[41].selected = true;
+ select.focus();
+ });
+
+ await openSelectPopup("key", "select", win);
+
+ ok(
+ selectPopup.getBoundingClientRect().top >
+ browser.getBoundingClientRect().top,
+ "select popup appears over selected item"
+ );
+
+ await hideSelectPopup("escape", win);
+ }
+}
+
+// This test checks select elements with a large number of options to ensure that
+// the popup appears within the browser area.
+add_task(async function test_large_popup() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ await performLargePopupTests(window);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test checks the same as the previous test but in a new, vertically smaller window.
+add_task(async function test_large_popup_in_small_window() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ let resizePromise = BrowserTestUtils.waitForEvent(
+ newWin,
+ "resize",
+ false,
+ e => {
+ info(`Got resize event (innerHeight: ${newWin.innerHeight})`);
+ return newWin.innerHeight <= 450;
+ }
+ );
+ newWin.resizeTo(600, 450);
+ await resizePromise;
+
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ newWin.gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURIString(newWin.gBrowser.selectedBrowser, pageUrl);
+ await browserLoadedPromise;
+
+ newWin.gBrowser.selectedBrowser.focus();
+
+ await performLargePopupTests(newWin);
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_searchfocus.js b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
new file mode 100644
index 0000000000..caae828668
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
@@ -0,0 +1,36 @@
+let SELECT = "<html><body><select id='one'>";
+for (let i = 0; i < 75; i++) {
+ SELECT += ` <option>${i}${i}${i}${i}${i}</option>`;
+}
+SELECT +=
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.selectSearch", true]],
+ });
+});
+
+add_task(async function test_focus_on_search_shouldnt_close_popup() {
+ const pageUrl = "data:text/html," + escape(SELECT);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+ let selectPopup = await openSelectPopup("mousedown");
+
+ let searchInput = selectPopup.querySelector(
+ ".contentSelectDropdown-searchbox"
+ );
+ searchInput.scrollIntoView();
+ let searchFocused = BrowserTestUtils.waitForEvent(searchInput, "focus", true);
+ await EventUtils.synthesizeMouseAtCenter(searchInput, {}, window);
+ await searchFocused;
+
+ is(
+ selectPopup.state,
+ "open",
+ "select popup should still be open after clicking on the search field"
+ );
+
+ await hideSelectPopup("escape");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_text_transform.js b/browser/base/content/test/forms/browser_selectpopup_text_transform.js
new file mode 100644
index 0000000000..671f39e2a6
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_text_transform.js
@@ -0,0 +1,40 @@
+const PAGE = `
+<!doctype html>
+<select style="text-transform: uppercase">
+ <option>abc</option>
+ <option>defg</option>
+</select>
+`;
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.select.customstyling", true]],
+ });
+ const url = "data:text/html," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popup = await openSelectPopup("click");
+ let menuitems = popup.querySelectorAll("menuitem");
+ is(menuitems[0].textContent, "abc", "Option text should be lowercase");
+ is(menuitems[1].textContent, "defg", "Option text should be lowercase");
+
+ let optionStyle = getComputedStyle(menuitems[0]);
+ is(
+ optionStyle.textTransform,
+ "uppercase",
+ "Option text should be transformed to uppercase"
+ );
+
+ optionStyle = getComputedStyle(menuitems[1]);
+ is(
+ optionStyle.textTransform,
+ "uppercase",
+ "Option text should be transformed to uppercase"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_toplevel.js b/browser/base/content/test/forms/browser_selectpopup_toplevel.js
new file mode 100644
index 0000000000..85a77ea676
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_toplevel.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ let select = document.createElement("select");
+ select.appendChild(new Option("abc"));
+ select.appendChild(new Option("defg"));
+ registerCleanupFunction(() => select.remove());
+ document.body.appendChild(select);
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(select, {});
+
+ let popup = await popupShownPromise;
+ ok(!!popup, "Should've shown the popup");
+ let items = popup.querySelectorAll("menuitem");
+ is(items.length, 2, "Should have two options");
+ is(items[0].textContent, "abc", "First option should be correct");
+ is(items[1].textContent, "defg", "First option should be correct");
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_user_input.js b/browser/base/content/test/forms/browser_selectpopup_user_input.js
new file mode 100644
index 0000000000..b3cdeaf7e6
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_user_input.js
@@ -0,0 +1,90 @@
+const PAGE = `
+<!doctype html>
+<select>
+ <option>ABC</option>
+ <option>DEFG</option>
+</select>
+`;
+
+function promiseChangeHandlingUserInput(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ content.document.clearUserGestureActivation();
+ let element = content.document.querySelector("select");
+ let reply = {};
+ function getUserInputState() {
+ return {
+ isHandlingUserInput: content.window.windowUtils.isHandlingUserInput,
+ hasValidTransientUserGestureActivation:
+ content.document.hasValidTransientUserGestureActivation,
+ };
+ }
+ reply.before = getUserInputState();
+ await ContentTaskUtils.waitForEvent(element, "change", false, () => {
+ reply.during = getUserInputState();
+ return true;
+ });
+ await new Promise(r => content.window.setTimeout(r));
+ reply.after = getUserInputState();
+ return reply;
+ });
+}
+
+async function testHandlingUserInputOnChange(aTriggerFn) {
+ const url = "data:text/html," + encodeURI(PAGE);
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popup = await openSelectPopup("click");
+ let userInputOnChange = promiseChangeHandlingUserInput(browser);
+ await aTriggerFn(popup);
+ let userInput = await userInputOnChange;
+ ok(
+ !userInput.before.isHandlingUserInput,
+ "Shouldn't be handling user input before test"
+ );
+ ok(
+ !userInput.before.hasValidTransientUserGestureActivation,
+ "transient activation should be cleared before test"
+ );
+ ok(
+ userInput.during.hasValidTransientUserGestureActivation,
+ "should provide transient activation during event"
+ );
+ ok(
+ userInput.during.isHandlingUserInput,
+ "isHandlingUserInput should be true during event"
+ );
+ ok(
+ userInput.after.hasValidTransientUserGestureActivation,
+ "should provide transient activation after event"
+ );
+ ok(
+ !userInput.after.isHandlingUserInput,
+ "isHandlingUserInput should be false after event"
+ );
+ }
+ );
+}
+
+// This test checks if the change/click event is considered as user input event.
+add_task(async function test_handling_user_input_key() {
+ return testHandlingUserInputOnChange(async function (popup) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup();
+ });
+});
+
+add_task(async function test_handling_user_input_click() {
+ return testHandlingUserInputOnChange(async function (popup) {
+ EventUtils.synthesizeMouseAtCenter(popup.lastElementChild, {});
+ });
+});
+
+add_task(async function test_handling_user_input_click() {
+ return testHandlingUserInputOnChange(async function (popup) {
+ EventUtils.synthesizeMouseAtCenter(popup.lastElementChild, {});
+ });
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_width.js b/browser/base/content/test/forms/browser_selectpopup_width.js
new file mode 100644
index 0000000000..d8f748fb18
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_width.js
@@ -0,0 +1,49 @@
+const PAGE = `
+<!doctype html>
+<select style="width: 600px">
+ <option>ABC</option>
+ <option>DEFG</option>
+</select>
+`;
+
+function tick() {
+ return new Promise(r =>
+ requestAnimationFrame(() => requestAnimationFrame(r))
+ );
+}
+
+add_task(async function () {
+ const url = "data:text/html," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popup = await openSelectPopup("click");
+ let arrowSB = popup.shadowRoot.querySelector(".menupopup-arrowscrollbox");
+ is(
+ arrowSB.getBoundingClientRect().width,
+ 600,
+ "Should be the right size"
+ );
+
+ // Trigger a layout change that would cause us to layout the popup again,
+ // and change our menulist to be zero-size so that the anchor rect
+ // codepath is used. We should still use the anchor rect to expand our
+ // size.
+ await tick();
+
+ popup.closest("menulist").style.width = "0";
+ popup.style.minWidth = "2px";
+
+ await tick();
+
+ is(
+ arrowSB.getBoundingClientRect().width,
+ 600,
+ "Should be the right size"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/forms/browser_selectpopup_xhtml.js b/browser/base/content/test/forms/browser_selectpopup_xhtml.js
new file mode 100644
index 0000000000..091649be89
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_xhtml.js
@@ -0,0 +1,36 @@
+const PAGE = `<?xml version="1.0"?>
+<html id="main-window"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.w3.org/1999/xhtml">
+<head/>
+<body>
+ <html:select>
+ <html:option>abc</html:option>
+ <html:optgroup>
+ <html:option>defg</html:option>
+ </html:optgroup>
+ </html:select>
+</body>
+</html>
+`;
+
+add_task(async function () {
+ const url = "data:application/xhtml+xml," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popup = await openSelectPopup("click");
+ let menuitems = popup.querySelectorAll("menuitem");
+ is(menuitems.length, 2, "Should've properly detected two menu items");
+ is(menuitems[0].textContent, "abc", "Option text should be correct");
+ is(menuitems[1].textContent, "defg", "Second text should be correct");
+ ok(
+ !!popup.querySelector("menucaption"),
+ "Should've created a caption for the optgroup"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/forms/head.js b/browser/base/content/test/forms/head.js
new file mode 100644
index 0000000000..1629c6a57c
--- /dev/null
+++ b/browser/base/content/test/forms/head.js
@@ -0,0 +1,51 @@
+async function openSelectPopup(
+ mode = "key",
+ selector = "select",
+ win = window
+) {
+ info("Opening select popup");
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(win);
+ if (mode == "click" || mode == "mousedown") {
+ let mousePromise;
+ if (mode == "click") {
+ mousePromise = BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ {},
+ win.gBrowser.selectedBrowser
+ );
+ } else {
+ mousePromise = BrowserTestUtils.synthesizeMouse(
+ selector,
+ 5,
+ 5,
+ { type: "mousedown" },
+ win.gBrowser.selectedBrowser
+ );
+ }
+ await mousePromise;
+ } else {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ }
+ return popupShownPromise;
+}
+
+function hideSelectPopup(mode = "enter", win = window) {
+ let browser = win.gBrowser.selectedBrowser;
+ let selectClosedPromise = SpecialPowers.spawn(browser, [], async function () {
+ let { SelectContentHelper } = ChromeUtils.importESModule(
+ "resource://gre/actors/SelectChild.sys.mjs"
+ );
+ return ContentTaskUtils.waitForCondition(() => !SelectContentHelper.open);
+ });
+
+ if (mode == "escape") {
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+ } else if (mode == "enter") {
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ } else if (mode == "click") {
+ let popup = win.document.getElementById("ContentSelectDropdown").menupopup;
+ EventUtils.synthesizeMouseAtCenter(popup.lastElementChild, {}, win);
+ }
+
+ return selectClosedPromise;
+}
diff --git a/browser/base/content/test/fullscreen/FullscreenFrame.sys.mjs b/browser/base/content/test/fullscreen/FullscreenFrame.sys.mjs
new file mode 100644
index 0000000000..9821837b3f
--- /dev/null
+++ b/browser/base/content/test/fullscreen/FullscreenFrame.sys.mjs
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * test helper JSWindowActors used by the browser_fullscreen_api_fission.js test.
+ */
+
+export class FullscreenFrameChild extends JSWindowActorChild {
+ actorCreated() {
+ this.fullscreen_events = [];
+ }
+
+ changed() {
+ return new Promise(resolve => {
+ this.contentWindow.document.addEventListener(
+ "fullscreenchange",
+ () => resolve(),
+ {
+ once: true,
+ }
+ );
+ });
+ }
+
+ requestFullscreen() {
+ let doc = this.contentWindow.document;
+ let button = doc.createElement("button");
+ doc.body.appendChild(button);
+
+ return new Promise(resolve => {
+ button.onclick = () => {
+ doc.body.requestFullscreen().then(resolve);
+ doc.body.removeChild(button);
+ };
+ button.click();
+ });
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "WaitForChange":
+ return this.changed();
+ case "ExitFullscreen":
+ return this.contentWindow.document.exitFullscreen();
+ case "RequestFullscreen":
+ this.browsingContext.isActive = true;
+ return Promise.all([this.changed(), this.requestFullscreen()]);
+ case "CreateChild":
+ let child = msg.data;
+ let iframe = this.contentWindow.document.createElement("iframe");
+ iframe.allow = child.allow_fullscreen ? "fullscreen" : "";
+ iframe.name = child.name;
+
+ let loaded = new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ () => resolve(iframe.browsingContext),
+ { once: true }
+ );
+ });
+ iframe.src = child.url;
+ this.contentWindow.document.body.appendChild(iframe);
+ return loaded;
+ case "GetEvents":
+ return Promise.resolve(this.fullscreen_events);
+ case "ClearEvents":
+ this.fullscreen_events = [];
+ return Promise.resolve();
+ case "GetFullscreenElement":
+ let document = this.contentWindow.document;
+ let child_iframe = this.contentWindow.document.getElementsByTagName(
+ "iframe"
+ )
+ ? this.contentWindow.document.getElementsByTagName("iframe")[0]
+ : null;
+ switch (document.fullscreenElement) {
+ case null:
+ return Promise.resolve("null");
+ case document:
+ return Promise.resolve("document");
+ case document.body:
+ return Promise.resolve("body");
+ case child_iframe:
+ return Promise.resolve("child_iframe");
+ default:
+ return Promise.resolve("other");
+ }
+ }
+
+ return Promise.reject("Unexpected Message");
+ }
+
+ async handleEvent(event) {
+ switch (event.type) {
+ case "fullscreenchange":
+ this.fullscreen_events.push(true);
+ break;
+ case "fullscreenerror":
+ this.fullscreen_events.push(false);
+ break;
+ }
+ }
+}
diff --git a/browser/base/content/test/fullscreen/browser.ini b/browser/base/content/test/fullscreen/browser.ini
new file mode 100644
index 0000000000..8eecb3f99c
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser.ini
@@ -0,0 +1,31 @@
+[DEFAULT]
+support-files =
+ head.js
+ open_and_focus_helper.html
+
+[browser_bug1557041.js]
+[browser_bug1620341.js]
+support-files = fullscreen.html fullscreen_frame.html
+[browser_fullscreen_api_fission.js]
+https_first_disabled = true
+support-files = fullscreen.html FullscreenFrame.sys.mjs
+[browser_fullscreen_context_menu.js]
+[browser_fullscreen_cross_origin.js]
+support-files = fullscreen.html fullscreen_frame.html
+[browser_fullscreen_enterInUrlbar.js]
+skip-if = (os == 'mac') || (os == 'linux') # Bug 1648649
+[browser_fullscreen_from_minimize.js]
+skip-if = (os == 'linux') || (os == 'win') # Bug 1818795 and Bug 1818796
+[browser_fullscreen_keydown_reservation.js]
+[browser_fullscreen_menus.js]
+[browser_fullscreen_newtab.js]
+[browser_fullscreen_newwindow.js]
+[browser_fullscreen_permissions_prompt.js]
+[browser_fullscreen_warning.js]
+support-files = fullscreen.html
+skip-if =
+ (os == 'mac') # Bug 1848423
+[browser_fullscreen_window_focus.js]
+skip-if = (os == 'mac') && debug # Bug 1568570
+[browser_fullscreen_window_open.js]
+skip-if = (os == 'linux') && swgl # Bug 1795491
diff --git a/browser/base/content/test/fullscreen/browser_bug1557041.js b/browser/base/content/test/fullscreen/browser_bug1557041.js
new file mode 100644
index 0000000000..3f00de86a0
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_bug1557041.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test tends to trigger a race in the fullscreen time telemetry,
+// where the fullscreen enter and fullscreen exit events (which use the
+// same histogram ID) overlap. That causes TelemetryStopwatch to log an
+// error.
+SimpleTest.ignoreAllUncaughtExceptions(true);
+
+add_task(async function test_identityPopupCausesFSExit() {
+ let url = "https://example.com/";
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.loadURIString(browser, url);
+ await loaded;
+
+ let identityPermissionBox = document.getElementById(
+ "identity-permission-box"
+ );
+
+ info("Entering DOM fullscreen");
+ await changeFullscreen(browser, true);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == document.getElementById("permission-popup")
+ );
+ let fsExit = waitForFullScreenState(browser, false);
+
+ identityPermissionBox.click();
+
+ info("Waiting for fullscreen exit and permission popup to show");
+ await Promise.all([fsExit, popupShown]);
+
+ let identityPopup = document.getElementById("permission-popup");
+ ok(
+ identityPopup.hasAttribute("panelopen"),
+ "Identity popup should be open"
+ );
+ ok(!window.fullScreen, "Should not be in full-screen");
+ });
+});
diff --git a/browser/base/content/test/fullscreen/browser_bug1620341.js b/browser/base/content/test/fullscreen/browser_bug1620341.js
new file mode 100644
index 0000000000..bd836eb5a3
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_bug1620341.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const tab1URL = `data:text/html,
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>First tab to be loaded</title>
+ </head>
+ <body>
+ <button>JUST A BUTTON</button>
+ </body>
+ </html>`;
+
+const ORIGIN =
+ "https://example.com/browser/browser/base/content/test/fullscreen/fullscreen_frame.html";
+
+add_task(async function test_fullscreen_cross_origin() {
+ async function requestFullscreenThenCloseTab() {
+ await BrowserTestUtils.withNewTab(ORIGIN, async function (browser) {
+ info("Start fullscreen on iframe frameAllowed");
+
+ // Make sure there is no attribute "inDOMFullscreen" before requesting fullscreen.
+ await TestUtils.waitForCondition(
+ () => !document.documentElement.hasAttribute("inDOMFullscreen")
+ );
+
+ let tabbrowser = browser.ownerDocument.querySelector("#tabbrowser-tabs");
+ ok(
+ !tabbrowser.hasAttribute("closebuttons"),
+ "Close buttons should be visible on every tab"
+ );
+
+ // Request fullscreen from iframe
+ await SpecialPowers.spawn(browser, [], async function () {
+ let frame = content.document.getElementById("frameAllowed");
+ frame.focus();
+ await SpecialPowers.spawn(frame, [], async () => {
+ let frameDoc = content.document;
+ const waitForFullscreen = new Promise(resolve => {
+ const message = "fullscreenchange";
+ function handler(evt) {
+ frameDoc.removeEventListener(message, handler);
+ Assert.equal(evt.type, message, `Request should be allowed`);
+ resolve();
+ }
+ frameDoc.addEventListener(message, handler);
+ });
+
+ frameDoc.getElementById("request").click();
+ await waitForFullscreen;
+ });
+ });
+
+ // Make sure there is attribute "inDOMFullscreen" after requesting fullscreen.
+ await TestUtils.waitForCondition(() =>
+ document.documentElement.hasAttribute("inDOMFullscreen")
+ );
+
+ await TestUtils.waitForCondition(
+ () => tabbrowser.hasAttribute("closebuttons"),
+ "Close buttons should be visible only on the active tab (tabs have width=0 so closebuttons gets set on them)"
+ );
+ });
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ });
+
+ // Open a tab with tab1URL.
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ tab1URL,
+ true
+ );
+
+ // 1. Open another tab and load a page with two iframes.
+ // 2. Request fullscreen from an iframe which is in a different origin.
+ // 3. Close the tab after receiving "fullscreenchange" message.
+ // Note that we don't do "doc.exitFullscreen()" before closing the tab
+ // on purpose.
+ await requestFullscreenThenCloseTab();
+
+ // Wait until attribute "inDOMFullscreen" is removed.
+ await TestUtils.waitForCondition(
+ () => !document.documentElement.hasAttribute("inDOMFullscreen")
+ );
+
+ await TestUtils.waitForCondition(
+ () => !gBrowser.tabContainer.hasAttribute("closebuttons"),
+ "Close buttons should come back to every tab"
+ );
+
+ // Remove the remaining tab and leave the test.
+ let tabClosed = BrowserTestUtils.waitForTabClosing(tab1);
+ BrowserTestUtils.removeTab(tab1);
+ await tabClosed;
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js b/browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js
new file mode 100644
index 0000000000..03b65ddc0e
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js
@@ -0,0 +1,252 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test checks that `document.fullscreenElement` is set correctly and
+ * proper fullscreenchange events fire when an element inside of a
+ * multi-origin tree of iframes calls `requestFullscreen()`. It is designed
+ * to make sure the fullscreen API is working properly in fission when the
+ * frame tree spans multiple processes.
+ *
+ * A similarly purposed Web Platform Test exists, but at the time of writing
+ * is manual, so it cannot be run in CI:
+ * `element-request-fullscreen-cross-origin-manual.sub.html`
+ */
+
+"use strict";
+
+const actorModuleURI = getRootDirectory(gTestPath) + "FullscreenFrame.sys.mjs";
+const actorName = "FullscreenFrame";
+
+const fullscreenPath =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", "") +
+ "fullscreen.html";
+
+const fullscreenTarget = "D";
+// TOP
+// | \
+// A B
+// |
+// C
+// |
+// D
+// |
+// E
+const frameTree = {
+ name: "TOP",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: `http://example.com${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "A",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: `http://example.org${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "C",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: `http://example.com${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "D",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: `http://example.com${fullscreenPath}?different-uri=1`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "E",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: `http://example.org${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: "B",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: `http://example.net${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [],
+ },
+ ],
+};
+
+add_task(async function test_fullscreen_api_cross_origin_tree() {
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ },
+ r
+ );
+ });
+
+ // Register a custom window actor to handle tracking events
+ // and constructing subframes
+ ChromeUtils.registerWindowActor(actorName, {
+ child: {
+ esModuleURI: actorModuleURI,
+ events: {
+ fullscreenchange: { mozSystemGroup: true, capture: true },
+ fullscreenerror: { mozSystemGroup: true, capture: true },
+ },
+ },
+ allFrames: true,
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: frameTree.url,
+ });
+
+ let frames = new Map();
+ async function construct_frame_children(browsingContext, tree) {
+ let actor = browsingContext.currentWindowGlobal.getActor(actorName);
+ frames.set(tree.name, {
+ browsingContext,
+ actor,
+ });
+
+ for (let child of tree.children) {
+ // Create the child IFrame and wait for it to load.
+ let childBC = await actor.sendQuery("CreateChild", child);
+ await construct_frame_children(childBC, child);
+ }
+ }
+
+ await construct_frame_children(tab.linkedBrowser.browsingContext, frameTree);
+
+ async function check_events(expected_events) {
+ for (let [name, expected] of expected_events) {
+ let actor = frames.get(name).actor;
+
+ // Each content process fires the fullscreenchange
+ // event independently and in parallel making it
+ // possible for the promises returned by
+ // `requestFullscreen` or `exitFullscreen` to
+ // resolve before all events have fired. We wait
+ // for the number of events to match before
+ // continuing to ensure we don't miss an expected
+ // event that hasn't fired yet.
+ let events;
+ await TestUtils.waitForCondition(async () => {
+ events = await actor.sendQuery("GetEvents");
+ return events.length == expected.length;
+ }, `Waiting for number of events to match`);
+
+ Assert.equal(events.length, expected.length, "Number of events equal");
+ events.forEach((value, i) => {
+ Assert.equal(value, expected[i], "Event type matches");
+ });
+ }
+ }
+
+ async function check_fullscreenElement(expected_elements) {
+ for (let [name, expected] of expected_elements) {
+ let element = await frames
+ .get(name)
+ .actor.sendQuery("GetFullscreenElement");
+ Assert.equal(element, expected, "The fullScreenElement matches");
+ }
+ }
+
+ // Trigger fullscreen from the target frame.
+ let target = frames.get(fullscreenTarget);
+ await target.actor.sendQuery("RequestFullscreen");
+ // true is fullscreenchange and false is fullscreenerror.
+ await check_events(
+ new Map([
+ ["TOP", [true]],
+ ["A", [true]],
+ ["B", []],
+ ["C", [true]],
+ ["D", [true]],
+ ["E", []],
+ ])
+ );
+ await check_fullscreenElement(
+ new Map([
+ ["TOP", "child_iframe"],
+ ["A", "child_iframe"],
+ ["B", "null"],
+ ["C", "child_iframe"],
+ ["D", "body"],
+ ["E", "null"],
+ ])
+ );
+
+ await target.actor.sendQuery("ExitFullscreen");
+ // fullscreenchange should have fired on exit as well.
+ // true is fullscreenchange and false is fullscreenerror.
+ await check_events(
+ new Map([
+ ["TOP", [true, true]],
+ ["A", [true, true]],
+ ["B", []],
+ ["C", [true, true]],
+ ["D", [true, true]],
+ ["E", []],
+ ])
+ );
+ await check_fullscreenElement(
+ new Map([
+ ["TOP", "null"],
+ ["A", "null"],
+ ["B", "null"],
+ ["C", "null"],
+ ["D", "null"],
+ ["E", "null"],
+ ])
+ );
+
+ // Clear previous events before testing exiting fullscreen with ESC.
+ for (const frame of frames.values()) {
+ frame.actor.sendQuery("ClearEvents");
+ }
+ await target.actor.sendQuery("RequestFullscreen");
+
+ // Escape should cause the proper events to fire and
+ // document.fullscreenElement should be cleared.
+ let finished_exiting = target.actor.sendQuery("WaitForChange");
+ EventUtils.sendKey("ESCAPE");
+ await finished_exiting;
+ // true is fullscreenchange and false is fullscreenerror.
+ await check_events(
+ new Map([
+ ["TOP", [true, true]],
+ ["A", [true, true]],
+ ["B", []],
+ ["C", [true, true]],
+ ["D", [true, true]],
+ ["E", []],
+ ])
+ );
+ await check_fullscreenElement(
+ new Map([
+ ["TOP", "null"],
+ ["A", "null"],
+ ["B", "null"],
+ ["C", "null"],
+ ["D", "null"],
+ ["E", "null"],
+ ])
+ );
+
+ // Remove the tests custom window actor.
+ ChromeUtils.unregisterWindowActor("FullscreenFrame");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js b/browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js
new file mode 100644
index 0000000000..ec874f1a3f
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function openContextMenu(itemElement, win = window) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ itemElement.ownerDocument,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ itemElement,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ let { target } = await popupShownPromise;
+ return target;
+}
+
+async function testContextMenu() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let panelUIMenuButton = document.getElementById("PanelUI-menu-button");
+ let contextMenu = await openContextMenu(panelUIMenuButton);
+ let array1 = AppConstants.MENUBAR_CAN_AUTOHIDE
+ ? [
+ ".customize-context-moveToPanel",
+ ".customize-context-removeFromToolbar",
+ "#toolbarItemsMenuSeparator",
+ "#toggle_toolbar-menubar",
+ "#toggle_PersonalToolbar",
+ "#viewToolbarsMenuSeparator",
+ ".viewCustomizeToolbar",
+ ]
+ : [
+ ".customize-context-moveToPanel",
+ ".customize-context-removeFromToolbar",
+ "#toolbarItemsMenuSeparator",
+ "#toggle_PersonalToolbar",
+ "#viewToolbarsMenuSeparator",
+ ".viewCustomizeToolbar",
+ ];
+ let result1 = verifyContextMenu(contextMenu, array1);
+ ok(!result1, "Expected no errors verifying context menu items");
+ contextMenu.hidePopup();
+ let onFullscreen = Promise.all([
+ BrowserTestUtils.waitForEvent(window, "fullscreen"),
+ BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange",
+ false,
+ e => window.fullScreen
+ ),
+ BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden"),
+ ]);
+ document.getElementById("View:FullScreen").doCommand();
+ contextMenu.hidePopup();
+ info("waiting for fullscreen");
+ await onFullscreen;
+ // make sure the toolbox is visible if it's autohidden
+ document.getElementById("Browser:OpenLocation").doCommand();
+ info("trigger the context menu");
+ let contextMenu2 = await openContextMenu(panelUIMenuButton);
+ info("context menu should be open, verify its menu items");
+ let array2 = AppConstants.MENUBAR_CAN_AUTOHIDE
+ ? [
+ ".customize-context-moveToPanel",
+ ".customize-context-removeFromToolbar",
+ "#toolbarItemsMenuSeparator",
+ "#toggle_toolbar-menubar",
+ "#toggle_PersonalToolbar",
+ "#viewToolbarsMenuSeparator",
+ ".viewCustomizeToolbar",
+ `menuseparator[contexttype="fullscreen"]`,
+ `.fullscreen-context-autohide`,
+ `menuitem[contexttype="fullscreen"]`,
+ ]
+ : [
+ ".customize-context-moveToPanel",
+ ".customize-context-removeFromToolbar",
+ "#toolbarItemsMenuSeparator",
+ "#toggle_PersonalToolbar",
+ "#viewToolbarsMenuSeparator",
+ ".viewCustomizeToolbar",
+ `menuseparator[contexttype="fullscreen"]`,
+ `.fullscreen-context-autohide`,
+ `menuitem[contexttype="fullscreen"]`,
+ ];
+ let result2 = verifyContextMenu(contextMenu2, array2);
+ ok(!result2, "Expected no errors verifying context menu items");
+ let onExitFullscreen = Promise.all([
+ BrowserTestUtils.waitForEvent(window, "fullscreen"),
+ BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange",
+ false,
+ e => !window.fullScreen
+ ),
+ BrowserTestUtils.waitForPopupEvent(contextMenu2, "hidden"),
+ ]);
+ document.getElementById("View:FullScreen").doCommand();
+ contextMenu2.hidePopup();
+ await onExitFullscreen;
+ });
+}
+
+function verifyContextMenu(contextMenu, itemSelectors) {
+ // Ignore hidden nodes
+ let items = Array.from(contextMenu.children).filter(n =>
+ BrowserTestUtils.is_visible(n)
+ );
+ let menuAsText = items
+ .map(n => {
+ return n.nodeName == "menuseparator"
+ ? "---"
+ : `${n.label} (${n.command})`;
+ })
+ .join("\n");
+ info("Got actual context menu items: \n" + menuAsText);
+
+ try {
+ is(
+ items.length,
+ itemSelectors.length,
+ "Context menu has the expected number of items"
+ );
+ for (let i = 0; i < items.length; i++) {
+ let selector = itemSelectors[i];
+ ok(
+ items[i].matches(selector),
+ `Item at ${i} matches expected selector: ${selector}`
+ );
+ }
+ } catch (ex) {
+ return ex;
+ }
+ return null;
+}
+
+add_task(testContextMenu);
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_cross_origin.js b/browser/base/content/test/fullscreen/browser_fullscreen_cross_origin.js
new file mode 100644
index 0000000000..0babb8b35e
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_cross_origin.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ORIGIN =
+ "https://example.com/browser/browser/base/content/test/fullscreen/fullscreen_frame.html";
+
+add_task(async function test_fullscreen_cross_origin() {
+ async function requestFullscreen(aAllow, aExpect) {
+ await BrowserTestUtils.withNewTab(ORIGIN, async function (browser) {
+ const iframeId = aExpect == "allowed" ? "frameAllowed" : "frameDenied";
+
+ info("Start fullscreen on iframe " + iframeId);
+ await SpecialPowers.spawn(
+ browser,
+ [{ aExpect, iframeId }],
+ async function (args) {
+ let frame = content.document.getElementById(args.iframeId);
+ frame.focus();
+ await SpecialPowers.spawn(frame, [args.aExpect], async expect => {
+ let frameDoc = content.document;
+ const waitForFullscreen = new Promise(resolve => {
+ const message =
+ expect == "allowed" ? "fullscreenchange" : "fullscreenerror";
+ function handler(evt) {
+ frameDoc.removeEventListener(message, handler);
+ Assert.equal(evt.type, message, `Request should be ${expect}`);
+ frameDoc.exitFullscreen();
+ resolve();
+ }
+ frameDoc.addEventListener(message, handler);
+ });
+ frameDoc.getElementById("request").click();
+ await waitForFullscreen;
+ });
+ }
+ );
+
+ if (aExpect == "allowed") {
+ waitForFullScreenState(browser, false);
+ }
+ });
+ }
+
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ },
+ r
+ );
+ });
+
+ await requestFullscreen(undefined, "denied");
+ await requestFullscreen("fullscreen", "allowed");
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_enterInUrlbar.js b/browser/base/content/test/fullscreen/browser_fullscreen_enterInUrlbar.js
new file mode 100644
index 0000000000..6ece64a6f3
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_enterInUrlbar.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that when the user presses enter in the urlbar in full
+// screen, the toolbars are hidden. This should not be run on macOS because we
+// don't hide the toolbars there.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do the View:FullScreen command and wait for the transition.
+ let onFullscreen = BrowserTestUtils.waitForEvent(window, "fullscreen");
+ document.getElementById("View:FullScreen").doCommand();
+ await onFullscreen;
+
+ // Do the Browser:OpenLocation command to show the nav toolbox and focus
+ // the urlbar.
+ let onToolboxShown = TestUtils.topicObserved(
+ "fullscreen-nav-toolbox",
+ (subject, data) => data == "shown"
+ );
+ document.getElementById("Browser:OpenLocation").doCommand();
+ info("Waiting for the nav toolbox to be shown");
+ await onToolboxShown;
+
+ // Enter a URL.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ value: "http://example.com/",
+ waitForFocus: SimpleTest.waitForFocus,
+ fireInputEvent: true,
+ });
+
+ // Press enter and wait for the nav toolbox to be hidden.
+ let onToolboxHidden = TestUtils.topicObserved(
+ "fullscreen-nav-toolbox",
+ (subject, data) => data == "hidden"
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ info("Waiting for the nav toolbox to be hidden");
+ await onToolboxHidden;
+
+ Assert.ok(true, "Nav toolbox hidden");
+
+ info("Waiting for exiting from the fullscreen mode...");
+ onFullscreen = BrowserTestUtils.waitForEvent(window, "fullscreen");
+ document.getElementById("View:FullScreen").doCommand();
+ await onFullscreen;
+ });
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_from_minimize.js b/browser/base/content/test/fullscreen/browser_fullscreen_from_minimize.js
new file mode 100644
index 0000000000..c4ef8fe642
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_from_minimize.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks whether fullscreen windows can transition to minimized windows,
+// and back again. This is sometimes not directly supported by the OS widgets. For
+// example, in macOS, the minimize button is greyed-out in the title bar of
+// fullscreen windows, making this transition impossible for users to initiate.
+// Still, web APIs do allow arbitrary combinations of window calls, and this test
+// exercises some of those combinations.
+
+const restoreWindowToNormal = async () => {
+ // Get the window to normal state by calling window.restore(). This may take
+ // multiple attempts since a call to restore could bring the window to either
+ // NORMAL or MAXIMIZED state.
+ while (window.windowState != window.STATE_NORMAL) {
+ info(
+ `Calling window.restore(), to try to reach "normal" state ${window.STATE_NORMAL}.`
+ );
+ let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ window.restore();
+ await promiseSizeModeChange;
+ info(`Window reached state ${window.windowState}.`);
+ }
+};
+
+add_task(async function () {
+ registerCleanupFunction(function () {
+ window.restore();
+ });
+
+ // We reuse these variables to create new promises for each transition.
+ let promiseSizeModeChange;
+ let promiseFullscreen;
+
+ await restoreWindowToNormal();
+ ok(!window.fullScreen, "Window should not be fullscreen at start of test.");
+
+ // Get to fullscreen.
+ info("Requesting fullscreen.");
+ promiseFullscreen = document.documentElement.requestFullscreen();
+ await promiseFullscreen;
+ ok(window.fullScreen, "Window should be fullscreen before being minimized.");
+
+ // Transition between fullscreen and minimize states.
+ info("Requesting minimize on a fullscreen window.");
+ promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ window.minimize();
+ await promiseSizeModeChange;
+ is(
+ window.windowState,
+ window.STATE_MINIMIZED,
+ "Window should be minimized after fullscreen."
+ );
+
+ // Whether or not the previous transition worked, restore the window
+ // and then minimize it.
+ await restoreWindowToNormal();
+
+ info("Requesting minimize on a normal window.");
+ promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ window.minimize();
+ await promiseSizeModeChange;
+ is(
+ window.windowState,
+ window.STATE_MINIMIZED,
+ "Window should be minimized before fullscreen."
+ );
+
+ info("Requesting fullscreen on a minimized window.");
+ promiseFullscreen = document.documentElement.requestFullscreen();
+ await promiseFullscreen;
+ ok(window.fullScreen, "Window should be fullscreen after being minimized.");
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_keydown_reservation.js b/browser/base/content/test/fullscreen/browser_fullscreen_keydown_reservation.js
new file mode 100644
index 0000000000..2d34ac6c7b
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_keydown_reservation.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test verifies that whether shortcut keys of toggling fullscreen modes
+// are reserved.
+add_task(async function test_keydown_event_reservation_toggling_fullscreen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ ],
+ });
+
+ let shortcutKeys = [{ key: "KEY_F11", modifiers: {} }];
+ if (navigator.platform.startsWith("Mac")) {
+ shortcutKeys.push({
+ key: "f",
+ modifiers: { metaKey: true, ctrlKey: true },
+ });
+ shortcutKeys.push({
+ key: "F",
+ modifiers: { metaKey: true, shiftKey: true },
+ });
+ }
+ function shortcutDescription(aShortcutKey) {
+ return `${
+ aShortcutKey.metaKey ? "Meta + " : ""
+ }${aShortcutKey.shiftKey ? "Shift + " : ""}${aShortcutKey.ctrlKey ? "Ctrl + " : ""}${aShortcutKey.key.replace("KEY_", "")}`;
+ }
+ for (const shortcutKey of shortcutKeys) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org/browser/browser/base/content/test/fullscreen/fullscreen.html"
+ );
+
+ await SimpleTest.promiseFocus(tab.linkedBrowser);
+
+ const fullScreenEntered = BrowserTestUtils.waitForEvent(
+ window,
+ "fullscreen"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.wrappedJSObject.keydown = null;
+ content.window.addEventListener("keydown", event => {
+ switch (event.key) {
+ case "Shift":
+ case "Meta":
+ case "Control":
+ break;
+ default:
+ content.wrappedJSObject.keydown = event;
+ }
+ });
+ });
+
+ EventUtils.synthesizeKey(shortcutKey.key, shortcutKey.modifiers);
+
+ info(
+ `Waiting for entering the fullscreen mode with synthesizing ${shortcutDescription(
+ shortcutKey
+ )}...`
+ );
+ await fullScreenEntered;
+
+ info("Retrieving the result...");
+ Assert.ok(
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => !!content.wrappedJSObject.keydown
+ ),
+ `Entering the fullscreen mode with ${shortcutDescription(
+ shortcutKey
+ )} should cause "keydown" event`
+ );
+
+ const fullScreenExited = BrowserTestUtils.waitForEvent(
+ window,
+ "fullscreen"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.wrappedJSObject.keydown = null;
+ });
+
+ EventUtils.synthesizeKey(shortcutKey.key, shortcutKey.modifiers);
+
+ info(
+ `Waiting for exiting from the fullscreen mode with synthesizing ${shortcutDescription(
+ shortcutKey
+ )}...`
+ );
+ await fullScreenExited;
+
+ info("Retrieving the result...");
+ Assert.ok(
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => !content.wrappedJSObject.keydown
+ ),
+ `Exiting from the fullscreen mode with ${shortcutDescription(
+ shortcutKey
+ )} should not cause "keydown" event`
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_menus.js b/browser/base/content/test/fullscreen/browser_fullscreen_menus.js
new file mode 100644
index 0000000000..90dd06192d
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_menus.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_shortcut_key_label_in_fullscreen_menu_item() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ ],
+ });
+
+ const isMac = AppConstants.platform == "macosx";
+ const shortCutKeyLabel = isMac ? "\u2303\u2318F" : "F11";
+ const enterMenuItemId = isMac ? "enterFullScreenItem" : "fullScreenItem";
+ const exitMenuItemId = isMac ? "exitFullScreenItem" : "fullScreenItem";
+ const accelKeyLabelSelector = ".menu-accel-container > label";
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org/browser/browser/base/content/test/fullscreen/fullscreen.html"
+ );
+
+ await SimpleTest.promiseFocus(tab.linkedBrowser);
+
+ document.getElementById(enterMenuItemId).render();
+ Assert.equal(
+ document
+ .getElementById(enterMenuItemId)
+ .querySelector(accelKeyLabelSelector)
+ ?.getAttribute("value"),
+ shortCutKeyLabel,
+ `The menu item to enter into the fullscreen mode should show a shortcut key`
+ );
+
+ const fullScreenEntered = BrowserTestUtils.waitForEvent(window, "fullscreen");
+
+ EventUtils.synthesizeKey("KEY_F11", {});
+
+ info(`Waiting for entering the fullscreen mode...`);
+ await fullScreenEntered;
+
+ document.getElementById(exitMenuItemId).render();
+ Assert.equal(
+ document
+ .getElementById(exitMenuItemId)
+ .querySelector(accelKeyLabelSelector)
+ ?.getAttribute("value"),
+ shortCutKeyLabel,
+ `The menu item to exiting from the fullscreen mode should show a shortcut key`
+ );
+
+ const fullScreenExited = BrowserTestUtils.waitForEvent(window, "fullscreen");
+
+ EventUtils.synthesizeKey("KEY_F11", {});
+
+ info(`Waiting for exiting from the fullscreen mode...`);
+ await fullScreenExited;
+
+ document.getElementById(enterMenuItemId).render();
+ Assert.equal(
+ document
+ .getElementById(enterMenuItemId)
+ .querySelector(accelKeyLabelSelector)
+ ?.getAttribute("value"),
+ shortCutKeyLabel,
+ `After exiting from the fullscreen mode, the menu item to enter the fullscreen mode should show a shortcut key`
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_newtab.js b/browser/base/content/test/fullscreen/browser_fullscreen_newtab.js
new file mode 100644
index 0000000000..d5a74a0aa3
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_newtab.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test verifies that when in fullscreen mode, and a new tab is opened,
+// fullscreen mode is exited and the url bar is focused.
+add_task(async function test_fullscreen_display_none() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org/browser/browser/base/content/test/fullscreen/fullscreen.html"
+ );
+
+ let fullScreenEntered = BrowserTestUtils.waitForEvent(
+ document,
+ "fullscreenchange",
+ false,
+ () => document.fullscreenElement
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.getElementById("request").click();
+ });
+
+ await fullScreenEntered;
+
+ let fullScreenExited = BrowserTestUtils.waitForEvent(
+ document,
+ "fullscreenchange",
+ false,
+ () => !document.fullscreenElement
+ );
+
+ let focusPromise = BrowserTestUtils.waitForEvent(window, "focus");
+ EventUtils.synthesizeKey("T", { accelKey: true });
+ await focusPromise;
+
+ is(
+ document.activeElement,
+ gURLBar.inputField,
+ "url bar is focused after new tab opened"
+ );
+
+ await fullScreenExited;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_newwindow.js b/browser/base/content/test/fullscreen/browser_fullscreen_newwindow.js
new file mode 100644
index 0000000000..dee02e2db0
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_newwindow.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test verifies that when in fullscreen mode, and a new window is opened,
+// fullscreen mode should not exit and the url bar is focused.
+add_task(async function test_fullscreen_new_window() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org/browser/browser/base/content/test/fullscreen/fullscreen.html"
+ );
+
+ let fullScreenEntered = BrowserTestUtils.waitForEvent(
+ document,
+ "fullscreenchange",
+ false,
+ () => document.fullscreenElement
+ );
+
+ // Enter fullscreen.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.getElementById("request").click();
+ });
+
+ await fullScreenEntered;
+
+ // Open a new window via ctrl+n.
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: "about:blank",
+ });
+ EventUtils.synthesizeKey("N", { accelKey: true });
+ let newWindow = await newWindowPromise;
+
+ // Check new window state.
+ is(
+ newWindow.document.activeElement,
+ newWindow.gURLBar.inputField,
+ "url bar is focused after new window opened"
+ );
+ ok(
+ !newWindow.fullScreen,
+ "The new chrome window should not be in fullscreen"
+ );
+ ok(
+ !newWindow.document.documentElement.hasAttribute("inDOMFullscreen"),
+ "The new chrome document should not be in fullscreen"
+ );
+
+ // Wait a bit then check the original window state.
+ await new Promise(resolve => TestUtils.executeSoon(resolve));
+ ok(
+ window.fullScreen,
+ "The original chrome window should be still in fullscreen"
+ );
+ ok(
+ document.documentElement.hasAttribute("inDOMFullscreen"),
+ "The original chrome document should be still in fullscreen"
+ );
+
+ // Close new window and move focus back to original window.
+ await BrowserTestUtils.closeWindow(newWindow);
+ await SimpleTest.promiseFocus(window);
+
+ // Exit fullscreen on original window.
+ let fullScreenExited = BrowserTestUtils.waitForEvent(
+ document,
+ "fullscreenchange",
+ false,
+ () => !document.fullscreenElement
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ await fullScreenExited;
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_permissions_prompt.js b/browser/base/content/test/fullscreen/browser_fullscreen_permissions_prompt.js
new file mode 100644
index 0000000000..82f0c97631
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_permissions_prompt.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test tends to trigger a race in the fullscreen time telemetry,
+// where the fullscreen enter and fullscreen exit events (which use the
+// same histogram ID) overlap. That causes TelemetryStopwatch to log an
+// error.
+SimpleTest.ignoreAllUncaughtExceptions(true);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/Not in fullscreen mode/);
+
+SimpleTest.requestCompleteLog();
+
+async function requestNotificationPermission(browser) {
+ return SpecialPowers.spawn(browser, [], () => {
+ return content.Notification.requestPermission();
+ });
+}
+
+async function requestCameraPermission(browser) {
+ return SpecialPowers.spawn(browser, [], () =>
+ content.navigator.mediaDevices
+ .getUserMedia({ video: true, fake: true })
+ .then(
+ () => true,
+ () => false
+ )
+ );
+}
+
+add_task(async function test_fullscreen_closes_permissionui_prompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.webnotifications.requireuserinteraction", false],
+ ["permissions.fullscreen.allowed", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ let browser = tab.linkedBrowser;
+
+ let popupShown, requestResult, popupHidden;
+
+ popupShown = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popupshown"
+ );
+
+ info("Requesting notification permission");
+ requestResult = requestNotificationPermission(browser);
+ await popupShown;
+
+ info("Entering DOM full-screen");
+ popupHidden = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ await changeFullscreen(browser, true);
+
+ await popupHidden;
+
+ is(
+ await requestResult,
+ "default",
+ "Expect permission request to be cancelled"
+ );
+
+ await changeFullscreen(browser, false);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_fullscreen_closes_webrtc_permission_prompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.navigator.permission.fake", true],
+ ["media.navigator.permission.force", true],
+ ["permissions.fullscreen.allowed", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ let browser = tab.linkedBrowser;
+ let popupShown, requestResult, popupHidden;
+
+ popupShown = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popupshown"
+ );
+
+ info("Requesting camera permission");
+ requestResult = requestCameraPermission(browser);
+
+ await popupShown;
+
+ info("Entering DOM full-screen");
+ popupHidden = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popuphidden"
+ );
+ await changeFullscreen(browser, true);
+
+ await popupHidden;
+
+ is(
+ await requestResult,
+ false,
+ "Expect webrtc permission request to be cancelled"
+ );
+
+ await changeFullscreen(browser, false);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_permission_prompt_closes_fullscreen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.webnotifications.requireuserinteraction", false],
+ ["permissions.fullscreen.allowed", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ let browser = tab.linkedBrowser;
+ info("Entering DOM full-screen");
+ await changeFullscreen(browser, true);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popupshown"
+ );
+ let fullScreenExit = waitForFullScreenState(browser, false);
+
+ info("Requesting notification permission");
+ requestNotificationPermission(browser).catch(() => {});
+ await popupShown;
+
+ info("Waiting for full-screen exit");
+ await fullScreenExit;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_warning.js b/browser/base/content/test/fullscreen/browser_fullscreen_warning.js
new file mode 100644
index 0000000000..b8bab5f90c
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_warning.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function checkWarningState(aWarningElement, aExpectedState, aMsg) {
+ ["hidden", "ontop", "onscreen"].forEach(state => {
+ is(
+ aWarningElement.hasAttribute(state),
+ state == aExpectedState,
+ `${aMsg} - check ${state} attribute.`
+ );
+ });
+}
+
+async function waitForWarningState(aWarningElement, aExpectedState) {
+ await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, "");
+ checkWarningState(
+ aWarningElement,
+ aExpectedState,
+ `Wait for ${aExpectedState} state`
+ );
+}
+
+add_setup(async function init() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ });
+});
+
+add_task(async function test_fullscreen_display_none() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Fullscreen Test</title>
+ </head>
+ <body id="body">
+ <iframe
+ src="https://example.org/browser/browser/base/content/test/fullscreen/fullscreen.html"
+ hidden
+ allowfullscreen></iframe>
+ </body>
+ </html>`,
+ },
+ async function (browser) {
+ let warning = document.getElementById("fullscreen-warning");
+ checkWarningState(
+ warning,
+ "hidden",
+ "Should not show full screen warning initially"
+ );
+
+ let warningShownPromise = waitForWarningState(warning, "onscreen");
+ // Enter fullscreen
+ await SpecialPowers.spawn(browser, [], async () => {
+ let frame = content.document.querySelector("iframe");
+ frame.focus();
+ await SpecialPowers.spawn(frame, [], () => {
+ content.document.getElementById("request").click();
+ });
+ });
+ await warningShownPromise;
+ ok(true, "Fullscreen warning shown");
+ // Exit fullscreen
+ let exitFullscreenPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "fullscreenchange",
+ false,
+ () => !document.fullscreenElement
+ );
+ document.getElementById("fullscreen-exit-button").click();
+ await exitFullscreenPromise;
+
+ checkWarningState(
+ warning,
+ "hidden",
+ "Should hide fullscreen warning after exiting fullscreen"
+ );
+ }
+ );
+});
+
+add_task(async function test_fullscreen_pointerlock_conflict() {
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ let fsWarning = document.getElementById("fullscreen-warning");
+ let plWarning = document.getElementById("pointerlock-warning");
+
+ checkWarningState(
+ fsWarning,
+ "hidden",
+ "Should not show full screen warning initially"
+ );
+ checkWarningState(
+ plWarning,
+ "hidden",
+ "Should not show pointer lock warning initially"
+ );
+
+ let fsWarningShownPromise = waitForWarningState(fsWarning, "onscreen");
+ info("Entering full screen and pointer lock.");
+ await SpecialPowers.spawn(browser, [], async () => {
+ await content.document.body.requestFullscreen();
+ await content.document.body.requestPointerLock();
+ });
+
+ await fsWarningShownPromise;
+ checkWarningState(
+ plWarning,
+ "hidden",
+ "Should not show pointer lock warning"
+ );
+
+ info("Exiting pointerlock");
+ await SpecialPowers.spawn(browser, [], async () => {
+ await content.document.exitPointerLock();
+ });
+
+ checkWarningState(
+ fsWarning,
+ "onscreen",
+ "Should still show full screen warning"
+ );
+ checkWarningState(
+ plWarning,
+ "hidden",
+ "Should not show pointer lock warning"
+ );
+
+ // Cleanup
+ info("Exiting fullscreen");
+ await document.exitFullscreen();
+ });
+});
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1821884
+add_task(async function test_reshow_fullscreen_notification() {
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let fsWarning = document.getElementById("fullscreen-warning");
+
+ info("Entering full screen and wait for the fullscreen warning to appear.");
+ await SimpleTest.promiseFocus(window);
+ await Promise.all([
+ waitForWarningState(fsWarning, "onscreen"),
+ BrowserTestUtils.waitForEvent(fsWarning, "transitionend"),
+ SpecialPowers.spawn(browser, [], async () => {
+ content.document.body.requestFullscreen();
+ }),
+ ]);
+
+ info(
+ "Switch focus away from the fullscreen window, the fullscreen warning should still hide automatically."
+ );
+ await Promise.all([
+ waitForWarningState(fsWarning, "hidden"),
+ SimpleTest.promiseFocus(newWin),
+ ]);
+
+ info(
+ "Switch focus back to the fullscreen window, the fullscreen warning should show again."
+ );
+ await Promise.all([
+ waitForWarningState(fsWarning, "onscreen"),
+ SimpleTest.promiseFocus(window),
+ ]);
+
+ info("Wait for fullscreen warning timed out.");
+ await waitForWarningState(fsWarning, "hidden");
+
+ info("The fullscreen warning should not show again.");
+ await SimpleTest.promiseFocus(newWin);
+ await SimpleTest.promiseFocus(window);
+ await new Promise(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+ checkWarningState(
+ fsWarning,
+ "hidden",
+ "The fullscreen warning should not show."
+ );
+
+ info("Close new browser window.");
+ await BrowserTestUtils.closeWindow(newWin);
+
+ info("Exit fullscreen.");
+ await document.exitFullscreen();
+ });
+});
+
+add_task(async function test_fullscreen_reappear() {
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ let fsWarning = document.getElementById("fullscreen-warning");
+
+ info("Entering full screen and wait for the fullscreen warning to appear.");
+ await Promise.all([
+ waitForWarningState(fsWarning, "onscreen"),
+ SpecialPowers.spawn(browser, [], async () => {
+ content.document.body.requestFullscreen();
+ }),
+ ]);
+
+ info("Wait for fullscreen warning timed out.");
+ await waitForWarningState(fsWarning, "hidden");
+
+ info("Move mouse to the top of screen.");
+ await Promise.all([
+ waitForWarningState(fsWarning, "ontop"),
+ EventUtils.synthesizeMouse(document.documentElement, 100, 0, {
+ type: "mousemove",
+ }),
+ ]);
+
+ info("Wait for fullscreen warning timed out again.");
+ await waitForWarningState(fsWarning, "hidden");
+
+ info("Exit fullscreen.");
+ await document.exitFullscreen();
+ });
+});
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1847901
+add_task(async function test_fullscreen_warning_disabled() {
+ // Disable fullscreen warning
+ await SpecialPowers.pushPrefEnv({
+ set: [["full-screen-api.warning.timeout", 0]],
+ });
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let fsWarning = document.getElementById("fullscreen-warning");
+ let mut = new MutationObserver(mutations => {
+ ok(false, `${mutations[0].attributeName} attribute should not change`);
+ });
+ mut.observe(fsWarning, {
+ attributeFilter: ["hidden", "onscreen", "ontop"],
+ });
+
+ info("Entering full screen.");
+ await SimpleTest.promiseFocus(window);
+ await SpecialPowers.spawn(browser, [], async () => {
+ return content.document.body.requestFullscreen();
+ });
+ // Wait a bit to ensure no state change.
+ await new Promise(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+
+ info("The fullscreen warning should still not show after switching focus.");
+ await SimpleTest.promiseFocus(newWin);
+ await SimpleTest.promiseFocus(window);
+ // Wait a bit to ensure no state change.
+ await new Promise(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+
+ mut.disconnect();
+
+ info("Close new browser window.");
+ await BrowserTestUtils.closeWindow(newWin);
+
+ info("Exit fullscreen.");
+ await document.exitFullscreen();
+ });
+
+ // Revert the setting to avoid affecting subsequent tests.
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js b/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js
new file mode 100644
index 0000000000..7c935f64d3
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function pause() {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, 500));
+}
+
+// This test tends to trigger a race in the fullscreen time telemetry,
+// where the fullscreen enter and fullscreen exit events (which use the
+// same histogram ID) overlap. That causes TelemetryStopwatch to log an
+// error.
+SimpleTest.ignoreAllUncaughtExceptions(true);
+
+const IFRAME_ID = "testIframe";
+
+async function testWindowFocus(isPopup, iframeID) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Calling window.open()");
+ let openedWindow = await jsWindowOpen(tab.linkedBrowser, isPopup, iframeID);
+ info("Letting OOP focus to stabilize");
+ await pause(); // Bug 1719659 for proper fix
+ info("re-focusing main window");
+ await waitForFocus(tab.linkedBrowser);
+
+ info("Entering full-screen");
+ await changeFullscreen(tab.linkedBrowser, true);
+
+ await testExpectFullScreenExit(tab.linkedBrowser, true, async () => {
+ info("Calling window.focus()");
+ await jsWindowFocus(tab.linkedBrowser, iframeID);
+ });
+
+ // Cleanup
+ if (isPopup) {
+ openedWindow.close();
+ } else {
+ BrowserTestUtils.removeTab(openedWindow);
+ }
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testWindowElementFocus(isPopup) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Calling window.open()");
+ let openedWindow = await jsWindowOpen(tab.linkedBrowser, isPopup);
+ info("Letting OOP focus to stabilize");
+ await pause(); // Bug 1719659 for proper fix
+ info("re-focusing main window");
+ await waitForFocus(tab.linkedBrowser);
+
+ info("Entering full-screen");
+ await changeFullscreen(tab.linkedBrowser, true);
+
+ await testExpectFullScreenExit(tab.linkedBrowser, false, async () => {
+ info("Calling element.focus() on popup");
+ await ContentTask.spawn(tab.linkedBrowser, {}, async args => {
+ await content.wrappedJSObject.sendMessage(
+ content.wrappedJSObject.openedWindow,
+ "elementfocus"
+ );
+ });
+ });
+
+ // Cleanup
+ await changeFullscreen(tab.linkedBrowser, false);
+ if (isPopup) {
+ openedWindow.close();
+ } else {
+ BrowserTestUtils.removeTab(openedWindow);
+ }
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.disable_open_during_load", false], // Allow window.focus calls without user interaction
+ ["browser.link.open_newwindow.disabled_in_fullscreen", false],
+ ],
+ });
+});
+
+add_task(function test_popupWindowFocus() {
+ return testWindowFocus(true);
+});
+
+add_task(function test_iframePopupWindowFocus() {
+ return testWindowFocus(true, IFRAME_ID);
+});
+
+add_task(function test_popupWindowElementFocus() {
+ return testWindowElementFocus(true);
+});
+
+add_task(function test_backgroundTabFocus() {
+ return testWindowFocus(false);
+});
+
+add_task(function test_iframebackgroundTabFocus() {
+ return testWindowFocus(false, IFRAME_ID);
+});
+
+add_task(function test_backgroundTabElementFocus() {
+ return testWindowElementFocus(false);
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_window_open.js b/browser/base/content/test/fullscreen/browser_fullscreen_window_open.js
new file mode 100644
index 0000000000..aafed57c75
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_window_open.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test tends to trigger a race in the fullscreen time telemetry,
+// where the fullscreen enter and fullscreen exit events (which use the
+// same histogram ID) overlap. That causes TelemetryStopwatch to log an
+// error.
+SimpleTest.ignoreAllUncaughtExceptions(true);
+SimpleTest.requestLongerTimeout(2);
+
+const IFRAME_ID = "testIframe";
+
+async function testWindowOpen(iframeID) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ info("Entering full-screen");
+ await changeFullscreen(tab.linkedBrowser, true);
+
+ let popup;
+ await testExpectFullScreenExit(tab.linkedBrowser, true, async () => {
+ info("Calling window.open()");
+ popup = await jsWindowOpen(tab.linkedBrowser, true, iframeID);
+ });
+
+ // Cleanup
+ await BrowserTestUtils.closeWindow(popup);
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testWindowOpenExistingWindow(funToOpenExitingWindow, iframeID) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ let popup = await jsWindowOpen(tab.linkedBrowser, true);
+
+ info("re-focusing main window");
+ await waitForFocus(tab.linkedBrowser);
+
+ info("Entering full-screen");
+ await changeFullscreen(tab.linkedBrowser, true);
+
+ info("open existing popup window");
+ await testExpectFullScreenExit(tab.linkedBrowser, true, async () => {
+ await funToOpenExitingWindow(tab.linkedBrowser, iframeID);
+ });
+
+ // Cleanup
+ await BrowserTestUtils.closeWindow(popup);
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.disable_open_during_load", false], // Allow window.open calls without user interaction
+ ["browser.link.open_newwindow.disabled_in_fullscreen", false],
+ ],
+ });
+});
+
+add_task(function test_parentWindowOpen() {
+ return testWindowOpen();
+});
+
+add_task(function test_iframeWindowOpen() {
+ return testWindowOpen(IFRAME_ID);
+});
+
+add_task(async function test_parentWindowOpenExistWindow() {
+ await testWindowOpenExistingWindow(browser => {
+ info(
+ "Calling window.open() with same name again should reuse the existing window"
+ );
+ jsWindowOpen(browser, true);
+ });
+});
+
+add_task(async function test_iframeWindowOpenExistWindow() {
+ await testWindowOpenExistingWindow((browser, iframeID) => {
+ info(
+ "Calling window.open() with same name again should reuse the existing window"
+ );
+ jsWindowOpen(browser, true, iframeID);
+ }, IFRAME_ID);
+});
+
+add_task(async function test_parentWindowClickLinkOpenExistWindow() {
+ await testWindowOpenExistingWindow(browser => {
+ info(
+ "Clicking link with same target name should reuse the existing window"
+ );
+ jsClickLink(browser, true);
+ });
+});
+
+add_task(async function test_iframeWindowClickLinkOpenExistWindow() {
+ await testWindowOpenExistingWindow((browser, iframeID) => {
+ info(
+ "Clicking link with same target name should reuse the existing window"
+ );
+ jsClickLink(browser, true, iframeID);
+ }, IFRAME_ID);
+});
diff --git a/browser/base/content/test/fullscreen/fullscreen.html b/browser/base/content/test/fullscreen/fullscreen.html
new file mode 100644
index 0000000000..8b4289bb36
--- /dev/null
+++ b/browser/base/content/test/fullscreen/fullscreen.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+function requestFScreen() {
+ document.body.requestFullscreen();
+}
+</script>
+<body>
+<button id="request" onclick="requestFScreen()"> Fullscreen </button>
+<button id="focus"> Fullscreen </button>
+</body>
+</html>
diff --git a/browser/base/content/test/fullscreen/fullscreen_frame.html b/browser/base/content/test/fullscreen/fullscreen_frame.html
new file mode 100644
index 0000000000..ca1b1a4dd8
--- /dev/null
+++ b/browser/base/content/test/fullscreen/fullscreen_frame.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <iframe id="frameAllowed"
+ src="https://example.org/browser/browser/base/content/test/fullscreen/fullscreen.html"
+ allowfullscreen></iframe>
+ <iframe id="frameDenied" src="https://example.org/browser/browser/base/content/test/fullscreen/fullscreen.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/fullscreen/head.js b/browser/base/content/test/fullscreen/head.js
new file mode 100644
index 0000000000..ce7a4b23b2
--- /dev/null
+++ b/browser/base/content/test/fullscreen/head.js
@@ -0,0 +1,164 @@
+const TEST_URL =
+ "https://example.com/browser/browser/base/content/test/fullscreen/open_and_focus_helper.html";
+
+function waitForFullScreenState(browser, state) {
+ return new Promise(resolve => {
+ let eventReceived = false;
+
+ let observe = (subject, topic, data) => {
+ if (!eventReceived) {
+ return;
+ }
+ Services.obs.removeObserver(observe, "fullscreen-painted");
+ resolve();
+ };
+ Services.obs.addObserver(observe, "fullscreen-painted");
+
+ browser.ownerGlobal.addEventListener(
+ `MozDOMFullscreen:${state ? "Entered" : "Exited"}`,
+ () => {
+ eventReceived = true;
+ },
+ { once: true }
+ );
+ });
+}
+
+/**
+ * Spawns content task in browser to enter / leave fullscreen
+ * @param browser - Browser to use for JS fullscreen requests
+ * @param {Boolean} fullscreenState - true to enter fullscreen, false to leave
+ * @returns {Promise} - Resolves once fullscreen change is applied
+ */
+async function changeFullscreen(browser, fullScreenState) {
+ await new Promise(resolve =>
+ SimpleTest.waitForFocus(resolve, browser.ownerGlobal)
+ );
+ let fullScreenChange = waitForFullScreenState(browser, fullScreenState);
+ SpecialPowers.spawn(browser, [fullScreenState], async state => {
+ // Wait for document focus before requesting full-screen
+ await ContentTaskUtils.waitForCondition(
+ () => content.browsingContext.isActive && content.document.hasFocus(),
+ "Waiting for document focus"
+ );
+ if (state) {
+ content.document.body.requestFullscreen();
+ } else {
+ content.document.exitFullscreen();
+ }
+ });
+ return fullScreenChange;
+}
+
+async function testExpectFullScreenExit(browser, leaveFS, action) {
+ let fsPromise = waitForFullScreenState(browser, false);
+ if (leaveFS) {
+ if (action) {
+ await action();
+ }
+ await fsPromise;
+ ok(true, "Should leave full-screen");
+ } else {
+ if (action) {
+ await action();
+ }
+ let result = await Promise.race([
+ fsPromise,
+ new Promise(resolve => {
+ SimpleTest.requestFlakyTimeout("Wait for failure condition");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => resolve(true), 2500);
+ }),
+ ]);
+ ok(result, "Should not leave full-screen");
+ }
+}
+
+function jsWindowFocus(browser, iframeId) {
+ return ContentTask.spawn(browser, { iframeId }, async args => {
+ let destWin = content;
+ if (args.iframeId) {
+ let iframe = content.document.getElementById(args.iframeId);
+ if (!iframe) {
+ throw new Error("iframe not set");
+ }
+ destWin = iframe.contentWindow;
+ }
+ await content.wrappedJSObject.sendMessage(destWin, "focus");
+ });
+}
+
+function jsElementFocus(browser, iframeId) {
+ return ContentTask.spawn(browser, { iframeId }, async args => {
+ let destWin = content;
+ if (args.iframeId) {
+ let iframe = content.document.getElementById(args.iframeId);
+ if (!iframe) {
+ throw new Error("iframe not set");
+ }
+ destWin = iframe.contentWindow;
+ }
+ await content.wrappedJSObject.sendMessage(destWin, "elementfocus");
+ });
+}
+
+async function jsWindowOpen(browser, isPopup, iframeId) {
+ //let windowOpened = BrowserTestUtils.waitForNewWindow();
+ let windowOpened = isPopup
+ ? BrowserTestUtils.waitForNewWindow({ url: TEST_URL })
+ : BrowserTestUtils.waitForNewTab(gBrowser, TEST_URL, true);
+ ContentTask.spawn(browser, { isPopup, iframeId }, async args => {
+ let destWin = content;
+ if (args.iframeId) {
+ // Create a cross origin iframe
+ destWin = (
+ await content.wrappedJSObject.createIframe(args.iframeId, true)
+ ).contentWindow;
+ }
+ // Send message to either the iframe or the current page to open a popup
+ await content.wrappedJSObject.sendMessage(
+ destWin,
+ args.isPopup ? "openpopup" : "open"
+ );
+ });
+ return windowOpened;
+}
+
+async function jsClickLink(browser, isPopup, iframeId) {
+ //let windowOpened = BrowserTestUtils.waitForNewWindow();
+ let windowOpened = isPopup
+ ? BrowserTestUtils.waitForNewWindow({ url: TEST_URL })
+ : BrowserTestUtils.waitForNewTab(gBrowser, TEST_URL, true);
+ ContentTask.spawn(browser, { isPopup, iframeId }, async args => {
+ let destWin = content;
+ if (args.iframeId) {
+ // Create a cross origin iframe
+ destWin = (
+ await content.wrappedJSObject.createIframe(args.iframeId, true)
+ ).contentWindow;
+ }
+ // Send message to either the iframe or the current page to click a link
+ await content.wrappedJSObject.sendMessage(destWin, "clicklink");
+ });
+ return windowOpened;
+}
+
+function waitForFocus(...args) {
+ return new Promise(resolve => SimpleTest.waitForFocus(resolve, ...args));
+}
+
+function waitForBrowserWindowActive(win) {
+ return new Promise(resolve => {
+ if (Services.focus.activeWindow == win) {
+ resolve();
+ } else {
+ win.addEventListener(
+ "activate",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+}
diff --git a/browser/base/content/test/fullscreen/open_and_focus_helper.html b/browser/base/content/test/fullscreen/open_and_focus_helper.html
new file mode 100644
index 0000000000..06d1800714
--- /dev/null
+++ b/browser/base/content/test/fullscreen/open_and_focus_helper.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset='utf-8'>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+ <input></input><br>
+ <a href="https://example.com" target="test">link</a>
+ <script>
+ const MY_ORIGIN = window.location.origin;
+ const CROSS_ORIGIN = "https://example.org";
+
+ // Creates an iframe with message channel to trigger window open and focus
+ window.createIframe = function(id, crossOrigin = false) {
+ return new Promise(resolve => {
+ const origin = crossOrigin ? CROSS_ORIGIN : MY_ORIGIN;
+ let iframe = document.createElement("iframe");
+ iframe.id = id;
+ iframe.src = origin + window.location.pathname;
+ iframe.onload = () => resolve(iframe);
+ document.body.appendChild(iframe);
+ });
+ }
+
+ window.sendMessage = function(destWin, msg) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = resolve;
+ destWin.postMessage(msg, "*", [channel.port2]);
+ });
+ }
+
+ window.onMessage = function(event) {
+ let canReply = event.ports && !!event.ports.length;
+ if(event.data === "open") {
+ window.openedWindow = window.open('https://example.com' + window.location.pathname);
+ if (canReply) event.ports[0].postMessage('opened');
+ } else if(event.data === "openpopup") {
+ window.openedWindow = window.open('https://example.com' + window.location.pathname, 'test', 'top=0,height=1, width=300');
+ if (canReply) event.ports[0].postMessage('popupopened');
+ } else if(event.data === "focus") {
+ window.openedWindow.focus();
+ if (canReply) event.ports[0].postMessage('focused');
+ } else if(event.data === "elementfocus") {
+ document.querySelector("input").focus();
+ if (canReply) event.ports[0].postMessage('elementfocused');
+ } else if(event.data === "clicklink") {
+ synthesizeMouseAtCenter(document.querySelector("a"), {});
+ if (canReply) event.ports[0].postMessage('linkclicked');
+ }
+ }
+ window.addEventListener('message', window.onMessage);
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/alltabslistener.html b/browser/base/content/test/general/alltabslistener.html
new file mode 100644
index 0000000000..166c31037a
--- /dev/null
+++ b/browser/base/content/test/general/alltabslistener.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Test page for bug 463387</title>
+</head>
+<body>
+<p>Test page for bug 463387</p>
+</body>
+</html>
diff --git a/browser/base/content/test/general/app_bug575561.html b/browser/base/content/test/general/app_bug575561.html
new file mode 100644
index 0000000000..13c525487e
--- /dev/null
+++ b/browser/base/content/test/general/app_bug575561.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=575561
+-->
+ <head>
+ <title>Test for links in app tabs</title>
+ </head>
+ <body>
+ <a href="http://example.com/browser/browser/base/content/test/general/dummy_page.html">same domain</a>
+ <a href="http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html">same domain (different subdomain)</a>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html">different domain</a>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html" target="foo">different domain (with target)</a>
+ <a href="http://www.example.com/browser/browser/base/content/test/general/dummy_page.html">same domain (www prefix)</a>
+ <a href="data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>">data: URI</a>
+ <iframe src="app_subframe_bug575561.html"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/app_subframe_bug575561.html b/browser/base/content/test/general/app_subframe_bug575561.html
new file mode 100644
index 0000000000..8690497ffb
--- /dev/null
+++ b/browser/base/content/test/general/app_subframe_bug575561.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=575561
+-->
+ <head>
+ <title>Test for links in app tab subframes</title>
+ </head>
+ <body>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html">different domain</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/audio.ogg b/browser/base/content/test/general/audio.ogg
new file mode 100644
index 0000000000..477544875d
--- /dev/null
+++ b/browser/base/content/test/general/audio.ogg
Binary files differ
diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini
new file mode 100644
index 0000000000..2e9438135a
--- /dev/null
+++ b/browser/base/content/test/general/browser.ini
@@ -0,0 +1,416 @@
+###############################################################################
+# DO NOT ADD MORE TESTS HERE. #
+# TRY ONE OF THE MORE TOPICAL SIBLING DIRECTORIES. #
+# THIS DIRECTORY HAS 200+ TESTS AND TAKES AGES TO RUN ON A DEBUG BUILD. #
+# PLEASE, FOR THE LOVE OF WHATEVER YOU HOLD DEAR, DO NOT ADD MORE TESTS HERE. #
+###############################################################################
+
+[DEFAULT]
+support-files =
+ alltabslistener.html
+ app_bug575561.html
+ app_subframe_bug575561.html
+ audio.ogg
+ browser_bug479408_sample.html
+ browser_star_hsts.sjs
+ browser_tab_dragdrop2_frame1.xhtml
+ browser_tab_dragdrop_embed.html
+ bug792517-2.html
+ bug792517.html
+ bug792517.sjs
+ clipboard_pastefile.html
+ download_page.html
+ download_page_1.txt
+ download_page_2.txt
+ download_with_content_disposition_header.sjs
+ dummy_page.html
+ file_documentnavigation_frameset.html
+ file_double_close_tab.html
+ file_fullscreen-window-open.html
+ file_with_link_to_http.html
+ head.js
+ moz.png
+ navigating_window_with_download.html
+ print_postdata.sjs
+ test_bug462673.html
+ test_bug628179.html
+ title_test.svg
+ unknownContentType_file.pif
+ unknownContentType_file.pif^headers^
+ video.ogg
+ web_video.html
+ web_video1.ogv
+ web_video1.ogv^headers^
+ !/image/test/mochitest/blue.png
+ !/toolkit/content/tests/browser/common/mockTransfer.js
+
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_accesskeys.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_addCertException.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_alltabslistener.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_backButtonFitts.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_beforeunload_duplicate_dialogs.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug1261299.js]
+skip-if =
+ os != "mac" # Because of tests for supporting Service Menu of macOS, bug 1261299
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug1297539.js]
+skip-if =
+ os != "mac" # Because of tests for supporting pasting from Service Menu of macOS, bug 1297539
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug1299667.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug321000.js]
+skip-if = true # browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug356571.js]
+skip-if =
+ verify && !debug && os == 'win'
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug380960.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug406216.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug417483.js]
+skip-if =
+ verify && debug && os == 'mac'
+ os == 'mac'
+ os == 'linux' #Bug 1444703
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug424101.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug427559.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug431826.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug432599.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug455852.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug462289.js]
+skip-if =
+ os == "mac"
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug462673.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug477014.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug479408.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug481560.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug484315.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug491431.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug495058.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug519216.js]
+skip-if = true # Bug 1478159
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug520538.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug521216.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug533232.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug537013.js]
+skip-if = true # bug 1393813
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug537474.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug563588.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug565575.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug567306.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug575561.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug577121.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug578534.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug579872.js]
+skip-if =
+ verify && debug && os == 'linux'
+ os == 'mac'
+ os == 'linux' && !debug #Bug 1448915
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug581253.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug585785.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug585830.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug594131.js]
+skip-if =
+ verify && debug && os == 'linux'
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug596687.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug597218.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug609700.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug623893.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug624734.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug664672.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug676619.js]
+support-files =
+ dummy.ics
+ dummy.ics^headers^
+ redirect_download.sjs
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug710878.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug724239.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug734076.js]
+skip-if =
+ verify && debug && os == 'linux'
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug749738.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug763468_perwindowpb.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug767836_perwindowpb.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug817947.js]
+skip-if =
+ os == 'linux' && !debug # Bug 1556066
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug832435.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug882977.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug963945.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_clipboard.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_clipboard_pastefile.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_contentAltClick.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_contentAreaClick.js]
+skip-if = true # Clicks in content don't go through contentAreaClick.
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_ctrlTab.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_datachoices_notification.js]
+skip-if =
+ !datareporting
+ verify && !debug && os == 'win'
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_documentnavigation.js]
+skip-if =
+ verify && !debug && os == 'linux'
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_domFullscreen_fullscreenMode.js]
+tags = fullscreen
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_double_close_tab.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_drag.js]
+skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_duplicateIDs.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_findbarClose.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_focusonkeydown.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_fullscreen-window-open.js]
+tags = fullscreen
+skip-if =
+ os == "linux" # Linux: Intermittent failures - bug 941575.
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_gestureSupport.js]
+support-files =
+ !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
+ !/gfx/layers/apz/test/mochitest/apz_test_utils.js
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_hide_removing.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_homeDrop.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_invalid_uri_back_forward_manipulation.js]
+skip-if =
+ os == 'mac' && socketprocess_networking
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_lastAccessedTab.js]
+skip-if =
+ os == "windows" # Disabled on Windows due to frequent failures (bug 969405)
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_menuButtonFitts.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_middleMouse_noJSPaste.js]
+https_first_disabled = true
+skip-if =
+ apple_silicon && !debug # Bug 1724711
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_minimize.js]
+skip-if =
+ apple_silicon && !debug # Bug 1725756
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_modifiedclick_inherit_principal.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_newTabDrop.js]
+https_first_disabled = true
+skip-if =
+ os == "linux" && fission && tsan # high frequency intermittent
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_newWindowDrop.js]
+https_first_disabled = true
+skip-if =
+ os == "win" && os_version == "6.1" # bug 1715862
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_new_http_window_opened_from_file_tab.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_newwindow_focus.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_plainTextLinks.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_printpreview.js]
+skip-if =
+ os == 'win'
+ os == 'linux' && os_version == '18.04' # Bug 1384127
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_private_browsing_window.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_private_no_prompt.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_refreshBlocker.js]
+skip-if =
+ os == "mac"
+ os == "linux" && !debug
+ os == "win" && bits == 32 # Bug 1559410 for all instances
+support-files =
+ refresh_header.sjs
+ refresh_meta.sjs
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_relatedTabs.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_remoteTroubleshoot.js]
+https_first_disabled = true
+skip-if =
+ !updater
+ os == 'linux' && asan # Bug 1711507
+reason = depends on UpdateUtils .Locale
+support-files =
+ test_remoteTroubleshoot.html
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_remoteWebNavigation_postdata.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_restore_isAppTab.js]
+skip-if =
+ !crashreporter # test requires crashreporter due to 1536221
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_save_link-perwindowpb.js]
+skip-if =
+ debug && os == "win"
+ verify # Bug 1280505
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_save_link_when_window_navigates.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_save_private_link_perwindowpb.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_save_video.js]
+skip-if =
+ os == 'mac'
+ verify && os == 'mac'
+ os == 'win' && debug
+ os =='linux' #Bug 1212419
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_save_video_frame.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_selectTabAtIndex.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_star_hsts.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_storagePressure_notification.js]
+skip-if = verify
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tabDrop.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tab_close_dependent_window.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tab_detach_restore.js]
+https_first_disabled = true
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tab_drag_drop_perwindow.js]
+skip-if =
+ os == "win" && os_version == "6.1" && bits == 32 # bug 1717587
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tab_dragdrop.js]
+skip-if = true # Bug 1312436, Bug 1388973
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tab_dragdrop2.js]
+skip-if =
+ os == "win" && bits == 32 && !debug # high frequency win7 intermittent: crash
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tabfocus.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tabkeynavigation.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tabs_close_beforeunload.js]
+support-files =
+ close_beforeunload_opens_second_tab.html
+ close_beforeunload.html
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tabs_isActive.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tabs_owner.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_typeAheadFind.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_unknownContentType_title.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_unloaddialogs.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_viewSourceInTabOnViewSource.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_visibleFindSelection.js]
+skip-if = true # Bug 1409184 disabled because interactive find next is not automating properly
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_visibleTabs.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_visibleTabs_bookmarkAllPages.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_visibleTabs_tabPreview.js]
+skip-if =
+ os == "win" && !debug
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_windowactivation.js]
+skip-if =
+ verify
+ os == "linux" && debug # Bug 1678774
+support-files =
+ file_window_activation.html
+ file_window_activation2.html
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_zbug569342.js]
+skip-if = true # Bug 1094240 - has findbar-related failures
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
diff --git a/browser/base/content/test/general/browser_accesskeys.js b/browser/base/content/test/general/browser_accesskeys.js
new file mode 100644
index 0000000000..c8b27d6307
--- /dev/null
+++ b/browser/base/content/test/general/browser_accesskeys.js
@@ -0,0 +1,202 @@
+add_task(async function () {
+ await pushPrefs(["ui.key.contentAccess", 5], ["ui.key.chromeAccess", 5]);
+
+ const gPageURL1 =
+ "data:text/html,<body><p>" +
+ "<button id='button' accesskey='y'>Button</button>" +
+ "<input id='checkbox' type='checkbox' accesskey='z'>Checkbox" +
+ "</p></body>";
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL1);
+
+ Services.focus.clearFocus(window);
+
+ // Press an accesskey in the child document while the chrome is focused.
+ let focusedId = await performAccessKey(tab1.linkedBrowser, "y");
+ is(focusedId, "button", "button accesskey");
+
+ // Press an accesskey in the child document while the content document is focused.
+ focusedId = await performAccessKey(tab1.linkedBrowser, "z");
+ is(focusedId, "checkbox", "checkbox accesskey");
+
+ // Add an element with an accesskey to the chrome and press its accesskey while the chrome is focused.
+ let newButton = document.createXULElement("button");
+ newButton.id = "chromebutton";
+ newButton.setAttribute("accesskey", "z");
+ document.documentElement.appendChild(newButton);
+
+ Services.focus.clearFocus(window);
+
+ newButton.getBoundingClientRect(); // Accesskey registration happens during frame construction.
+
+ focusedId = await performAccessKeyForChrome("z");
+ is(focusedId, "chromebutton", "chromebutton accesskey");
+
+ // Add a second tab and ensure that accesskey from the first tab is not used.
+ const gPageURL2 =
+ "data:text/html,<body>" +
+ "<button id='tab2button' accesskey='y'>Button in Tab 2</button>" +
+ "</body>";
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL2);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = await performAccessKey(tab2.linkedBrowser, "y");
+ is(focusedId, "tab2button", "button accesskey in tab2");
+
+ // Press the accesskey for the chrome element while the content document is focused.
+ focusedId = await performAccessKeyForChrome("z");
+ is(focusedId, "chromebutton", "chromebutton accesskey");
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+
+ // Test whether access key for the newButton isn't available when content
+ // consumes the key event.
+
+ // When content in the tab3 consumes all keydown events.
+ const gPageURL3 =
+ "data:text/html,<body id='tab3body'>" +
+ "<button id='tab3button' accesskey='y'>Button in Tab 3</button>" +
+ "<script>" +
+ "document.body.addEventListener('keydown', (event)=>{ event.preventDefault(); });" +
+ "</script></body>";
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL3);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = await performAccessKey(tab3.linkedBrowser, "y");
+ is(focusedId, "tab3button", "button accesskey in tab3 should be focused");
+
+ newButton.onfocus = () => {
+ ok(false, "chromebutton shouldn't get focus during testing with tab3");
+ };
+
+ // Press the accesskey for the chrome element while the content document is focused.
+ focusedId = await performAccessKey(tab3.linkedBrowser, "z");
+ is(
+ focusedId,
+ "tab3body",
+ "button accesskey in tab3 should keep having focus"
+ );
+
+ newButton.onfocus = null;
+
+ gBrowser.removeTab(tab3);
+
+ // When content in the tab4 consumes all keypress events.
+ const gPageURL4 =
+ "data:text/html,<body id='tab4body'>" +
+ "<button id='tab4button' accesskey='y'>Button in Tab 4</button>" +
+ "<script>" +
+ "document.body.addEventListener('keypress', (event)=>{ event.preventDefault(); });" +
+ "</script></body>";
+ let tab4 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL4);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = await performAccessKey(tab4.linkedBrowser, "y");
+ is(focusedId, "tab4button", "button accesskey in tab4 should be focused");
+
+ newButton.onfocus = () => {
+ // EventStateManager handles accesskey before dispatching keypress event
+ // into the DOM tree, therefore, chrome accesskey always wins focus from
+ // content. However, this is different from shortcut keys.
+ todo(false, "chromebutton shouldn't get focus during testing with tab4");
+ };
+
+ // Press the accesskey for the chrome element while the content document is focused.
+ focusedId = await performAccessKey(tab4.linkedBrowser, "z");
+ is(
+ focusedId,
+ "tab4body",
+ "button accesskey in tab4 should keep having focus"
+ );
+
+ newButton.onfocus = null;
+
+ gBrowser.removeTab(tab4);
+
+ newButton.remove();
+});
+
+function performAccessKey(browser, key) {
+ return new Promise(resolve => {
+ let removeFocus, removeKeyDown, removeKeyUp;
+ function callback(eventName, result) {
+ removeFocus();
+ removeKeyUp();
+ removeKeyDown();
+
+ SpecialPowers.spawn(browser, [], () => {
+ let oldFocusedElement = content._oldFocusedElement;
+ delete content._oldFocusedElement;
+ return oldFocusedElement.id;
+ }).then(oldFocus => resolve(oldFocus));
+ }
+
+ removeFocus = BrowserTestUtils.addContentEventListener(
+ browser,
+ "focus",
+ callback,
+ { capture: true },
+ event => {
+ if (!HTMLElement.isInstance(event.target)) {
+ return false; // ignore window and document focus events
+ }
+
+ event.target.ownerGlobal._sent = true;
+ let focusedElement = event.target.ownerGlobal.document.activeElement;
+ event.target.ownerGlobal._oldFocusedElement = focusedElement;
+ focusedElement.blur();
+ return true;
+ }
+ );
+
+ removeKeyDown = BrowserTestUtils.addContentEventListener(
+ browser,
+ "keydown",
+ () => {},
+ { capture: true },
+ event => {
+ event.target.ownerGlobal._sent = false;
+ return true;
+ }
+ );
+
+ removeKeyUp = BrowserTestUtils.addContentEventListener(
+ browser,
+ "keyup",
+ callback,
+ {},
+ event => {
+ if (!event.target.ownerGlobal._sent) {
+ event.target.ownerGlobal._sent = true;
+ let focusedElement = event.target.ownerGlobal.document.activeElement;
+ event.target.ownerGlobal._oldFocusedElement = focusedElement;
+ focusedElement.blur();
+ return true;
+ }
+
+ return false;
+ }
+ );
+
+ // Spawn an no-op content task to better ensure that the messages
+ // for adding the event listeners above get handled.
+ SpecialPowers.spawn(browser, [], () => {}).then(() => {
+ EventUtils.synthesizeKey(key, { altKey: true, shiftKey: true });
+ });
+ });
+}
+
+// This version is used when a chrome element is expected to be found for an accesskey.
+async function performAccessKeyForChrome(key, inChild) {
+ let waitFocusChangePromise = BrowserTestUtils.waitForEvent(
+ document,
+ "focus",
+ true
+ );
+ EventUtils.synthesizeKey(key, { altKey: true, shiftKey: true });
+ await waitFocusChangePromise;
+ return document.activeElement.id;
+}
diff --git a/browser/base/content/test/general/browser_addCertException.js b/browser/base/content/test/general/browser_addCertException.js
new file mode 100644
index 0000000000..d3d1ac1ce4
--- /dev/null
+++ b/browser/base/content/test/general/browser_addCertException.js
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 adding a certificate exception by attempting to browse to a site with
+// a bad certificate, being redirected to the internal about:certerror page,
+// using the button contained therein to load the certificate exception
+// dialog, using that to add an exception, and finally successfully visiting
+// the site, including showing the right identity box and control center icons.
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await loadBadCertPage("https://expired.example.com");
+
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "ViewShown"
+ );
+ document.getElementById("identity-popup-security-button").click();
+ await promiseViewShown;
+
+ is_element_visible(
+ document.getElementById("identity-icon"),
+ "Should see identity icon"
+ );
+ let identityIconImage = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("identity-icon"))
+ .getPropertyValue("list-style-image");
+ let securityViewBG = gBrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-securityView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("list-style-image");
+ let securityContentBG = gBrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-mainView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("list-style-image");
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/security-warning.svg")',
+ "Using expected icon image in the identity block"
+ );
+ is(
+ securityViewBG,
+ 'url("chrome://global/skin/icons/security-warning.svg")',
+ "Using expected icon image in the Control Center main view"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/security-warning.svg")',
+ "Using expected icon image in the Control Center subview"
+ );
+
+ gIdentityHandler._identityPopup.hidePopup();
+
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("expired.example.com", -1, {});
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/general/browser_alltabslistener.js b/browser/base/content/test/general/browser_alltabslistener.js
new file mode 100644
index 0000000000..0c9677306d
--- /dev/null
+++ b/browser/base/content/test/general/browser_alltabslistener.js
@@ -0,0 +1,331 @@
+const gCompleteState =
+ Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+
+function getOriginalURL(request) {
+ return request && request.QueryInterface(Ci.nsIChannel).originalURI.spec;
+}
+
+var gFrontProgressListener = {
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {},
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ return;
+ }
+ var state = "onStateChange";
+ info(
+ "FrontProgress (" + url + "): " + state + " 0x" + aStateFlags.toString(16)
+ );
+ assertCorrectBrowserAndEventOrderForFront(state);
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ return;
+ }
+ var state = "onLocationChange";
+ info("FrontProgress: " + state + " " + aLocationURI.spec);
+ assertCorrectBrowserAndEventOrderForFront(state);
+ },
+
+ onSecurityChange(aWebProgress, aRequest, aState) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ return;
+ }
+ var state = "onSecurityChange";
+ info("FrontProgress (" + url + "): " + state + " 0x" + aState.toString(16));
+ assertCorrectBrowserAndEventOrderForFront(state);
+ },
+};
+
+function assertCorrectBrowserAndEventOrderForFront(aEventName) {
+ Assert.less(
+ gFrontNotificationsPos,
+ gFrontNotifications.length,
+ "Got an expected notification for the front notifications listener"
+ );
+ is(
+ aEventName,
+ gFrontNotifications[gFrontNotificationsPos],
+ "Got a notification for the front notifications listener"
+ );
+ gFrontNotificationsPos++;
+}
+
+var gAllProgressListener = {
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ // ignore initial about blank
+ return;
+ }
+ var state = "onStateChange";
+ info(
+ "AllProgress (" + url + "): " + state + " 0x" + aStateFlags.toString(16)
+ );
+ assertCorrectBrowserAndEventOrderForAll(state, aBrowser);
+ assertReceivedFlags(
+ state,
+ gAllNotifications[gAllNotificationsPos],
+ aStateFlags
+ );
+ gAllNotificationsPos++;
+
+ if ((aStateFlags & gCompleteState) == gCompleteState) {
+ is(
+ gAllNotificationsPos,
+ gAllNotifications.length,
+ "Saw the expected number of notifications"
+ );
+ is(
+ gFrontNotificationsPos,
+ gFrontNotifications.length,
+ "Saw the expected number of frontnotifications"
+ );
+ executeSoon(gNextTest);
+ }
+ },
+
+ onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ // ignore initial about blank
+ return;
+ }
+ var state = "onLocationChange";
+ info("AllProgress: " + state + " " + aLocationURI.spec);
+ assertCorrectBrowserAndEventOrderForAll(state, aBrowser);
+ assertReceivedFlags(
+ "onLocationChange",
+ gAllNotifications[gAllNotificationsPos],
+ aFlags
+ );
+ gAllNotificationsPos++;
+ },
+
+ onSecurityChange(aBrowser, aWebProgress, aRequest, aState) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ // ignore initial about blank
+ return;
+ }
+ var state = "onSecurityChange";
+ info("AllProgress (" + url + "): " + state + " 0x" + aState.toString(16));
+ assertCorrectBrowserAndEventOrderForAll(state, aBrowser);
+ is(
+ state,
+ gAllNotifications[gAllNotificationsPos],
+ "Got a notification for the all notifications listener"
+ );
+ gAllNotificationsPos++;
+ },
+};
+
+function assertCorrectBrowserAndEventOrderForAll(aState, aBrowser) {
+ ok(
+ aBrowser == gTestBrowser,
+ aState + " notification came from the correct browser"
+ );
+ Assert.less(
+ gAllNotificationsPos,
+ gAllNotifications.length,
+ "Got an expected notification for the all notifications listener"
+ );
+}
+
+function assertReceivedFlags(aState, aObjOrEvent, aFlags) {
+ if (aObjOrEvent !== null && typeof aObjOrEvent === "object") {
+ is(
+ aState,
+ aObjOrEvent.state,
+ "Got a notification for the all notifications listener"
+ );
+ is(aFlags, aFlags & aObjOrEvent.flags, `Got correct flags for ${aState}`);
+ } else {
+ is(
+ aState,
+ aObjOrEvent,
+ "Got a notification for the all notifications listener"
+ );
+ }
+}
+
+var gFrontNotifications,
+ gAllNotifications,
+ gFrontNotificationsPos,
+ gAllNotificationsPos;
+var gBackgroundTab,
+ gForegroundTab,
+ gBackgroundBrowser,
+ gForegroundBrowser,
+ gTestBrowser;
+var gTestPage =
+ "/browser/browser/base/content/test/general/alltabslistener.html";
+const kBasePage =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/dummy_page.html";
+var gNextTest;
+
+async function test() {
+ waitForExplicitFinish();
+
+ gBackgroundTab = BrowserTestUtils.addTab(gBrowser);
+ gForegroundTab = BrowserTestUtils.addTab(gBrowser);
+ gBackgroundBrowser = gBrowser.getBrowserForTab(gBackgroundTab);
+ gForegroundBrowser = gBrowser.getBrowserForTab(gForegroundTab);
+ gBrowser.selectedTab = gForegroundTab;
+
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange",
+ ];
+
+ // We must wait until a page has completed loading before
+ // starting tests or we get notifications from that
+ let promises = [
+ BrowserTestUtils.browserStopped(gBackgroundBrowser, kBasePage),
+ BrowserTestUtils.browserStopped(gForegroundBrowser, kBasePage),
+ ];
+ BrowserTestUtils.loadURIString(gBackgroundBrowser, kBasePage);
+ BrowserTestUtils.loadURIString(gForegroundBrowser, kBasePage);
+ await Promise.all(promises);
+ // If we process switched, the tabbrowser may still be processing the state_stop
+ // notification here because of how microtasks work. Ensure that that has
+ // happened before starting to test (which would add listeners to the tabbrowser
+ // which would get confused by being called about kBasePage loading).
+ await new Promise(executeSoon);
+ startTest1();
+}
+
+function runTest(browser, url, next) {
+ gFrontNotificationsPos = 0;
+ gAllNotificationsPos = 0;
+ gNextTest = next;
+ gTestBrowser = browser;
+ BrowserTestUtils.loadURIString(browser, url);
+}
+
+function startTest1() {
+ info("\nTest 1");
+ gBrowser.addProgressListener(gFrontProgressListener);
+ gBrowser.addTabsProgressListener(gAllProgressListener);
+
+ gFrontNotifications = gAllNotifications;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest2);
+}
+
+function startTest2() {
+ info("\nTest 2");
+ gFrontNotifications = gAllNotifications;
+ runTest(gForegroundBrowser, "https://example.com" + gTestPage, startTest3);
+}
+
+function startTest3() {
+ info("\nTest 3");
+ gFrontNotifications = [];
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ runTest(gBackgroundBrowser, "http://example.org" + gTestPage, startTest4);
+}
+
+function startTest4() {
+ info("\nTest 4");
+ gFrontNotifications = [];
+ runTest(gBackgroundBrowser, "https://example.com" + gTestPage, startTest5);
+}
+
+function startTest5() {
+ info("\nTest 5");
+ // Switch the foreground browser
+ [gForegroundBrowser, gBackgroundBrowser] = [
+ gBackgroundBrowser,
+ gForegroundBrowser,
+ ];
+ [gForegroundTab, gBackgroundTab] = [gBackgroundTab, gForegroundTab];
+ // Avoid the onLocationChange this will fire
+ gBrowser.removeProgressListener(gFrontProgressListener);
+ gBrowser.selectedTab = gForegroundTab;
+ gBrowser.addProgressListener(gFrontProgressListener);
+
+ gFrontNotifications = gAllNotifications;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest6);
+}
+
+function startTest6() {
+ info("\nTest 6");
+ gFrontNotifications = [];
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ runTest(gBackgroundBrowser, "http://example.org" + gTestPage, startTest7);
+}
+
+// Navigate from remote to non-remote
+function startTest7() {
+ info("\nTest 7");
+ gFrontNotifications = [];
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ {
+ state: "onLocationChange",
+ flags: Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT,
+ }, // dummy onLocationChange event
+ "onStateChange",
+ ];
+ runTest(gBackgroundBrowser, "about:preferences", startTest8);
+}
+
+// Navigate from non-remote to non-remote
+function startTest8() {
+ info("\nTest 8");
+ gFrontNotifications = [];
+ gAllNotifications = [
+ "onStateChange",
+ {
+ state: "onStateChange",
+ flags:
+ Ci.nsIWebProgressListener.STATE_IS_REDIRECTED_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_IS_REQUEST |
+ Ci.nsIWebProgressListener.STATE_START,
+ },
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange",
+ ];
+ runTest(gBackgroundBrowser, "about:config", startTest9);
+}
+
+// Navigate from non-remote to remote
+function startTest9() {
+ info("\nTest 9");
+ gFrontNotifications = [];
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange",
+ ];
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ runTest(gBackgroundBrowser, "http://example.org" + gTestPage, finishTest);
+}
+
+function finishTest() {
+ gBrowser.removeProgressListener(gFrontProgressListener);
+ gBrowser.removeTabsProgressListener(gAllProgressListener);
+ gBrowser.removeTab(gBackgroundTab);
+ gBrowser.removeTab(gForegroundTab);
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_backButtonFitts.js b/browser/base/content/test/general/browser_backButtonFitts.js
new file mode 100644
index 0000000000..8ef3006a2c
--- /dev/null
+++ b/browser/base/content/test/general/browser_backButtonFitts.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function () {
+ let firstLocation =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, firstLocation);
+
+ await ContentTask.spawn(gBrowser.selectedBrowser, {}, async function () {
+ // Push the state before maximizing the window and clicking below.
+ content.history.pushState("page2", "page2", "page2");
+ });
+
+ window.maximize();
+
+ // Find where the nav-bar is vertically.
+ var navBar = document.getElementById("nav-bar");
+ var boundingRect = navBar.getBoundingClientRect();
+ var yPixel = boundingRect.top + Math.floor(boundingRect.height / 2);
+ var xPixel = 0; // Use the first pixel of the screen since it is maximized.
+
+ let popStatePromise = BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "popstate",
+ true
+ );
+ EventUtils.synthesizeMouseAtPoint(xPixel, yPixel, {}, window);
+ await popStatePromise;
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ firstLocation,
+ "Clicking the first pixel should have navigated back."
+ );
+ window.restore();
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
new file mode 100644
index 0000000000..8a77f01ce4
--- /dev/null
+++ b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
@@ -0,0 +1,114 @@
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
+
+const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+var expectingDialog = false;
+var wantToClose = true;
+var resolveDialogPromise;
+
+function onTabModalDialogLoaded(node) {
+ ok(
+ !CONTENT_PROMPT_SUBDIALOG,
+ "Should not be using content prompt subdialogs."
+ );
+ ok(expectingDialog, "Should be expecting this dialog.");
+ expectingDialog = false;
+ if (wantToClose) {
+ // This accepts the dialog, closing it
+ node.querySelector(".tabmodalprompt-button0").click();
+ } else {
+ // This keeps the page open
+ node.querySelector(".tabmodalprompt-button1").click();
+ }
+ if (resolveDialogPromise) {
+ resolveDialogPromise();
+ }
+}
+
+function onCommonDialogLoaded(promptWindow) {
+ ok(CONTENT_PROMPT_SUBDIALOG, "Should be using content prompt subdialogs.");
+ ok(expectingDialog, "Should be expecting this dialog.");
+ expectingDialog = false;
+ let dialog = promptWindow.Dialog;
+ if (wantToClose) {
+ // This accepts the dialog, closing it.
+ dialog.ui.button0.click();
+ } else {
+ // This keeps the page open
+ dialog.ui.button1.click();
+ }
+ if (resolveDialogPromise) {
+ resolveDialogPromise();
+ }
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+});
+
+// Listen for the dialog being created
+Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
+Services.obs.addObserver(onCommonDialogLoaded, "common-dialog-loaded");
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.tabs.warnOnClose");
+ Services.obs.removeObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
+ Services.obs.removeObserver(onCommonDialogLoaded, "common-dialog-loaded");
+});
+
+add_task(async function closeLastTabInWindow() {
+ let newWin = await promiseOpenAndLoadWindow({}, true);
+ let firstTab = newWin.gBrowser.selectedTab;
+ await promiseTabLoadEvent(firstTab, TEST_PAGE);
+ let windowClosedPromise = BrowserTestUtils.domWindowClosed(newWin);
+ expectingDialog = true;
+ // close tab:
+ firstTab.closeButton.click();
+ await windowClosedPromise;
+ ok(!expectingDialog, "There should have been a dialog.");
+ ok(newWin.closed, "Window should be closed.");
+});
+
+add_task(async function closeWindowWithMultipleTabsIncludingOneBeforeUnload() {
+ Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+ let newWin = await promiseOpenAndLoadWindow({}, true);
+ let firstTab = newWin.gBrowser.selectedTab;
+ await promiseTabLoadEvent(firstTab, TEST_PAGE);
+ await promiseTabLoadEvent(
+ BrowserTestUtils.addTab(newWin.gBrowser),
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ let windowClosedPromise = BrowserTestUtils.domWindowClosed(newWin);
+ expectingDialog = true;
+ newWin.BrowserTryToCloseWindow();
+ await windowClosedPromise;
+ ok(!expectingDialog, "There should have been a dialog.");
+ ok(newWin.closed, "Window should be closed.");
+ Services.prefs.clearUserPref("browser.tabs.warnOnClose");
+});
+
+add_task(async function closeWindoWithSingleTabTwice() {
+ let newWin = await promiseOpenAndLoadWindow({}, true);
+ let firstTab = newWin.gBrowser.selectedTab;
+ await promiseTabLoadEvent(firstTab, TEST_PAGE);
+ let windowClosedPromise = BrowserTestUtils.domWindowClosed(newWin);
+ expectingDialog = true;
+ wantToClose = false;
+ let firstDialogShownPromise = new Promise((resolve, reject) => {
+ resolveDialogPromise = resolve;
+ });
+ firstTab.closeButton.click();
+ await firstDialogShownPromise;
+ info("Got initial dialog, now trying again");
+ expectingDialog = true;
+ wantToClose = true;
+ resolveDialogPromise = null;
+ firstTab.closeButton.click();
+ await windowClosedPromise;
+ ok(!expectingDialog, "There should have been a dialog.");
+ ok(newWin.closed, "Window should be closed.");
+});
diff --git a/browser/base/content/test/general/browser_bug1261299.js b/browser/base/content/test/general/browser_bug1261299.js
new file mode 100644
index 0000000000..47b82a5da0
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1261299.js
@@ -0,0 +1,112 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 for Bug 1261299
+ * Test that the service menu code path is called properly and the
+ * current selection (transferable) is cached properly on the parent process.
+ */
+
+add_task(async function test_content_and_chrome_selection() {
+ let testPage =
+ "data:text/html," +
+ '<textarea id="textarea">Write something here</textarea>';
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ let selectedText;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage);
+ await BrowserTestUtils.synthesizeMouse(
+ "#textarea",
+ 0,
+ 0,
+ {},
+ gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeKey(
+ "KEY_ArrowRight",
+ { shiftKey: true, ctrlKey: true },
+ gBrowser.selectedBrowser
+ );
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(
+ selectedText,
+ "Write something here",
+ "The macOS services got the selected content text"
+ );
+ gURLBar.value = "test.mozilla.org";
+ await gURLBar.editor.selectAll();
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(
+ selectedText,
+ "test.mozilla.org",
+ "The macOS services got the selected chrome text"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test switching active selection.
+// Each tab has a content selection and when you switch to that tab, its selection becomes
+// active aka the current selection.
+// Expect: The active selection is what is being sent to OSX service menu.
+
+add_task(async function test_active_selection_switches_properly() {
+ let testPage1 =
+ // eslint-disable-next-line no-useless-concat
+ "data:text/html," +
+ '<textarea id="textarea">Write something here</textarea>';
+ let testPage2 =
+ // eslint-disable-next-line no-useless-concat
+ "data:text/html," + '<textarea id="textarea">Nothing available</textarea>';
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ let selectedText;
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage1);
+ await BrowserTestUtils.synthesizeMouse(
+ "#textarea",
+ 0,
+ 0,
+ {},
+ gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeKey(
+ "KEY_ArrowRight",
+ { shiftKey: true, ctrlKey: true },
+ gBrowser.selectedBrowser
+ );
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage2);
+ await BrowserTestUtils.synthesizeMouse(
+ "#textarea",
+ 0,
+ 0,
+ {},
+ gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeKey(
+ "KEY_ArrowRight",
+ { shiftKey: true, ctrlKey: true },
+ gBrowser.selectedBrowser
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(
+ selectedText,
+ "Write something here",
+ "The macOS services got the selected content text"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(
+ selectedText,
+ "Nothing available",
+ "The macOS services got the selected content text"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/base/content/test/general/browser_bug1297539.js b/browser/base/content/test/general/browser_bug1297539.js
new file mode 100644
index 0000000000..7572d85eaf
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1297539.js
@@ -0,0 +1,122 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 1297539
+ * Test that the content event "pasteTransferable"
+ * (mozilla::EventMessage::eContentCommandPasteTransferable)
+ * is handled correctly for plain text and html in the remote case.
+ *
+ * Original test test_bug525389.html for command content event
+ * "pasteTransferable" runs only in the content process.
+ * This doesn't test the remote case.
+ *
+ */
+
+"use strict";
+
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function getTransferableFromClipboard(asHTML) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ if (asHTML) {
+ trans.addDataFlavor("text/html");
+ } else {
+ trans.addDataFlavor("text/plain");
+ }
+ Services.clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
+ return trans;
+}
+
+async function cutCurrentSelection(elementQueryString, property, browser) {
+ // Cut the current selection.
+ await BrowserTestUtils.synthesizeKey("x", { accelKey: true }, browser);
+
+ // The editor should be empty after cut.
+ await SpecialPowers.spawn(
+ browser,
+ [[elementQueryString, property]],
+ async function ([contentElementQueryString, contentProperty]) {
+ let element = content.document.querySelector(contentElementQueryString);
+ is(
+ element[contentProperty],
+ "",
+ `${contentElementQueryString} should be empty after cut (superkey + x)`
+ );
+ }
+ );
+}
+
+// Test that you are able to pasteTransferable for plain text
+// which is handled by TextEditor::PasteTransferable to paste into the editor.
+add_task(async function test_paste_transferable_plain_text() {
+ let testPage =
+ "data:text/html," +
+ '<textarea id="textarea">Write something here</textarea>';
+
+ await BrowserTestUtils.withNewTab(testPage, async function (browser) {
+ // Select all the content in your editor element.
+ await BrowserTestUtils.synthesizeMouse("#textarea", 0, 0, {}, browser);
+ await BrowserTestUtils.synthesizeKey("a", { accelKey: true }, browser);
+
+ await cutCurrentSelection("#textarea", "value", browser);
+
+ let trans = getTransferableFromClipboard(false);
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ DOMWindowUtils.sendContentCommandEvent("pasteTransferable", trans);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let textArea = content.document.querySelector("#textarea");
+ is(
+ textArea.value,
+ "Write something here",
+ "Send content command pasteTransferable successful"
+ );
+ });
+ });
+});
+
+// Test that you are able to pasteTransferable for html
+// which is handled by HTMLEditor::PasteTransferable to paste into the editor.
+//
+// On Linux,
+// BrowserTestUtils.synthesizeKey("a", {accelKey: true}, browser);
+// doesn't seem to trigger for contenteditable which is why we use
+// Selection to select the contenteditable contents.
+add_task(async function test_paste_transferable_html() {
+ let testPage =
+ "data:text/html," +
+ '<div contenteditable="true"><b>Bold Text</b><i>italics</i></div>';
+
+ await BrowserTestUtils.withNewTab(testPage, async function (browser) {
+ // Select all the content in your editor element.
+ await BrowserTestUtils.synthesizeMouse("div", 0, 0, {}, browser);
+ await SpecialPowers.spawn(browser, [], async function () {
+ let element = content.document.querySelector("div");
+ let selection = content.window.getSelection();
+ selection.selectAllChildren(element);
+ });
+
+ await cutCurrentSelection("div", "textContent", browser);
+
+ let trans = getTransferableFromClipboard(true);
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ DOMWindowUtils.sendContentCommandEvent("pasteTransferable", trans);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let textArea = content.document.querySelector("div");
+ is(
+ textArea.innerHTML,
+ "<b>Bold Text</b><i>italics</i>",
+ "Send content command pasteTransferable successful"
+ );
+ });
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug1299667.js b/browser/base/content/test/general/browser_bug1299667.js
new file mode 100644
index 0000000000..d281652c44
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1299667.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.history.pushState({}, "2", "2.html");
+ });
+
+ await TestUtils.topicObserved("sessionstore-state-write-complete");
+
+ // Wait for the session data to be flushed before continuing the test
+ await new Promise(resolve =>
+ SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
+ );
+
+ let backButton = document.getElementById("back-button");
+ let contextMenu = document.getElementById("backForwardMenu");
+
+ info("waiting for the history menu to open");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(backButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ let event = await popupShownPromise;
+
+ ok(true, "history menu opened");
+
+ // Wait for the session data to be flushed before continuing the test
+ await new Promise(resolve =>
+ SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
+ );
+
+ is(event.target.children.length, 2, "Two history items");
+
+ let node = event.target.firstElementChild;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(node.getAttribute("uri"), "http://example.com/2.html", "first item uri");
+ is(node.getAttribute("index"), "1", "first item index");
+ is(node.getAttribute("historyindex"), "0", "first item historyindex");
+
+ node = event.target.lastElementChild;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(node.getAttribute("uri"), "http://example.com/", "second item uri");
+ is(node.getAttribute("index"), "0", "second item index");
+ is(node.getAttribute("historyindex"), "-1", "second item historyindex");
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ event.target.hidePopup();
+ await popupHiddenPromise;
+ info("Hidden popup");
+
+ let onClose = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await onClose;
+ info("Tab closed");
+});
diff --git a/browser/base/content/test/general/browser_bug321000.js b/browser/base/content/test/general/browser_bug321000.js
new file mode 100644
index 0000000000..78ab74e543
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug321000.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 kTestString = " hello hello \n world\nworld ";
+
+var gTests = [
+ {
+ desc: "Urlbar strips newlines and surrounding whitespace",
+ element: gURLBar,
+ expected: kTestString.replace(/\s*\n\s*/g, ""),
+ },
+
+ {
+ desc: "Searchbar replaces newlines with spaces",
+ element: document.getElementById("searchbar"),
+ expected: kTestString.replace(/\n/g, " "),
+ },
+];
+
+// Test for bug 23485 and bug 321000.
+// Urlbar should strip newlines,
+// search bar should replace newlines with spaces.
+function test() {
+ waitForExplicitFinish();
+
+ let cbHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+
+ // Put a multi-line string in the clipboard.
+ // Setting the clipboard value is an async OS operation, so we need to poll
+ // the clipboard for valid data before going on.
+ waitForClipboard(
+ kTestString,
+ function () {
+ cbHelper.copyString(kTestString);
+ },
+ next_test,
+ finish
+ );
+}
+
+function next_test() {
+ if (gTests.length) {
+ test_paste(gTests.shift());
+ } else {
+ finish();
+ }
+}
+
+function test_paste(aCurrentTest) {
+ var element = aCurrentTest.element;
+
+ // Register input listener.
+ var inputListener = {
+ test: aCurrentTest,
+ handleEvent(event) {
+ element.removeEventListener(event.type, this);
+
+ is(element.value, this.test.expected, this.test.desc);
+
+ // Clear the field and go to next test.
+ element.value = "";
+ setTimeout(next_test, 0);
+ },
+ };
+ element.addEventListener("input", inputListener);
+
+ // Focus the window.
+ window.focus();
+ gBrowser.selectedBrowser.focus();
+
+ // Focus the element and wait for focus event.
+ info("About to focus " + element.id);
+ element.addEventListener(
+ "focus",
+ function () {
+ executeSoon(function () {
+ // Pasting is async because the Accel+V codepath ends up going through
+ // nsDocumentViewer::FireClipboardEvent.
+ info("Pasting into " + element.id);
+ EventUtils.synthesizeKey("v", { accelKey: true });
+ });
+ },
+ { once: true }
+ );
+ element.focus();
+}
diff --git a/browser/base/content/test/general/browser_bug356571.js b/browser/base/content/test/general/browser_bug356571.js
new file mode 100644
index 0000000000..185d59d8fd
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug356571.js
@@ -0,0 +1,100 @@
+// Bug 356571 - loadOneOrMoreURIs gives up if one of the URLs has an unknown protocol
+
+var Cm = Components.manager;
+
+// Set to true when docShell alerts for unknown protocol error
+var didFail = false;
+
+// Override Alert to avoid blocking the test due to unknown protocol error
+const kPromptServiceUUID = "{6cc9c9fe-bc0b-432b-a410-253ef8bcc699}";
+const kPromptServiceContractID = "@mozilla.org/prompter;1";
+
+// Save original prompt service factory
+const kPromptServiceFactory = Cm.getClassObject(
+ Cc[kPromptServiceContractID],
+ Ci.nsIFactory
+);
+
+var fakePromptServiceFactory = {
+ createInstance(aIid) {
+ return promptService.QueryInterface(aIid);
+ },
+};
+
+var promptService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ alert() {
+ didFail = true;
+ },
+};
+
+/* FIXME
+Cm.QueryInterface(Ci.nsIComponentRegistrar)
+ .registerFactory(Components.ID(kPromptServiceUUID), "Prompt Service",
+ kPromptServiceContractID, fakePromptServiceFactory);
+*/
+
+const kCompleteState =
+ Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+
+const kDummyPage =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+const kURIs = ["bad://www.mozilla.org/", kDummyPage, kDummyPage];
+
+var gProgressListener = {
+ _runCount: 0,
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if ((aStateFlags & kCompleteState) == kCompleteState) {
+ if (++this._runCount != kURIs.length) {
+ return;
+ }
+ // Check we failed on unknown protocol (received an alert from docShell)
+ ok(didFail, "Correctly failed on unknown protocol");
+ // Check we opened all tabs
+ ok(
+ gBrowser.tabs.length == kURIs.length,
+ "Correctly opened all expected tabs"
+ );
+ finishTest();
+ }
+ },
+};
+
+function test() {
+ todo(false, "temp. disabled");
+ /* FIXME */
+ /*
+ waitForExplicitFinish();
+ // Wait for all tabs to finish loading
+ gBrowser.addTabsProgressListener(gProgressListener);
+ loadOneOrMoreURIs(kURIs.join("|"));
+ */
+}
+
+function finishTest() {
+ // Unregister the factory so we do not leak
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory(
+ Components.ID(kPromptServiceUUID),
+ fakePromptServiceFactory
+ );
+
+ // Restore the original factory
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory(
+ Components.ID(kPromptServiceUUID),
+ "Prompt Service",
+ kPromptServiceContractID,
+ kPromptServiceFactory
+ );
+
+ // Remove the listener
+ gBrowser.removeTabsProgressListener(gProgressListener);
+
+ // Close opened tabs
+ for (var i = gBrowser.tabs.length - 1; i > 0; i--) {
+ gBrowser.removeTab(gBrowser.tabs[i]);
+ }
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug380960.js b/browser/base/content/test/general/browser_bug380960.js
new file mode 100644
index 0000000000..5571d8f08e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug380960.js
@@ -0,0 +1,18 @@
+function test() {
+ var tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ gBrowser.removeTab(tab);
+ is(tab.parentNode, null, "tab removed immediately");
+
+ tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ gBrowser.removeTab(tab, { animate: true });
+ gBrowser.removeTab(tab);
+ is(
+ tab.parentNode,
+ null,
+ "tab removed immediately when calling removeTab again after the animation was kicked off"
+ );
+}
diff --git a/browser/base/content/test/general/browser_bug406216.js b/browser/base/content/test/general/browser_bug406216.js
new file mode 100644
index 0000000000..bee262e4f8
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug406216.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * "TabClose" event is possibly used for closing related tabs of the current.
+ * "removeTab" method should work correctly even if the number of tabs are
+ * changed while "TabClose" event.
+ */
+
+var count = 0;
+const URIS = [
+ "about:config",
+ "about:plugins",
+ "about:buildconfig",
+ "data:text/html,<title>OK</title>",
+];
+
+function test() {
+ waitForExplicitFinish();
+ URIS.forEach(addTab);
+}
+
+function addTab(aURI, aIndex) {
+ var tab = BrowserTestUtils.addTab(gBrowser, aURI);
+ if (aIndex == 0) {
+ gBrowser.removeTab(gBrowser.tabs[0], { skipPermitUnload: true });
+ }
+
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ if (++count == URIS.length) {
+ executeSoon(doTabsTest);
+ }
+ });
+}
+
+function doTabsTest() {
+ is(gBrowser.tabs.length, URIS.length, "Correctly opened all expected tabs");
+
+ // sample of "close related tabs" feature
+ gBrowser.tabContainer.addEventListener(
+ "TabClose",
+ function (event) {
+ var closedTab = event.originalTarget;
+ var scheme = closedTab.linkedBrowser.currentURI.scheme;
+ Array.from(gBrowser.tabs).forEach(function (aTab) {
+ if (
+ aTab != closedTab &&
+ aTab.linkedBrowser.currentURI.scheme == scheme
+ ) {
+ gBrowser.removeTab(aTab, { skipPermitUnload: true });
+ }
+ });
+ },
+ { capture: true, once: true }
+ );
+
+ gBrowser.removeTab(gBrowser.tabs[0], { skipPermitUnload: true });
+ is(gBrowser.tabs.length, 1, "Related tabs are not closed unexpectedly");
+
+ BrowserTestUtils.addTab(gBrowser, "about:blank");
+ gBrowser.removeTab(gBrowser.tabs[0], { skipPermitUnload: true });
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug417483.js b/browser/base/content/test/general/browser_bug417483.js
new file mode 100644
index 0000000000..6c8619b532
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug417483.js
@@ -0,0 +1,50 @@
+add_task(async function () {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true
+ );
+ const htmlContent =
+ "data:text/html, <iframe src='data:text/html,text text'></iframe>";
+ BrowserTestUtils.loadURIString(gBrowser, htmlContent);
+ await loadedPromise;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function (arg) {
+ let frame = content.frames[0];
+ let sel = frame.getSelection();
+ let range = frame.document.createRange();
+ let tn = frame.document.body.childNodes[0];
+ range.setStart(tn, 4);
+ range.setEnd(tn, 5);
+ sel.addRange(range);
+ frame.focus();
+ });
+
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "frame",
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ ok(
+ document.getElementById("frame-sep").hidden,
+ "'frame-sep' should be hidden if the selection contains only spaces"
+ );
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ contentAreaContextMenu.hidePopup();
+ await popupHiddenPromise;
+});
diff --git a/browser/base/content/test/general/browser_bug424101.js b/browser/base/content/test/general/browser_bug424101.js
new file mode 100644
index 0000000000..ecaf7064ab
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug424101.js
@@ -0,0 +1,72 @@
+/* Make sure that the context menu appears on form elements */
+
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/html,test");
+
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+
+ let tests = [
+ { element: "input", type: "text" },
+ { element: "input", type: "password" },
+ { element: "input", type: "image" },
+ { element: "input", type: "button" },
+ { element: "input", type: "submit" },
+ { element: "input", type: "reset" },
+ { element: "input", type: "checkbox" },
+ { element: "input", type: "radio" },
+ { element: "button" },
+ { element: "select" },
+ { element: "option" },
+ { element: "optgroup" },
+ ];
+
+ for (let index = 0; index < tests.length; index++) {
+ let test = tests[index];
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ element: test.element, type: test.type, index }],
+ async function (arg) {
+ let element = content.document.createElement(arg.element);
+ element.id = "element" + arg.index;
+ if (arg.type) {
+ element.setAttribute("type", arg.type);
+ }
+ content.document.body.appendChild(element);
+ }
+ );
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#element" + index,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ let typeAttr = test.type ? "type=" + test.type + " " : "";
+ is(
+ gContextMenu.shouldDisplay,
+ true,
+ "context menu behavior for <" +
+ test.element +
+ " " +
+ typeAttr +
+ "> is wrong"
+ );
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ contentAreaContextMenu.hidePopup();
+ await popupHiddenPromise;
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug427559.js b/browser/base/content/test/general/browser_bug427559.js
new file mode 100644
index 0000000000..29acf0862b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug427559.js
@@ -0,0 +1,41 @@
+"use strict";
+
+/*
+ * Test bug 427559 to make sure focused elements that are no longer on the page
+ * will have focus transferred to the window when changing tabs back to that
+ * tab with the now-gone element.
+ */
+
+// Default focus on a button and have it kill itself on blur.
+const URL =
+ "data:text/html;charset=utf-8," +
+ '<body><button onblur="this.remove()">' +
+ "<script>document.body.firstElementChild.focus()</script></body>";
+
+function getFocusedLocalName(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ return content.document.activeElement.localName;
+ });
+}
+
+add_task(async function () {
+ let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+
+ let browser = testTab.linkedBrowser;
+
+ is(await getFocusedLocalName(browser), "button", "button is focused");
+
+ let blankTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, testTab);
+
+ // Make sure focus is given to the window because the element is now gone.
+ is(await getFocusedLocalName(browser), "body", "body is focused");
+
+ // Cleanup.
+ gBrowser.removeTab(blankTab);
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug431826.js b/browser/base/content/test/general/browser_bug431826.js
new file mode 100644
index 0000000000..704cd4a675
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug431826.js
@@ -0,0 +1,56 @@
+function remote(task) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], task);
+}
+
+add_task(async function () {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser, "https://nocert.example.com/");
+ await promise;
+
+ await remote(() => {
+ // Confirm that we are displaying the contributed error page, not the default
+ let uri = content.document.documentURI;
+ Assert.ok(
+ uri.startsWith("about:certerror"),
+ "Broken page should go to about:certerror, not about:neterror"
+ );
+ });
+
+ await remote(() => {
+ let div = content.document.getElementById("badCertAdvancedPanel");
+ // Confirm that the expert section is collapsed
+ Assert.ok(div, "Advanced content div should exist");
+ Assert.equal(
+ div.ownerGlobal.getComputedStyle(div).display,
+ "none",
+ "Advanced content should not be visible by default"
+ );
+ });
+
+ // Tweak the expert mode pref
+ Services.prefs.setBoolPref("browser.xul.error_pages.expert_bad_cert", true);
+
+ promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ gBrowser.reload();
+ await promise;
+
+ await remote(() => {
+ let div = content.document.getElementById("badCertAdvancedPanel");
+ Assert.ok(div, "Advanced content div should exist");
+ Assert.equal(
+ div.ownerGlobal.getComputedStyle(div).display,
+ "block",
+ "Advanced content should be visible by default"
+ );
+ });
+
+ // Clean up
+ gBrowser.removeCurrentTab();
+ if (
+ Services.prefs.prefHasUserValue("browser.xul.error_pages.expert_bad_cert")
+ ) {
+ Services.prefs.clearUserPref("browser.xul.error_pages.expert_bad_cert");
+ }
+});
diff --git a/browser/base/content/test/general/browser_bug432599.js b/browser/base/content/test/general/browser_bug432599.js
new file mode 100644
index 0000000000..be4a4b8b5c
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug432599.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function invokeUsingCtrlD(phase) {
+ switch (phase) {
+ case 1:
+ EventUtils.synthesizeKey("d", { accelKey: true });
+ break;
+ case 2:
+ case 4:
+ EventUtils.synthesizeKey("KEY_Escape");
+ break;
+ case 3:
+ EventUtils.synthesizeKey("d", { accelKey: true });
+ EventUtils.synthesizeKey("d", { accelKey: true });
+ break;
+ }
+}
+
+function invokeUsingStarButton(phase) {
+ switch (phase) {
+ case 1:
+ EventUtils.synthesizeMouseAtCenter(BookmarkingUI.star, {});
+ break;
+ case 2:
+ case 4:
+ EventUtils.synthesizeKey("KEY_Escape");
+ break;
+ case 3:
+ EventUtils.synthesizeMouseAtCenter(BookmarkingUI.star, { clickCount: 2 });
+ break;
+ }
+}
+
+add_task(async function () {
+ const TEST_URL = "data:text/plain,Content";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ // Changing the location causes the star to asynchronously update, thus wait
+ // for it to be in a stable state before proceeding.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status == BookmarkingUI.STATUS_UNSTARRED
+ );
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "Bug 432599 Test",
+ });
+ Assert.equal(
+ BookmarkingUI.status,
+ BookmarkingUI.STATUS_STARRED,
+ "The star state should be starred"
+ );
+
+ for (let invoker of [invokeUsingStarButton, invokeUsingCtrlD]) {
+ for (let phase = 1; phase < 5; ++phase) {
+ let promise = checkBookmarksPanel(phase);
+ invoker(phase);
+ await promise;
+ Assert.equal(
+ BookmarkingUI.status,
+ BookmarkingUI.STATUS_STARRED,
+ "The star state shouldn't change"
+ );
+ }
+ }
+});
+
+var initialValue;
+var initialRemoveHidden;
+async function checkBookmarksPanel(phase) {
+ StarUI._createPanelIfNeeded();
+ let popupElement = document.getElementById("editBookmarkPanel");
+ let titleElement = document.getElementById("editBookmarkPanelTitle");
+ let removeElement = document.getElementById("editBookmarkPanelRemoveButton");
+ await document.l10n.translateElements([titleElement]);
+ switch (phase) {
+ case 1:
+ case 3:
+ await promisePopupShown(popupElement);
+ break;
+ case 2:
+ initialValue = titleElement.textContent;
+ initialRemoveHidden = removeElement.hidden;
+ await promisePopupHidden(popupElement);
+ break;
+ case 4:
+ Assert.equal(
+ titleElement.textContent,
+ initialValue,
+ "The bookmark panel's title should be the same"
+ );
+ Assert.equal(
+ removeElement.hidden,
+ initialRemoveHidden,
+ "The bookmark panel's visibility should not change"
+ );
+ await promisePopupHidden(popupElement);
+ break;
+ default:
+ throw new Error("Unknown phase");
+ }
+}
diff --git a/browser/base/content/test/general/browser_bug455852.js b/browser/base/content/test/general/browser_bug455852.js
new file mode 100644
index 0000000000..567f655e99
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug455852.js
@@ -0,0 +1,27 @@
+add_task(async function () {
+ is(gBrowser.tabs.length, 1, "one tab is open");
+
+ gBrowser.selectedBrowser.focus();
+ isnot(
+ document.activeElement,
+ gURLBar.inputField,
+ "location bar is not focused"
+ );
+
+ var tab = gBrowser.selectedTab;
+ Services.prefs.setBoolPref("browser.tabs.closeWindowWithLastTab", false);
+
+ EventUtils.synthesizeKey("w", { accelKey: true });
+
+ is(tab.parentNode, null, "ctrl+w removes the tab");
+ is(gBrowser.tabs.length, 1, "a new tab has been opened");
+ is(
+ document.activeElement,
+ gURLBar.inputField,
+ "location bar is focused for the new tab"
+ );
+
+ if (Services.prefs.prefHasUserValue("browser.tabs.closeWindowWithLastTab")) {
+ Services.prefs.clearUserPref("browser.tabs.closeWindowWithLastTab");
+ }
+});
diff --git a/browser/base/content/test/general/browser_bug462289.js b/browser/base/content/test/general/browser_bug462289.js
new file mode 100644
index 0000000000..c8be399639
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug462289.js
@@ -0,0 +1,144 @@
+var tab1, tab2;
+
+function focus_in_navbar() {
+ var parent = document.activeElement.parentNode;
+ while (parent && parent.id != "nav-bar") {
+ parent = parent.parentNode;
+ }
+
+ return parent != null;
+}
+
+function test() {
+ // Put the home button in the pre-proton placement to test focus states.
+ CustomizableUI.addWidgetToArea(
+ "home-button",
+ "nav-bar",
+ CustomizableUI.getPlacementOfWidget("stop-reload-button").position + 1
+ );
+ registerCleanupFunction(async function resetToolbar() {
+ await CustomizableUI.reset();
+ });
+
+ waitForExplicitFinish();
+
+ tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+
+ EventUtils.synthesizeMouseAtCenter(tab1, {});
+ setTimeout(step2, 0);
+}
+
+function step2() {
+ is(gBrowser.selectedTab, tab1, "1st click on tab1 selects tab");
+ isnot(
+ document.activeElement,
+ tab1,
+ "1st click on tab1 does not activate tab"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(tab1, {});
+ setTimeout(step3, 0);
+}
+
+async function step3() {
+ is(
+ gBrowser.selectedTab,
+ tab1,
+ "2nd click on selected tab1 keeps tab selected"
+ );
+ isnot(
+ document.activeElement,
+ tab1,
+ "2nd click on selected tab1 does not activate tab"
+ );
+
+ info("focusing URLBar then sending 3 Shift+Tab.");
+ gURLBar.focus();
+
+ let focused = BrowserTestUtils.waitForEvent(
+ document.getElementById("home-button"),
+ "focus"
+ );
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ await focused;
+ info("Focus is now on Home button");
+
+ focused = BrowserTestUtils.waitForEvent(
+ document.getElementById("tabs-newtab-button"),
+ "focus"
+ );
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ await focused;
+ info("Focus is now on the new tab button");
+
+ focused = BrowserTestUtils.waitForEvent(tab1, "focus");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ await focused;
+ is(gBrowser.selectedTab, tab1, "tab key to selected tab1 keeps tab selected");
+ is(document.activeElement, tab1, "tab key to selected tab1 activates tab");
+
+ EventUtils.synthesizeMouseAtCenter(tab1, {});
+ setTimeout(step4, 0);
+}
+
+function step4() {
+ is(
+ gBrowser.selectedTab,
+ tab1,
+ "3rd click on activated tab1 keeps tab selected"
+ );
+ is(
+ document.activeElement,
+ tab1,
+ "3rd click on activated tab1 keeps tab activated"
+ );
+
+ gBrowser.addEventListener("TabSwitchDone", step5);
+ EventUtils.synthesizeMouseAtCenter(tab2, {});
+}
+
+function step5() {
+ gBrowser.removeEventListener("TabSwitchDone", step5);
+
+ // The tabbox selects a tab within a setTimeout in a bubbling mousedown event
+ // listener, and focuses the current tab if another tab previously had focus.
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "click on tab2 while tab1 is activated selects tab"
+ );
+ is(
+ document.activeElement,
+ tab2,
+ "click on tab2 while tab1 is activated activates tab"
+ );
+
+ info("focusing content then sending middle-button mousedown to tab2.");
+ gBrowser.selectedBrowser.focus();
+
+ EventUtils.synthesizeMouseAtCenter(tab2, { button: 1, type: "mousedown" });
+ setTimeout(step6, 0);
+}
+
+function step6() {
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "middle-button mousedown on selected tab2 keeps tab selected"
+ );
+ isnot(
+ document.activeElement,
+ tab2,
+ "middle-button mousedown on selected tab2 does not activate tab"
+ );
+
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab1);
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug462673.js b/browser/base/content/test/general/browser_bug462673.js
new file mode 100644
index 0000000000..fb550cb2b5
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug462673.js
@@ -0,0 +1,66 @@
+add_task(async function () {
+ var win = openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no"
+ );
+ await SimpleTest.promiseFocus(win);
+
+ let tab = win.gBrowser.tabs[0];
+ await promiseTabLoadEvent(
+ tab,
+ getRootDirectory(gTestPath) + "test_bug462673.html"
+ );
+
+ is(
+ win.gBrowser.browsers.length,
+ 2,
+ "test_bug462673.html has opened a second tab"
+ );
+ is(
+ win.gBrowser.selectedTab,
+ tab.nextElementSibling,
+ "dependent tab is selected"
+ );
+ win.gBrowser.removeTab(tab);
+
+ // Closing a tab will also close its parent chrome window, but async
+ await BrowserTestUtils.domWindowClosed(win);
+});
+
+add_task(async function () {
+ var win = openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no"
+ );
+ await SimpleTest.promiseFocus(win);
+
+ let tab = win.gBrowser.tabs[0];
+ await promiseTabLoadEvent(
+ tab,
+ getRootDirectory(gTestPath) + "test_bug462673.html"
+ );
+
+ var newTab = BrowserTestUtils.addTab(win.gBrowser);
+ var newBrowser = newTab.linkedBrowser;
+ win.gBrowser.removeTab(tab);
+ ok(!win.closed, "Window stays open");
+ if (!win.closed) {
+ is(win.gBrowser.tabs.length, 1, "Window has one tab");
+ is(win.gBrowser.browsers.length, 1, "Window has one browser");
+ is(win.gBrowser.selectedTab, newTab, "Remaining tab is selected");
+ is(
+ win.gBrowser.selectedBrowser,
+ newBrowser,
+ "Browser for remaining tab is selected"
+ );
+ is(
+ win.gBrowser.tabbox.selectedPanel,
+ newBrowser.parentNode.parentNode.parentNode,
+ "Panel for remaining tab is selected"
+ );
+ }
+
+ await promiseWindowClosed(win);
+});
diff --git a/browser/base/content/test/general/browser_bug479408.js b/browser/base/content/test/general/browser_bug479408.js
new file mode 100644
index 0000000000..f616fa0ee4
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug479408.js
@@ -0,0 +1,23 @@
+function test() {
+ waitForExplicitFinish();
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/base/content/test/general/browser_bug479408_sample.html"
+ ));
+
+ BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "DOMLinkAdded",
+ true
+ ).then(() => {
+ executeSoon(function () {
+ ok(
+ !tab.linkedBrowser.engines,
+ "the subframe's search engine wasn't detected"
+ );
+
+ gBrowser.removeTab(tab);
+ finish();
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug479408_sample.html b/browser/base/content/test/general/browser_bug479408_sample.html
new file mode 100644
index 0000000000..f83f02bb9d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug479408_sample.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<title>Testcase for bug 479408</title>
+
+<iframe src='data:text/html,<link%20rel="search"%20type="application/opensearchdescription+xml"%20title="Search%20bug%20479408"%20href="http://example.com/search.xml">'>
diff --git a/browser/base/content/test/general/browser_bug481560.js b/browser/base/content/test/general/browser_bug481560.js
new file mode 100644
index 0000000000..737ac729a2
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug481560.js
@@ -0,0 +1,16 @@
+add_task(async function testTabCloseShortcut() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(win);
+
+ function onTabClose() {
+ ok(false, "shouldn't have gotten the TabClose event for the last tab");
+ }
+ var tab = win.gBrowser.selectedTab;
+ tab.addEventListener("TabClose", onTabClose);
+
+ EventUtils.synthesizeKey("w", { accelKey: true }, win);
+
+ ok(win.closed, "accel+w closed the window immediately");
+
+ tab.removeEventListener("TabClose", onTabClose);
+});
diff --git a/browser/base/content/test/general/browser_bug484315.js b/browser/base/content/test/general/browser_bug484315.js
new file mode 100644
index 0000000000..21b4e69a33
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug484315.js
@@ -0,0 +1,14 @@
+add_task(async function test() {
+ window.open("about:blank", "", "width=100,height=100,noopener");
+
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ Services.prefs.setBoolPref("browser.tabs.closeWindowWithLastTab", false);
+ win.gBrowser.removeCurrentTab();
+ ok(win.closed, "popup is closed");
+
+ // clean up
+ if (!win.closed) {
+ win.close();
+ }
+ Services.prefs.clearUserPref("browser.tabs.closeWindowWithLastTab");
+});
diff --git a/browser/base/content/test/general/browser_bug491431.js b/browser/base/content/test/general/browser_bug491431.js
new file mode 100644
index 0000000000..d8eaa15f45
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug491431.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/. */
+
+var testPage = "data:text/plain,test bug 491431 Page";
+
+function test() {
+ waitForExplicitFinish();
+
+ let newWin, tabA, tabB;
+
+ // test normal close
+ tabA = BrowserTestUtils.addTab(gBrowser, testPage);
+ gBrowser.tabContainer.addEventListener(
+ "TabClose",
+ function (firstTabCloseEvent) {
+ ok(!firstTabCloseEvent.detail.adoptedBy, "This was a normal tab close");
+
+ // test tab close by moving
+ tabB = BrowserTestUtils.addTab(gBrowser, testPage);
+ gBrowser.tabContainer.addEventListener(
+ "TabClose",
+ function (secondTabCloseEvent) {
+ executeSoon(function () {
+ ok(
+ secondTabCloseEvent.detail.adoptedBy,
+ "This was a tab closed by moving"
+ );
+
+ // cleanup
+ newWin.close();
+ executeSoon(finish);
+ });
+ },
+ { capture: true, once: true }
+ );
+ newWin = gBrowser.replaceTabWithWindow(tabB);
+ },
+ { capture: true, once: true }
+ );
+ gBrowser.removeTab(tabA);
+}
diff --git a/browser/base/content/test/general/browser_bug495058.js b/browser/base/content/test/general/browser_bug495058.js
new file mode 100644
index 0000000000..95a444bf6a
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug495058.js
@@ -0,0 +1,53 @@
+/**
+ * Tests that the right elements of a tab are focused when it is
+ * torn out into its own window.
+ */
+
+const URIS = [
+ "about:blank",
+ "about:home",
+ "about:sessionrestore",
+ "about:privatebrowsing",
+];
+
+add_task(async function () {
+ for (let uri of URIS) {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, uri);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let isRemote = tab.linkedBrowser.isRemoteBrowser;
+
+ let win = gBrowser.replaceTabWithWindow(tab);
+
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == win
+ );
+ // In the e10s case, we wait for the content to first paint before we focus
+ // the URL in the new window, to optimize for content paint time.
+ if (isRemote) {
+ await win.gBrowserInit.firstContentWindowPaintPromise;
+ }
+
+ tab = win.gBrowser.selectedTab;
+
+ Assert.equal(
+ win.gBrowser.currentURI.spec,
+ uri,
+ uri + ": uri loaded in detached tab"
+ );
+
+ const expectedActiveElement = tab.isEmpty
+ ? win.gURLBar.inputField
+ : win.gBrowser.selectedBrowser;
+ Assert.equal(
+ win.document.activeElement,
+ expectedActiveElement,
+ `${uri}: the active element is expected: ${win.document.activeElement?.nodeName}`
+ );
+ Assert.equal(win.gURLBar.value, "", uri + ": urlbar is empty");
+ Assert.ok(win.gURLBar.placeholder, uri + ": placeholder text is present");
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/browser/base/content/test/general/browser_bug519216.js b/browser/base/content/test/general/browser_bug519216.js
new file mode 100644
index 0000000000..d83d082556
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug519216.js
@@ -0,0 +1,48 @@
+function test() {
+ waitForExplicitFinish();
+ gBrowser.addProgressListener(progressListener1);
+ gBrowser.addProgressListener(progressListener2);
+ gBrowser.addProgressListener(progressListener3);
+ BrowserTestUtils.loadURIString(gBrowser, "data:text/plain,bug519216");
+}
+
+var calledListener1 = false;
+var progressListener1 = {
+ onLocationChange: function onLocationChange() {
+ calledListener1 = true;
+ gBrowser.removeProgressListener(this);
+ },
+};
+
+var calledListener2 = false;
+var progressListener2 = {
+ onLocationChange: function onLocationChange() {
+ ok(calledListener1, "called progressListener1 before progressListener2");
+ calledListener2 = true;
+ gBrowser.removeProgressListener(this);
+ },
+};
+
+var progressListener3 = {
+ onLocationChange: function onLocationChange() {
+ ok(calledListener2, "called progressListener2 before progressListener3");
+ gBrowser.removeProgressListener(this);
+ gBrowser.addProgressListener(progressListener4);
+ executeSoon(function () {
+ expectListener4 = true;
+ gBrowser.reload();
+ });
+ },
+};
+
+var expectListener4 = false;
+var progressListener4 = {
+ onLocationChange: function onLocationChange() {
+ ok(
+ expectListener4,
+ "didn't call progressListener4 for the first location change"
+ );
+ gBrowser.removeProgressListener(this);
+ executeSoon(finish);
+ },
+};
diff --git a/browser/base/content/test/general/browser_bug520538.js b/browser/base/content/test/general/browser_bug520538.js
new file mode 100644
index 0000000000..234747fcbf
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug520538.js
@@ -0,0 +1,27 @@
+function test() {
+ var tabCount = gBrowser.tabs.length;
+ gBrowser.selectedBrowser.focus();
+ window.browserDOMWindow.openURI(
+ makeURI("about:blank"),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ is(
+ gBrowser.tabs.length,
+ tabCount + 1,
+ "'--new-tab about:blank' opens a new tab"
+ );
+ is(
+ gBrowser.selectedTab,
+ gBrowser.tabs[tabCount],
+ "'--new-tab about:blank' selects the new tab"
+ );
+ is(
+ document.activeElement,
+ gURLBar.inputField,
+ "'--new-tab about:blank' focuses the location bar"
+ );
+ gBrowser.removeCurrentTab();
+}
diff --git a/browser/base/content/test/general/browser_bug521216.js b/browser/base/content/test/general/browser_bug521216.js
new file mode 100644
index 0000000000..8c885bbcc8
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug521216.js
@@ -0,0 +1,68 @@
+var expected = [
+ "TabOpen",
+ "onStateChange",
+ "onLocationChange",
+ "onLinkIconAvailable",
+];
+var actual = [];
+var tabIndex = -1;
+this.__defineGetter__("tab", () => gBrowser.tabs[tabIndex]);
+
+function test() {
+ waitForExplicitFinish();
+ tabIndex = gBrowser.tabs.length;
+ gBrowser.addTabsProgressListener(progressListener);
+ gBrowser.tabContainer.addEventListener("TabOpen", TabOpen);
+ BrowserTestUtils.addTab(
+ gBrowser,
+ "data:text/html,<html><head><link href='about:logo' rel='shortcut icon'>"
+ );
+}
+
+function recordEvent(aName) {
+ info("got " + aName);
+ if (!actual.includes(aName)) {
+ actual.push(aName);
+ }
+ if (actual.length == expected.length) {
+ is(
+ actual.toString(),
+ expected.toString(),
+ "got events and progress notifications in expected order"
+ );
+
+ executeSoon(
+ // eslint-disable-next-line no-shadow
+ function (tab) {
+ gBrowser.removeTab(tab);
+ gBrowser.removeTabsProgressListener(progressListener);
+ gBrowser.tabContainer.removeEventListener("TabOpen", TabOpen);
+ finish();
+ }.bind(null, tab)
+ );
+ }
+}
+
+function TabOpen(aEvent) {
+ if (aEvent.target == tab) {
+ recordEvent("TabOpen");
+ }
+}
+
+var progressListener = {
+ onLocationChange: function onLocationChange(aBrowser) {
+ if (aBrowser == tab.linkedBrowser) {
+ recordEvent("onLocationChange");
+ }
+ },
+ onStateChange: function onStateChange(aBrowser) {
+ if (aBrowser == tab.linkedBrowser) {
+ recordEvent("onStateChange");
+ }
+ },
+ onLinkIconAvailable: function onLinkIconAvailable(aBrowser) {
+ if (aBrowser == tab.linkedBrowser) {
+ recordEvent("onLinkIconAvailable");
+ }
+ },
+};
diff --git a/browser/base/content/test/general/browser_bug533232.js b/browser/base/content/test/general/browser_bug533232.js
new file mode 100644
index 0000000000..7f6225b519
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug533232.js
@@ -0,0 +1,56 @@
+function test() {
+ var tab1 = gBrowser.selectedTab;
+ var tab2 = BrowserTestUtils.addTab(gBrowser);
+ var childTab1;
+ var childTab2;
+
+ childTab1 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ relatedToCurrent: true,
+ });
+ gBrowser.selectedTab = childTab1;
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(
+ idx(gBrowser.selectedTab),
+ idx(tab1),
+ "closing a tab next to its parent selects the parent"
+ );
+
+ childTab1 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ relatedToCurrent: true,
+ });
+ gBrowser.selectedTab = tab2;
+ gBrowser.selectedTab = childTab1;
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(
+ idx(gBrowser.selectedTab),
+ idx(tab2),
+ "closing a tab next to its parent doesn't select the parent if another tab had been selected ad interim"
+ );
+
+ gBrowser.selectedTab = tab1;
+ childTab1 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ relatedToCurrent: true,
+ });
+ childTab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ relatedToCurrent: true,
+ });
+ gBrowser.selectedTab = childTab1;
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(
+ idx(gBrowser.selectedTab),
+ idx(childTab2),
+ "closing a tab next to its parent selects the next tab with the same parent"
+ );
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(
+ idx(gBrowser.selectedTab),
+ idx(tab2),
+ "closing the last tab in a set of child tabs doesn't go back to the parent"
+ );
+
+ gBrowser.removeTab(tab2, { skipPermitUnload: true });
+}
+
+function idx(tab) {
+ return Array.prototype.indexOf.call(gBrowser.tabs, tab);
+}
diff --git a/browser/base/content/test/general/browser_bug537013.js b/browser/base/content/test/general/browser_bug537013.js
new file mode 100644
index 0000000000..5c871a759c
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug537013.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests for bug 537013 to ensure proper tab-sequestration of find bar. */
+
+var tabs = [];
+var texts = [
+ "This side up.",
+ "The world is coming to an end. Please log off.",
+ "Klein bottle for sale. Inquire within.",
+ "To err is human; to forgive is not company policy.",
+];
+
+var HasFindClipboard = Services.clipboard.isClipboardTypeSupported(
+ Services.clipboard.kFindClipboard
+);
+
+function addTabWithText(aText, aCallback) {
+ let newTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "data:text/html;charset=utf-8,<h1 id='h1'>" + aText + "</h1>"
+ );
+ tabs.push(newTab);
+ gBrowser.selectedTab = newTab;
+}
+
+function setFindString(aString) {
+ gFindBar.open();
+ gFindBar._findField.focus();
+ gFindBar._findField.select();
+ EventUtils.sendString(aString);
+ is(gFindBar._findField.value, aString, "Set the field correctly!");
+}
+
+var newWindow;
+
+function test() {
+ waitForExplicitFinish();
+ registerCleanupFunction(function () {
+ while (tabs.length) {
+ gBrowser.removeTab(tabs.pop());
+ }
+ });
+ texts.forEach(aText => addTabWithText(aText));
+
+ // Set up the first tab
+ gBrowser.selectedTab = tabs[0];
+
+ gBrowser.getFindBar().then(initialTest);
+}
+
+function initialTest() {
+ setFindString(texts[0]);
+ // Turn on highlight for testing bug 891638
+ gFindBar.getElement("highlight").checked = true;
+
+ // Make sure the second tab is correct, then set it up
+ gBrowser.selectedTab = tabs[1];
+ gBrowser.selectedTab.addEventListener("TabFindInitialized", continueTests1, {
+ once: true,
+ });
+ // Initialize the findbar
+ gBrowser.getFindBar();
+}
+function continueTests1() {
+ ok(true, "'TabFindInitialized' event properly dispatched!");
+ ok(gFindBar.hidden, "Second tab doesn't show find bar!");
+ gFindBar.open();
+ is(
+ gFindBar._findField.value,
+ texts[0],
+ "Second tab kept old find value for new initialization!"
+ );
+ setFindString(texts[1]);
+
+ // Confirm the first tab is still correct, ensure re-hiding works as expected
+ gBrowser.selectedTab = tabs[0];
+ ok(!gFindBar.hidden, "First tab shows find bar!");
+ // When the Find Clipboard is supported, this test not relevant.
+ if (!HasFindClipboard) {
+ is(gFindBar._findField.value, texts[0], "First tab persists find value!");
+ }
+ ok(
+ gFindBar.getElement("highlight").checked,
+ "Highlight button state persists!"
+ );
+
+ // While we're here, let's test bug 253793
+ gBrowser.reload();
+ gBrowser.addEventListener("DOMContentLoaded", continueTests2, true);
+}
+
+function continueTests2() {
+ gBrowser.removeEventListener("DOMContentLoaded", continueTests2, true);
+ ok(gFindBar.getElement("highlight").checked, "Highlight never reset!");
+ continueTests3();
+}
+
+function continueTests3() {
+ ok(gFindBar.getElement("highlight").checked, "Highlight button reset!");
+ gFindBar.close();
+ ok(gFindBar.hidden, "First tab doesn't show find bar!");
+ gBrowser.selectedTab = tabs[1];
+ ok(!gFindBar.hidden, "Second tab shows find bar!");
+ // Test for bug 892384
+ is(
+ gFindBar._findField.getAttribute("focused"),
+ "true",
+ "Open findbar refocused on tab change!"
+ );
+ gURLBar.focus();
+ gBrowser.selectedTab = tabs[0];
+ ok(gFindBar.hidden, "First tab doesn't show find bar!");
+
+ // Set up a third tab, no tests here
+ gBrowser.selectedTab = tabs[2];
+ gBrowser.selectedTab.addEventListener("TabFindInitialized", continueTests4, {
+ once: true,
+ });
+ gBrowser.getFindBar();
+}
+
+function continueTests4() {
+ setFindString(texts[2]);
+
+ // Now we jump to the second, then first, and then fourth
+ gBrowser.selectedTab = tabs[1];
+ // Test for bug 892384
+ ok(
+ !gFindBar._findField.hasAttribute("focused"),
+ "Open findbar not refocused on tab change!"
+ );
+ gBrowser.selectedTab = tabs[0];
+ gBrowser.selectedTab = tabs[3];
+ ok(gFindBar.hidden, "Fourth tab doesn't show find bar!");
+ is(gFindBar, gBrowser.getFindBar(), "Find bar is right one!");
+ gFindBar.open();
+ // Disabled the following assertion due to intermittent failure on OSX 10.6 Debug.
+ if (!HasFindClipboard) {
+ is(
+ gFindBar._findField.value,
+ texts[1],
+ "Fourth tab has second tab's find value!"
+ );
+ }
+
+ newWindow = gBrowser.replaceTabWithWindow(tabs.pop());
+ whenDelayedStartupFinished(newWindow, checkNewWindow);
+}
+
+// Test that findbar gets restored when a tab is moved to a new window.
+function checkNewWindow() {
+ ok(!newWindow.gFindBar.hidden, "New window shows find bar!");
+ // Disabled the following assertion due to intermittent failure on OSX 10.6 Debug.
+ if (!HasFindClipboard) {
+ is(
+ newWindow.gFindBar._findField.value,
+ texts[1],
+ "New window find bar has correct find value!"
+ );
+ ok(
+ !newWindow.gFindBar.getElement("find-next").disabled,
+ "New window findbar has enabled buttons!"
+ );
+ }
+ newWindow.close();
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug537474.js b/browser/base/content/test/general/browser_bug537474.js
new file mode 100644
index 0000000000..b890bf2fea
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug537474.js
@@ -0,0 +1,20 @@
+add_task(async function () {
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:mozilla"
+ );
+ window.browserDOMWindow.openURI(
+ makeURI("about:mozilla"),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ await browserLoadedPromise;
+ is(
+ gBrowser.currentURI.spec,
+ "about:mozilla",
+ "page loads in the current content window"
+ );
+});
diff --git a/browser/base/content/test/general/browser_bug563588.js b/browser/base/content/test/general/browser_bug563588.js
new file mode 100644
index 0000000000..26c8fd1767
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug563588.js
@@ -0,0 +1,42 @@
+function press(key, expectedPos) {
+ var originalSelectedTab = gBrowser.selectedTab;
+ EventUtils.synthesizeKey("VK_" + key.toUpperCase(), {
+ accelKey: true,
+ shiftKey: true,
+ });
+ is(
+ gBrowser.selectedTab,
+ originalSelectedTab,
+ "shift+accel+" + key + " doesn't change which tab is selected"
+ );
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ expectedPos,
+ "shift+accel+" + key + " moves the tab to the expected position"
+ );
+ is(
+ document.activeElement,
+ gBrowser.selectedTab,
+ "shift+accel+" + key + " leaves the selected tab focused"
+ );
+}
+
+function test() {
+ BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.addTab(gBrowser);
+ is(gBrowser.tabs.length, 3, "got three tabs");
+ is(gBrowser.tabs[0], gBrowser.selectedTab, "first tab is selected");
+
+ gBrowser.selectedTab.focus();
+ is(document.activeElement, gBrowser.selectedTab, "selected tab is focused");
+
+ press("right", 1);
+ press("down", 2);
+ press("left", 1);
+ press("up", 0);
+ press("end", 2);
+ press("home", 0);
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+}
diff --git a/browser/base/content/test/general/browser_bug565575.js b/browser/base/content/test/general/browser_bug565575.js
new file mode 100644
index 0000000000..6176c537e3
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug565575.js
@@ -0,0 +1,21 @@
+add_task(async function () {
+ gBrowser.selectedBrowser.focus();
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => BrowserOpenTab(),
+ false
+ );
+ ok(gURLBar.focused, "location bar is focused for a new tab");
+
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+ ok(
+ !gURLBar.focused,
+ "location bar isn't focused for the previously selected tab"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
+ ok(gURLBar.focused, "location bar is re-focused when selecting the new tab");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug567306.js b/browser/base/content/test/general/browser_bug567306.js
new file mode 100644
index 0000000000..3d3e47e17d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug567306.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var HasFindClipboard = Services.clipboard.isClipboardTypeSupported(
+ Services.clipboard.kFindClipboard
+);
+
+add_task(async function () {
+ let newwindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ let selectedBrowser = newwindow.gBrowser.selectedBrowser;
+ await new Promise((resolve, reject) => {
+ BrowserTestUtils.waitForContentEvent(
+ selectedBrowser,
+ "pageshow",
+ true,
+ event => {
+ return event.target.location != "about:blank";
+ }
+ ).then(function pageshowListener() {
+ ok(
+ true,
+ "pageshow listener called: " + newwindow.gBrowser.currentURI.spec
+ );
+ resolve();
+ });
+ selectedBrowser.loadURI(
+ Services.io.newURI("data:text/html,<h1 id='h1'>Select Me</h1>"),
+ {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ });
+
+ await SimpleTest.promiseFocus(newwindow);
+
+ ok(!newwindow.gFindBarInitialized, "find bar is not yet initialized");
+ let findBar = await newwindow.gFindBarPromise;
+
+ await SpecialPowers.spawn(selectedBrowser, [], async function () {
+ let elt = content.document.getElementById("h1");
+ let selection = content.getSelection();
+ let range = content.document.createRange();
+ range.setStart(elt, 0);
+ range.setEnd(elt, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ });
+
+ await findBar.onFindCommand();
+
+ // When the OS supports the Find Clipboard (OSX), the find field value is
+ // persisted across Fx sessions, thus not useful to test.
+ if (!HasFindClipboard) {
+ is(
+ findBar._findField.value,
+ "Select Me",
+ "Findbar is initialized with selection"
+ );
+ }
+ findBar.close();
+ await promiseWindowClosed(newwindow);
+});
diff --git a/browser/base/content/test/general/browser_bug575561.js b/browser/base/content/test/general/browser_bug575561.js
new file mode 100644
index 0000000000..a429cdf5c7
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug575561.js
@@ -0,0 +1,118 @@
+requestLongerTimeout(2);
+
+const TEST_URL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/general/app_bug575561.html";
+
+add_task(async function () {
+ SimpleTest.requestCompleteLog();
+
+ // allow top level data: URI navigations, otherwise clicking data: link fails
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+
+ // Pinned: Link to the same domain should not open a new tab
+ // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html
+ await testLink(0, true, false);
+ // Pinned: Link to a different subdomain should open a new tab
+ // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html
+ await testLink(1, true, true);
+
+ // Pinned: Link to a different domain should open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
+ await testLink(2, true, true);
+
+ // Not Pinned: Link to a different domain should not open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
+ await testLink(2, false, false);
+
+ // Pinned: Targetted link should open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo"
+ await testLink(3, true, true);
+
+ // Pinned: Link in a subframe should not open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe
+ await testLink(0, true, false, true);
+
+ // Pinned: Link to the same domain (with www prefix) should not open a new tab
+ // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html
+ await testLink(4, true, false);
+
+ // Pinned: Link to a data: URI should not open a new tab
+ // Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>
+ await testLink(5, true, false);
+
+ // Pinned: Link to an about: URI should not open a new tab
+ // Tests link to about:logo
+ await testLink(
+ function (doc) {
+ let link = doc.createElement("a");
+ link.textContent = "Link to Mozilla";
+ link.href = "about:logo";
+ doc.body.appendChild(link);
+ return link;
+ },
+ true,
+ false,
+ false,
+ "about:robots"
+ );
+});
+
+async function testLink(
+ aLinkIndexOrFunction,
+ pinTab,
+ expectNewTab,
+ testSubFrame,
+ aURL = TEST_URL
+) {
+ let appTab = BrowserTestUtils.addTab(gBrowser, aURL, { skipAnimation: true });
+ if (pinTab) {
+ gBrowser.pinTab(appTab);
+ }
+ gBrowser.selectedTab = appTab;
+
+ let browser = appTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let promise;
+ if (expectNewTab) {
+ promise = BrowserTestUtils.waitForNewTab(gBrowser).then(tab => {
+ let loaded = tab.linkedBrowser.documentURI.spec;
+ BrowserTestUtils.removeTab(tab);
+ return loaded;
+ });
+ } else {
+ promise = BrowserTestUtils.browserLoaded(browser, testSubFrame);
+ }
+
+ let href;
+ if (typeof aLinkIndexOrFunction === "function") {
+ ok(!browser.isRemoteBrowser, "don't pass a function for a remote browser");
+ let link = aLinkIndexOrFunction(browser.contentDocument);
+ info("Clicking " + link.textContent);
+ link.click();
+ href = link.href;
+ } else {
+ href = await SpecialPowers.spawn(
+ browser,
+ [[testSubFrame, aLinkIndexOrFunction]],
+ function ([subFrame, index]) {
+ let doc = subFrame
+ ? content.document.querySelector("iframe").contentDocument
+ : content.document;
+ let link = doc.querySelectorAll("a")[index];
+
+ info("Clicking " + link.textContent);
+ link.click();
+ return link.href;
+ }
+ );
+ }
+
+ info(`Waiting on load of ${href}`);
+ let loaded = await promise;
+ is(loaded, href, "loaded the right document");
+ BrowserTestUtils.removeTab(appTab);
+}
diff --git a/browser/base/content/test/general/browser_bug577121.js b/browser/base/content/test/general/browser_bug577121.js
new file mode 100644
index 0000000000..cbaa379e85
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug577121.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ // Disable tab animations
+ gReduceMotionOverride = true;
+
+ // Open 2 other tabs, and pin the second one. Like that, the initial tab
+ // should get closed.
+ let testTab1 = BrowserTestUtils.addTab(gBrowser);
+ let testTab2 = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.pinTab(testTab2);
+
+ // Now execute "Close other Tabs" on the first manually opened tab (tab1).
+ // -> tab2 ist pinned, tab1 should remain open and the initial tab should
+ // get closed.
+ gBrowser.removeAllTabsBut(testTab1);
+
+ is(gBrowser.tabs.length, 2, "there are two remaining tabs open");
+ is(gBrowser.tabs[0], testTab2, "pinned tab2 stayed open");
+ is(gBrowser.tabs[1], testTab1, "tab1 stayed open");
+
+ // Cleanup. Close only one tab because we need an opened tab at the end of
+ // the test.
+ gBrowser.removeTab(testTab2);
+}
diff --git a/browser/base/content/test/general/browser_bug578534.js b/browser/base/content/test/general/browser_bug578534.js
new file mode 100644
index 0000000000..04b5fe9cfd
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug578534.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+add_task(async function test() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let uriString = "http://example.com/";
+ let cookieBehavior = "network.cookie.cookieBehavior";
+
+ await SpecialPowers.pushPrefEnv({ set: [[cookieBehavior, 2]] });
+ PermissionTestUtils.add(uriString, "cookie", Services.perms.ALLOW_ACTION);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: uriString },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], function () {
+ is(
+ content.navigator.cookieEnabled,
+ true,
+ "navigator.cookieEnabled should be true"
+ );
+ });
+ }
+ );
+
+ PermissionTestUtils.add(uriString, "cookie", Services.perms.UNKNOWN_ACTION);
+});
diff --git a/browser/base/content/test/general/browser_bug579872.js b/browser/base/content/test/general/browser_bug579872.js
new file mode 100644
index 0000000000..47de7ea240
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug579872.js
@@ -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/. */
+
+add_task(async function () {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = BrowserTestUtils.addTab(gBrowser, "http://example.com");
+ await BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+
+ gBrowser.pinTab(newTab);
+ gBrowser.selectedTab = newTab;
+
+ openTrustedLinkIn("javascript:var x=0;", "current");
+ is(gBrowser.tabs.length, 2, "Should open in current tab");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ openTrustedLinkIn("http://example.com/1", "current");
+ is(gBrowser.tabs.length, 2, "Should open in current tab");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ openTrustedLinkIn("http://example.org/", "current");
+ is(gBrowser.tabs.length, 3, "Should open in new tab");
+
+ await BrowserTestUtils.removeTab(newTab);
+ await BrowserTestUtils.removeTab(gBrowser.tabs[1]); // example.org tab
+});
diff --git a/browser/base/content/test/general/browser_bug581253.js b/browser/base/content/test/general/browser_bug581253.js
new file mode 100644
index 0000000000..a901ce96e1
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug581253.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var testURL = "data:text/plain,nothing but plain text";
+var testTag = "581253_tag";
+
+add_task(async function test_remove_bookmark_with_tag_via_edit_bookmark() {
+ waitForExplicitFinish();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "",
+ url: testURL,
+ });
+
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch({ url: testURL }),
+ "the test url is bookmarked"
+ );
+
+ BrowserTestUtils.loadURIString(gBrowser, testURL);
+
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status == BookmarkingUI.STATUS_STARRED,
+ "star button indicates that the page is bookmarked"
+ );
+
+ PlacesUtils.tagging.tagURI(makeURI(testURL), [testTag]);
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ StarUI.panel,
+ "popupshown"
+ );
+
+ BookmarkingUI.star.click();
+
+ await popupShownPromise;
+
+ let tagsField = document.getElementById("editBMPanel_tagsField");
+ Assert.ok(tagsField.value == testTag, "tags field value was set");
+ tagsField.focus();
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ StarUI.panel,
+ "popuphidden"
+ );
+
+ let removeNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => unescape(event.url) == testURL)
+ );
+
+ let removeButton = document.getElementById("editBookmarkPanelRemoveButton");
+ removeButton.click();
+
+ await popupHiddenPromise;
+
+ await removeNotification;
+
+ is(
+ BookmarkingUI.status,
+ BookmarkingUI.STATUS_UNSTARRED,
+ "star button indicates that the bookmark has been removed"
+ );
+});
diff --git a/browser/base/content/test/general/browser_bug585785.js b/browser/base/content/test/general/browser_bug585785.js
new file mode 100644
index 0000000000..23e0c5ddf5
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug585785.js
@@ -0,0 +1,48 @@
+var tab;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ tab = BrowserTestUtils.addTab(gBrowser);
+ isnot(
+ tab.getAttribute("fadein"),
+ "true",
+ "newly opened tab is yet to fade in"
+ );
+
+ // Try to remove the tab right before the opening animation's first frame
+ window.requestAnimationFrame(checkAnimationState);
+}
+
+function checkAnimationState() {
+ is(tab.getAttribute("fadein"), "true", "tab opening animation initiated");
+
+ info(window.getComputedStyle(tab).maxWidth);
+ gBrowser.removeTab(tab, { animate: true });
+ if (!tab.parentNode) {
+ ok(
+ true,
+ "tab removed synchronously since the opening animation hasn't moved yet"
+ );
+ finish();
+ return;
+ }
+
+ info(
+ "tab didn't close immediately, so the tab opening animation must have started moving"
+ );
+ info("waiting for the tab to close asynchronously");
+ tab.addEventListener(
+ "TabAnimationEnd",
+ function listener() {
+ executeSoon(function () {
+ ok(!tab.parentNode, "tab removed asynchronously");
+ finish();
+ });
+ },
+ { once: true }
+ );
+}
diff --git a/browser/base/content/test/general/browser_bug585830.js b/browser/base/content/test/general/browser_bug585830.js
new file mode 100644
index 0000000000..2267a8b2ac
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug585830.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = tab2;
+
+ gBrowser.removeCurrentTab({ animate: true });
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, tab1, "First tab should be selected");
+ gBrowser.removeTab(tab2);
+
+ // test for "null has no properties" fix. See Bug 585830 Comment 13
+ gBrowser.removeCurrentTab({ animate: true });
+ try {
+ gBrowser.tabContainer.advanceSelectedTab(-1, false);
+ } catch (err) {
+ ok(false, "Shouldn't throw");
+ }
+
+ gBrowser.removeTab(tab1);
+}
diff --git a/browser/base/content/test/general/browser_bug594131.js b/browser/base/content/test/general/browser_bug594131.js
new file mode 100644
index 0000000000..db06b69425
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug594131.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = BrowserTestUtils.addTab(gBrowser, "http://example.com");
+ waitForExplicitFinish();
+ BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(mainPart);
+
+ function mainPart() {
+ gBrowser.pinTab(newTab);
+ gBrowser.selectedTab = newTab;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ openTrustedLinkIn("http://example.org/", "current", {
+ inBackground: true,
+ });
+ isnot(gBrowser.selectedTab, newTab, "shouldn't load in background");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(gBrowser.tabs[1]); // example.org tab
+ finish();
+ }
+}
diff --git a/browser/base/content/test/general/browser_bug596687.js b/browser/base/content/test/general/browser_bug596687.js
new file mode 100644
index 0000000000..8c68cd5a03
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug596687.js
@@ -0,0 +1,28 @@
+add_task(async function test() {
+ var tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ var gotTabAttrModified = false;
+ var gotTabClose = false;
+
+ function onTabClose() {
+ gotTabClose = true;
+ tab.addEventListener("TabAttrModified", onTabAttrModified);
+ }
+
+ function onTabAttrModified() {
+ gotTabAttrModified = true;
+ }
+
+ tab.addEventListener("TabClose", onTabClose);
+
+ BrowserTestUtils.removeTab(tab);
+
+ ok(gotTabClose, "should have got the TabClose event");
+ ok(
+ !gotTabAttrModified,
+ "shouldn't have got the TabAttrModified event after TabClose"
+ );
+
+ tab.removeEventListener("TabClose", onTabClose);
+ tab.removeEventListener("TabAttrModified", onTabAttrModified);
+});
diff --git a/browser/base/content/test/general/browser_bug597218.js b/browser/base/content/test/general/browser_bug597218.js
new file mode 100644
index 0000000000..963912c9da
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug597218.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ // establish initial state
+ is(gBrowser.tabs.length, 1, "we start with one tab");
+
+ // create a tab
+ let tab = gBrowser.addTab("about:blank", {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ ok(!tab.hidden, "tab starts out not hidden");
+ is(gBrowser.tabs.length, 2, "we now have two tabs");
+
+ // make sure .hidden is read-only
+ tab.hidden = true;
+ ok(!tab.hidden, "can't set .hidden directly");
+
+ // hide the tab
+ gBrowser.hideTab(tab);
+ ok(tab.hidden, "tab is hidden");
+
+ // now pin it and make sure it gets unhidden
+ gBrowser.pinTab(tab);
+ ok(tab.pinned, "tab was pinned");
+ ok(!tab.hidden, "tab was unhidden");
+
+ // try hiding it now that it's pinned; shouldn't be able to
+ gBrowser.hideTab(tab);
+ ok(!tab.hidden, "tab did not hide");
+
+ // clean up
+ gBrowser.removeTab(tab);
+ is(gBrowser.tabs.length, 1, "we finish with one tab");
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug609700.js b/browser/base/content/test/general/browser_bug609700.js
new file mode 100644
index 0000000000..8195eba4ec
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug609700.js
@@ -0,0 +1,28 @@
+function test() {
+ waitForExplicitFinish();
+
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ if (aTopic == "domwindowopened") {
+ Services.ww.unregisterNotification(notification);
+
+ ok(true, "duplicateTabIn opened a new window");
+
+ whenDelayedStartupFinished(
+ aSubject,
+ function () {
+ executeSoon(function () {
+ aSubject.close();
+ finish();
+ });
+ },
+ false
+ );
+ }
+ });
+
+ duplicateTabIn(gBrowser.selectedTab, "window");
+}
diff --git a/browser/base/content/test/general/browser_bug623893.js b/browser/base/content/test/general/browser_bug623893.js
new file mode 100644
index 0000000000..27751e74ad
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug623893.js
@@ -0,0 +1,50 @@
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ "data:text/plain;charset=utf-8,1",
+ async function (browser) {
+ BrowserTestUtils.loadURIString(
+ browser,
+ "data:text/plain;charset=utf-8,2"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(
+ browser,
+ "data:text/plain;charset=utf-8,3"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await duplicate(0, "maintained the original index");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await duplicate(-1, "went back");
+ await duplicate(1, "went forward");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ );
+});
+
+async function promiseGetIndex(browser) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ return SpecialPowers.spawn(browser, [], function () {
+ let shistory =
+ docShell.browsingContext.childSessionHistory.legacySHistory;
+ return shistory.index;
+ });
+ }
+
+ let shistory = browser.browsingContext.sessionHistory;
+ return shistory.index;
+}
+
+let duplicate = async function (delta, msg, cb) {
+ var startIndex = await promiseGetIndex(gBrowser.selectedBrowser);
+
+ duplicateTabIn(gBrowser.selectedTab, "tab", delta);
+
+ await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored");
+
+ let endIndex = await promiseGetIndex(gBrowser.selectedBrowser);
+ is(endIndex, startIndex + delta, msg);
+};
diff --git a/browser/base/content/test/general/browser_bug624734.js b/browser/base/content/test/general/browser_bug624734.js
new file mode 100644
index 0000000000..962c62c5d8
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug624734.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 624734 - Star UI has no tooltip until bookmarked page is visited
+
+function finishTest() {
+ let elem = document.getElementById("context-bookmarkpage");
+ let l10n = document.l10n.getAttributes(elem);
+ ok(
+ [
+ "main-context-menu-bookmark-page",
+ "main-context-menu-bookmark-page-with-shortcut",
+ "main-context-menu-bookmark-page-mac",
+ ].includes(l10n.id)
+ );
+
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ if (BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING) {
+ waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING,
+ finishTest,
+ "BookmarkingUI was updating for too long"
+ );
+ } else {
+ CustomizableUI.removeWidgetFromArea("bookmarks-menu-button");
+ finishTest();
+ }
+ });
+
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html"
+ );
+}
diff --git a/browser/base/content/test/general/browser_bug664672.js b/browser/base/content/test/general/browser_bug664672.js
new file mode 100644
index 0000000000..4f9dbcea9f
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug664672.js
@@ -0,0 +1,27 @@
+function test() {
+ waitForExplicitFinish();
+
+ var tab = BrowserTestUtils.addTab(gBrowser);
+
+ tab.addEventListener(
+ "TabClose",
+ function () {
+ ok(
+ tab.linkedBrowser,
+ "linkedBrowser should still exist during the TabClose event"
+ );
+
+ executeSoon(function () {
+ ok(
+ !tab.linkedBrowser,
+ "linkedBrowser should be gone after the TabClose event"
+ );
+
+ finish();
+ });
+ },
+ { once: true }
+ );
+
+ gBrowser.removeTab(tab);
+}
diff --git a/browser/base/content/test/general/browser_bug676619.js b/browser/base/content/test/general/browser_bug676619.js
new file mode 100644
index 0000000000..24d8d88447
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug676619.js
@@ -0,0 +1,225 @@
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+function waitForNewWindow() {
+ return new Promise(resolve => {
+ var listener = {
+ onOpenWindow: aXULWindow => {
+ info("Download window shown...");
+ Services.wm.removeListener(listener);
+
+ function downloadOnLoad() {
+ domwindow.removeEventListener("load", downloadOnLoad, true);
+
+ is(
+ domwindow.document.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Download page appeared"
+ );
+ resolve(domwindow);
+ }
+
+ var domwindow = aXULWindow.docShell.domWindow;
+ domwindow.addEventListener("load", downloadOnLoad, true);
+ },
+ onCloseWindow: aXULWindow => {},
+ };
+
+ Services.wm.addListener(listener);
+ registerCleanupFunction(() => {
+ try {
+ Services.wm.removeListener(listener);
+ } catch (e) {}
+ });
+ });
+}
+
+async function waitForFilePickerTest(link, name) {
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ ok(true, "Filepicker shown.");
+ is(name, fp.defaultString, " filename matches download name");
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ });
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+
+ await filePickerShownPromise;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ Assert.equal(
+ content.document.getElementById("unload-flag").textContent,
+ "Okay",
+ "beforeunload shouldn't have fired"
+ );
+ });
+}
+
+async function testLink(link, name) {
+ info("Checking " + link + " with name: " + name);
+
+ if (
+ Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types",
+ false
+ )
+ ) {
+ let winPromise = waitForNewWindow();
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+
+ let win = await winPromise;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ Assert.equal(
+ content.document.getElementById("unload-flag").textContent,
+ "Okay",
+ "beforeunload shouldn't have fired"
+ );
+ });
+
+ is(
+ win.document.getElementById("location").value,
+ name,
+ `file name should match (${link})`
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ } else {
+ await waitForFilePickerTest(link, name);
+ }
+}
+
+// Cross-origin URL does not trigger a download
+async function testLocation(link, url) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+
+ let tab = await tabPromise;
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function runTest(url) {
+ let tab = BrowserTestUtils.addTab(gBrowser, url);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await testLink("link1", "test.txt");
+ await testLink("link2", "video.ogg");
+ await testLink("link3", "just some video.ogg");
+ await testLink("link4", "with-target.txt");
+ await testLink("link5", "javascript.html");
+ await testLink("link6", "test.blob");
+ await testLink("link7", "test.file");
+ await testLink("link8", "download_page_3.txt");
+ await testLink("link9", "download_page_3.txt");
+ await testLink("link10", "download_page_4.txt");
+ await testLink("link11", "download_page_4.txt");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await testLocation("link12", "http://example.com/");
+
+ // Check that we enforce the correct extension if the website's
+ // is bogus or missing. These extensions can differ slightly (ogx vs ogg,
+ // htm vs html) on different OSes.
+ let oggExtension = getMIMEInfoForType("application/ogg").primaryExtension;
+ await testLink("link13", "no file extension." + oggExtension);
+
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1690051#c8
+ if (AppConstants.platform != "win") {
+ const PREF = "browser.download.sanitize_non_media_extensions";
+ ok(Services.prefs.getBoolPref(PREF), "pref is set before");
+
+ // Check that ics (iCal) extension is changed/fixed when the pref is true.
+ await testLink("link14", "dummy.ics");
+
+ // And not changed otherwise.
+ Services.prefs.setBoolPref(PREF, false);
+ await testLink("link14", "dummy.not-ics");
+ Services.prefs.clearUserPref(PREF);
+ }
+
+ await testLink("link15", "download_page_3.txt");
+ await testLink("link16", "download_page_3.txt");
+ await testLink("link17", "download_page_4.txt");
+ await testLink("link18", "download_page_4.txt");
+ await testLink("link19", "download_page_4.txt");
+ await testLink("link20", "download_page_4.txt");
+ await testLink("link21", "download_page_4.txt");
+ await testLink("link22", "download_page_4.txt");
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function setDownloadDir() {
+ let tmpDir = PathUtils.join(
+ PathUtils.tempDir,
+ "testsavedir" + Math.floor(Math.random() * 2 ** 32)
+ );
+ // Create this dir if it doesn't exist (ignores existing dirs)
+ await IOUtils.makeDirectory(tmpDir);
+ registerCleanupFunction(async function () {
+ try {
+ await IOUtils.remove(tmpDir, { recursive: true });
+ } catch (e) {
+ console.error(e);
+ }
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+}
+
+add_task(async function () {
+ requestLongerTimeout(3);
+ waitForExplicitFinish();
+
+ await setDownloadDir();
+
+ info(
+ "Test with browser.download.always_ask_before_handling_new_types enabled."
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", true],
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ await runTest(
+ "http://mochi.test:8888/browser/browser/base/content/test/general/download_page.html"
+ );
+ await runTest(
+ "https://example.com:443/browser/browser/base/content/test/general/download_page.html"
+ );
+
+ info(
+ "Test with browser.download.always_ask_before_handling_new_types disabled."
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", false],
+ ],
+ });
+
+ await runTest(
+ "http://mochi.test:8888/browser/browser/base/content/test/general/download_page.html"
+ );
+ await runTest(
+ "https://example.com:443/browser/browser/base/content/test/general/download_page.html"
+ );
+
+ MockFilePicker.cleanup();
+});
diff --git a/browser/base/content/test/general/browser_bug710878.js b/browser/base/content/test/general/browser_bug710878.js
new file mode 100644
index 0000000000..a91f8f9a1e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug710878.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PAGE =
+ "data:text/html;charset=utf-8,<a href='%23xxx'><span>word1 <span> word2 </span></span><span> word3</span></a>";
+
+/**
+ * Tests that we correctly compute the text for context menu
+ * selection of some content.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "a",
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+
+ await awaitPopupShown;
+
+ is(
+ gContextMenu.linkTextStr,
+ "word1 word2 word3",
+ "Text under link is correctly computed."
+ );
+
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+ }
+ );
+});
diff --git a/browser/base/content/test/general/browser_bug724239.js b/browser/base/content/test/general/browser_bug724239.js
new file mode 100644
index 0000000000..78290e21f5
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug724239.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+add_task(async function test_blank() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(browser, "http://example.com");
+ await BrowserTestUtils.browserLoaded(browser);
+ ok(!gBrowser.canGoBack, "about:blank wasn't added to session history");
+ }
+ );
+});
+
+add_task(async function test_newtab() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ // Can't load it directly because that'll use a preloaded tab if present.
+ let stopped = BrowserTestUtils.browserStopped(browser, "about:newtab");
+ BrowserTestUtils.loadURIString(browser, "about:newtab");
+ await stopped;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ stopped = BrowserTestUtils.browserStopped(browser, "http://example.com/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(browser, "http://example.com/");
+ await stopped;
+
+ // This makes sure the parent process has the most up-to-date notion
+ // of the tab's session history.
+ await TabStateFlusher.flush(browser);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ let tabState = JSON.parse(SessionStore.getTabState(tab));
+ Assert.equal(
+ tabState.entries.length,
+ 2,
+ "We should have 2 entries in the session history."
+ );
+
+ Assert.equal(
+ tabState.entries[0].url,
+ "about:newtab",
+ "about:newtab should be the first entry."
+ );
+
+ Assert.ok(gBrowser.canGoBack, "Should be able to browse back.");
+ }
+ );
+});
diff --git a/browser/base/content/test/general/browser_bug734076.js b/browser/base/content/test/general/browser_bug734076.js
new file mode 100644
index 0000000000..9e7bcf5977
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug734076.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ // allow top level data: URI navigations, otherwise loading data: URIs
+ // in toplevel windows fail.
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null, false);
+
+ tab.linkedBrowser.stop(); // stop the about:blank load
+
+ let writeDomainURL = encodeURI(
+ "data:text/html,<script>document.write(document.domain);</script>"
+ );
+
+ let tests = [
+ {
+ name: "view image with background image",
+ url: "http://mochi.test:8888/",
+ element: "body",
+ opensNewTab: true,
+ go() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ writeDomainURL }],
+ async function (arg) {
+ let contentBody = content.document.body;
+ contentBody.style.backgroundImage =
+ "url('" + arg.writeDomainURL + "')";
+
+ return "context-viewimage";
+ }
+ );
+ },
+ verify(browser) {
+ return SpecialPowers.spawn(browser, [], async function (arg) {
+ Assert.equal(
+ content.document.body.textContent,
+ "",
+ "no domain was inherited for view image with background image"
+ );
+ });
+ },
+ },
+ {
+ name: "view image",
+ url: "http://mochi.test:8888/",
+ element: "img",
+ opensNewTab: true,
+ go() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ writeDomainURL }],
+ async function (arg) {
+ let doc = content.document;
+ let img = doc.createElement("img");
+ img.height = 100;
+ img.width = 100;
+ img.setAttribute("src", arg.writeDomainURL);
+ doc.body.insertBefore(img, doc.body.firstElementChild);
+
+ return "context-viewimage";
+ }
+ );
+ },
+ verify(browser) {
+ return SpecialPowers.spawn(browser, [], async function (arg) {
+ Assert.equal(
+ content.document.body.textContent,
+ "",
+ "no domain was inherited for view image"
+ );
+ });
+ },
+ },
+ {
+ name: "show only this frame",
+ url: "http://mochi.test:8888/",
+ element: "html",
+ frameIndex: 0,
+ go() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ writeDomainURL }],
+ async function (arg) {
+ let doc = content.document;
+ let iframe = doc.createElement("iframe");
+ iframe.setAttribute("src", arg.writeDomainURL);
+ doc.body.insertBefore(iframe, doc.body.firstElementChild);
+
+ // Wait for the iframe to load.
+ return new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ function () {
+ resolve("context-showonlythisframe");
+ },
+ { capture: true, once: true }
+ );
+ });
+ }
+ );
+ },
+ verify(browser) {
+ return SpecialPowers.spawn(browser, [], async function (arg) {
+ Assert.equal(
+ content.document.body.textContent,
+ "",
+ "no domain was inherited for 'show only this frame'"
+ );
+ });
+ },
+ },
+ ];
+
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+
+ for (let test of tests) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURIString(gBrowser, test.url);
+ await loadedPromise;
+
+ info("Run subtest " + test.name);
+ let commandToRun = await test.go();
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+
+ let browsingContext = gBrowser.selectedBrowser.browsingContext;
+ if (test.frameIndex != null) {
+ browsingContext = browsingContext.children[test.frameIndex];
+ }
+
+ await new Promise(r => {
+ SimpleTest.executeSoon(r);
+ });
+
+ // Sometimes, the iframe test fails as the child iframe hasn't finishing layout
+ // yet. Try again in this case.
+ while (true) {
+ try {
+ await BrowserTestUtils.synthesizeMouse(
+ test.element,
+ 3,
+ 3,
+ { type: "contextmenu", button: 2 },
+ browsingContext
+ );
+ } catch (ex) {
+ continue;
+ }
+ break;
+ }
+
+ await popupShownPromise;
+ info("onImage: " + gContextMenu.onImage);
+
+ let loadedAfterCommandPromise = test.opensNewTab
+ ? BrowserTestUtils.waitForNewTab(gBrowser, null, true)
+ : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ if (commandToRun == "context-showonlythisframe") {
+ let subMenu = document.getElementById("frame");
+ let subMenuShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
+ subMenu.openMenu(true);
+ await subMenuShown;
+ }
+ contentAreaContextMenu.activateItem(document.getElementById(commandToRun));
+ let result = await loadedAfterCommandPromise;
+
+ await test.verify(
+ test.opensNewTab ? result.linkedBrowser : gBrowser.selectedBrowser
+ );
+
+ await popupHiddenPromise;
+
+ if (test.opensNewTab) {
+ gBrowser.removeCurrentTab();
+ }
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug749738.js b/browser/base/content/test/general/browser_bug749738.js
new file mode 100644
index 0000000000..4430e5d8a7
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug749738.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 DUMMY_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+
+/**
+ * This test checks that if you search for something on one tab, then close
+ * that tab and have the find bar open on the next tab you get switched to,
+ * closing the find bar in that tab works without exceptions.
+ */
+add_task(async function test_bug749738() {
+ // Open find bar on initial tab.
+ await gFindBarPromise;
+
+ await BrowserTestUtils.withNewTab(DUMMY_PAGE, async function () {
+ await gFindBarPromise;
+ gFindBar.onFindCommand();
+ EventUtils.sendString("Dummy");
+ });
+
+ try {
+ gFindBar.close();
+ ok(true, "findbar.close should not throw an exception");
+ } catch (e) {
+ ok(false, "findbar.close threw exception: " + e);
+ }
+});
diff --git a/browser/base/content/test/general/browser_bug763468_perwindowpb.js b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
new file mode 100644
index 0000000000..bed03561ca
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 makes sure that opening a new tab in private browsing mode opens about:privatebrowsing
+add_task(async function testPBNewTab() {
+ registerCleanupFunction(async function () {
+ for (let win of windowsToClose) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+ });
+
+ let windowsToClose = [];
+
+ async function doTest(aIsPrivateMode) {
+ let newTabURL;
+ let mode;
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: aIsPrivateMode,
+ });
+ windowsToClose.push(win);
+
+ if (aIsPrivateMode) {
+ mode = "per window private browsing";
+ newTabURL = "about:privatebrowsing";
+ } else {
+ mode = "normal";
+ newTabURL = "about:newtab";
+ }
+ await openNewTab(win, newTabURL);
+
+ is(
+ win.gBrowser.currentURI.spec,
+ newTabURL,
+ "URL of NewTab should be " + newTabURL + " in " + mode + " mode"
+ );
+ }
+
+ await doTest(false);
+ await doTest(true);
+ await doTest(false);
+});
+
+async function openNewTab(aWindow, aExpectedURL) {
+ // Open a new tab
+ aWindow.BrowserOpenTab();
+ let browser = aWindow.gBrowser.selectedBrowser;
+
+ // We're already loaded.
+ if (browser.currentURI.spec === aExpectedURL) {
+ return;
+ }
+
+ // Wait for any location change.
+ await BrowserTestUtils.waitForLocationChange(aWindow.gBrowser);
+}
diff --git a/browser/base/content/test/general/browser_bug767836_perwindowpb.js b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
new file mode 100644
index 0000000000..7fcc6ad565
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+async function doTest(isPrivate) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: isPrivate });
+ let defaultURL = AboutNewTab.newTabURL;
+ let newTabURL;
+ let mode;
+ let testURL = "https://example.com/";
+ if (isPrivate) {
+ mode = "per window private browsing";
+ newTabURL = "about:privatebrowsing";
+ } else {
+ mode = "normal";
+ newTabURL = "about:newtab";
+ }
+
+ await openNewTab(win, newTabURL);
+ // Check the new tab opened while in normal/private mode
+ is(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ newTabURL,
+ "URL of NewTab should be " + newTabURL + " in " + mode + " mode"
+ );
+
+ // Set the custom newtab url
+ AboutNewTab.newTabURL = testURL;
+ is(AboutNewTab.newTabURL, testURL, "Custom newtab url is set");
+
+ // Open a newtab after setting the custom newtab url
+ await openNewTab(win, testURL);
+ is(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ testURL,
+ "URL of NewTab should be the custom url"
+ );
+
+ // Clear the custom url.
+ AboutNewTab.resetNewTabURL();
+ is(AboutNewTab.newTabURL, defaultURL, "No custom newtab url is set");
+
+ win.gBrowser.removeTab(win.gBrowser.selectedTab);
+ win.gBrowser.removeTab(win.gBrowser.selectedTab);
+ await BrowserTestUtils.closeWindow(win);
+}
+
+add_task(async function test_newTabService() {
+ // check whether any custom new tab url has been configured
+ ok(!AboutNewTab.newTabURLOverridden, "No custom newtab url is set");
+
+ // test normal mode
+ await doTest(false);
+
+ // test private mode
+ await doTest(true);
+});
+
+async function openNewTab(aWindow, aExpectedURL) {
+ // Open a new tab
+ aWindow.BrowserOpenTab();
+ let browser = aWindow.gBrowser.selectedBrowser;
+
+ // We're already loaded.
+ if (browser.currentURI.spec === aExpectedURL) {
+ return;
+ }
+
+ // Wait for any location change.
+ await BrowserTestUtils.waitForLocationChange(aWindow.gBrowser);
+}
diff --git a/browser/base/content/test/general/browser_bug817947.js b/browser/base/content/test/general/browser_bug817947.js
new file mode 100644
index 0000000000..eba54aea5b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug817947.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/browser/";
+const PREF = "browser.sessionstore.restore_on_demand";
+
+add_task(async () => {
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF);
+ });
+
+ let tab = await preparePendingTab();
+
+ let deferredTab = PromiseUtils.defer();
+
+ let win = gBrowser.replaceTabWithWindow(tab);
+ win.addEventListener(
+ "before-initial-tab-adopted",
+ async () => {
+ let [newTab] = win.gBrowser.tabs;
+ await BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+ deferredTab.resolve(newTab);
+ },
+ { once: true }
+ );
+
+ let newTab = await deferredTab.promise;
+ is(newTab.linkedBrowser.currentURI.spec, URL, "correct url should be loaded");
+ ok(!newTab.hasAttribute("pending"), "tab should not be pending");
+
+ win.close();
+});
+
+async function preparePendingTab(aCallback) {
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ let [{ state }] = SessionStore.getClosedTabDataForWindow(window);
+
+ tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ SessionStore.setTabState(tab, JSON.stringify(state));
+ ok(tab.hasAttribute("pending"), "tab should be pending");
+
+ return tab;
+}
diff --git a/browser/base/content/test/general/browser_bug832435.js b/browser/base/content/test/general/browser_bug832435.js
new file mode 100644
index 0000000000..c3140608c5
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug832435.js
@@ -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/. */
+
+function test() {
+ waitForExplicitFinish();
+ ok(true, "Starting up");
+
+ gBrowser.selectedBrowser.focus();
+ gURLBar.addEventListener(
+ "focus",
+ function () {
+ ok(true, "Invoked onfocus handler");
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
+
+ // javscript: URIs are evaluated async.
+ SimpleTest.executeSoon(function () {
+ ok(true, "Evaluated without crashing");
+ finish();
+ });
+ },
+ { once: true }
+ );
+ gURLBar.inputField.value = "javascript: var foo = '11111111'; ";
+ gURLBar.focus();
+}
diff --git a/browser/base/content/test/general/browser_bug882977.js b/browser/base/content/test/general/browser_bug882977.js
new file mode 100644
index 0000000000..116f01b349
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug882977.js
@@ -0,0 +1,33 @@
+"use strict";
+
+/**
+ * Tests that the identity-box shows the chromeUI styling
+ * when viewing such a page in a new window.
+ */
+add_task(async function () {
+ let homepage = "about:preferences";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.startup.homepage", homepage],
+ ["browser.startup.page", 1],
+ ],
+ });
+
+ let win = OpenBrowserWindow();
+ await BrowserTestUtils.firstBrowserLoaded(win, false);
+
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, homepage, "Loaded the correct homepage");
+ checkIdentityMode(win);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+function checkIdentityMode(win) {
+ let identityMode = win.document.getElementById("identity-box").className;
+ is(
+ identityMode,
+ "chromeUI",
+ "Identity state should be chromeUI for about:home in a new window"
+ );
+}
diff --git a/browser/base/content/test/general/browser_bug963945.js b/browser/base/content/test/general/browser_bug963945.js
new file mode 100644
index 0000000000..688d8b79ff
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug963945.js
@@ -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/. */
+
+/*
+ * This test ensures the about:addons tab is only
+ * opened one time when in private browsing.
+ */
+
+add_task(async function test() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ let tab = (win.gBrowser.selectedTab = BrowserTestUtils.addTab(
+ win.gBrowser,
+ "about:addons"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await promiseWaitForFocus(win);
+
+ EventUtils.synthesizeKey("a", { ctrlKey: true, shiftKey: true }, win);
+
+ is(win.gBrowser.tabs.length, 2, "about:addons tab was re-focused.");
+ is(win.gBrowser.currentURI.spec, "about:addons", "Addons tab was opened.");
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/general/browser_clipboard.js b/browser/base/content/test/general/browser_clipboard.js
new file mode 100644
index 0000000000..a4c823969f
--- /dev/null
+++ b/browser/base/content/test/general/browser_clipboard.js
@@ -0,0 +1,290 @@
+// This test is used to check copy and paste in editable areas to ensure that non-text
+// types (html and images) are copied to and pasted from the clipboard properly.
+
+var testPage =
+ "<body style='margin: 0'>" +
+ " <img id='img' tabindex='1' src='http://example.org/browser/browser/base/content/test/general/moz.png'>" +
+ " <div id='main' contenteditable='true'>Test <b>Bold</b> After Text</div>" +
+ "</body>";
+
+add_task(async function () {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ gBrowser.selectedTab = tab;
+
+ await promiseTabLoadEvent(tab, "data:text/html," + escape(testPage));
+ await SimpleTest.promiseFocus(browser);
+
+ function sendKey(key, code) {
+ return BrowserTestUtils.synthesizeKey(
+ key,
+ { code, accelKey: true },
+ browser
+ );
+ }
+
+ // On windows, HTML clipboard includes extra data.
+ // The values are from widget/windows/nsDataObj.cpp.
+ const htmlPrefix = navigator.platform.includes("Win")
+ ? "<html><body>\n<!--StartFragment-->"
+ : "";
+ const htmlPostfix = navigator.platform.includes("Win")
+ ? "<!--EndFragment-->\n</body>\n</html>"
+ : "";
+
+ await SpecialPowers.spawn(browser, [], () => {
+ var doc = content.document;
+ var main = doc.getElementById("main");
+ main.focus();
+
+ // Select an area of the text.
+ let selection = doc.getSelection();
+ selection.modify("move", "left", "line");
+ selection.modify("move", "right", "character");
+ selection.modify("move", "right", "character");
+ selection.modify("move", "right", "character");
+ selection.modify("extend", "right", "word");
+ selection.modify("extend", "right", "word");
+ });
+
+ // The data is empty as the selection was copied during the event default phase.
+ let copyEventPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "copy",
+ false,
+ event => {
+ return event.clipboardData.mozItemCount == 0;
+ }
+ );
+ await SpecialPowers.spawn(browser, [], () => {});
+ await sendKey("c");
+ await copyEventPromise;
+
+ let pastePromise = SpecialPowers.spawn(
+ browser,
+ [htmlPrefix, htmlPostfix],
+ (htmlPrefixChild, htmlPostfixChild) => {
+ let selection = content.document.getSelection();
+ selection.modify("move", "right", "line");
+
+ return new Promise((resolve, reject) => {
+ content.addEventListener(
+ "paste",
+ event => {
+ let clipboardData = event.clipboardData;
+ Assert.equal(
+ clipboardData.mozItemCount,
+ 1,
+ "One item on clipboard"
+ );
+ Assert.equal(
+ clipboardData.types.length,
+ 2,
+ "Two types on clipboard"
+ );
+ Assert.equal(
+ clipboardData.types[0],
+ "text/html",
+ "text/html on clipboard"
+ );
+ Assert.equal(
+ clipboardData.types[1],
+ "text/plain",
+ "text/plain on clipboard"
+ );
+ Assert.equal(
+ clipboardData.getData("text/html"),
+ htmlPrefixChild + "t <b>Bold</b>" + htmlPostfixChild,
+ "text/html value"
+ );
+ Assert.equal(
+ clipboardData.getData("text/plain"),
+ "t Bold",
+ "text/plain value"
+ );
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+ }
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {});
+
+ await sendKey("v");
+ await pastePromise;
+
+ let copyPromise = SpecialPowers.spawn(browser, [], () => {
+ var main = content.document.getElementById("main");
+
+ Assert.equal(
+ main.innerHTML,
+ "Test <b>Bold</b> After Textt <b>Bold</b>",
+ "Copy and paste html"
+ );
+
+ let selection = content.document.getSelection();
+ selection.modify("extend", "left", "word");
+ selection.modify("extend", "left", "word");
+ selection.modify("extend", "left", "character");
+
+ return new Promise((resolve, reject) => {
+ content.addEventListener(
+ "cut",
+ event => {
+ event.clipboardData.setData("text/plain", "Some text");
+ event.clipboardData.setData("text/html", "<i>Italic</i> ");
+ selection.deleteFromDocument();
+ event.preventDefault();
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+ });
+
+ await SpecialPowers.spawn(browser, [], () => {});
+
+ await sendKey("x");
+ await copyPromise;
+
+ pastePromise = SpecialPowers.spawn(
+ browser,
+ [htmlPrefix, htmlPostfix],
+ (htmlPrefixChild, htmlPostfixChild) => {
+ let selection = content.document.getSelection();
+ selection.modify("move", "left", "line");
+
+ return new Promise((resolve, reject) => {
+ content.addEventListener(
+ "paste",
+ event => {
+ let clipboardData = event.clipboardData;
+ Assert.equal(
+ clipboardData.mozItemCount,
+ 1,
+ "One item on clipboard 2"
+ );
+ Assert.equal(
+ clipboardData.types.length,
+ 2,
+ "Two types on clipboard 2"
+ );
+ Assert.equal(
+ clipboardData.types[0],
+ "text/html",
+ "text/html on clipboard 2"
+ );
+ Assert.equal(
+ clipboardData.types[1],
+ "text/plain",
+ "text/plain on clipboard 2"
+ );
+ Assert.equal(
+ clipboardData.getData("text/html"),
+ htmlPrefixChild + "<i>Italic</i> " + htmlPostfixChild,
+ "text/html value 2"
+ );
+ Assert.equal(
+ clipboardData.getData("text/plain"),
+ "Some text",
+ "text/plain value 2"
+ );
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+ }
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {});
+
+ await sendKey("v");
+ await pastePromise;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ var main = content.document.getElementById("main");
+ Assert.equal(
+ main.innerHTML,
+ "<i>Italic</i> Test <b>Bold</b> After<b></b>",
+ "Copy and paste html 2"
+ );
+ });
+
+ // Next, check that the Copy Image command works.
+
+ // The context menu needs to be opened to properly initialize for the copy
+ // image command to run.
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let contextMenuShown = promisePopupShown(contextMenu);
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#img",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await contextMenuShown;
+
+ document.getElementById("context-copyimage-contents").doCommand();
+
+ contextMenu.hidePopup();
+ await promisePopupHidden(contextMenu);
+
+ // Focus the content again
+ await SimpleTest.promiseFocus(browser);
+
+ pastePromise = SpecialPowers.spawn(
+ browser,
+ [htmlPrefix, htmlPostfix],
+ (htmlPrefixChild, htmlPostfixChild) => {
+ var doc = content.document;
+ var main = doc.getElementById("main");
+ main.focus();
+
+ return new Promise((resolve, reject) => {
+ content.addEventListener(
+ "paste",
+ event => {
+ let clipboardData = event.clipboardData;
+
+ // DataTransfer doesn't support the image types yet, so only text/html
+ // will be present.
+ if (
+ clipboardData.getData("text/html") !==
+ htmlPrefixChild +
+ '<img id="img" tabindex="1" src="http://example.org/browser/browser/base/content/test/general/moz.png">' +
+ htmlPostfixChild
+ ) {
+ reject(
+ "Clipboard Data did not contain an image, was " +
+ clipboardData.getData("text/html")
+ );
+ }
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+ }
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {});
+ await sendKey("v");
+ await pastePromise;
+
+ // The new content should now include an image.
+ await SpecialPowers.spawn(browser, [], () => {
+ var main = content.document.getElementById("main");
+ Assert.equal(
+ main.innerHTML,
+ '<i>Italic</i> <img id="img" tabindex="1" ' +
+ 'src="http://example.org/browser/browser/base/content/test/general/moz.png">' +
+ "Test <b>Bold</b> After<b></b>",
+ "Paste after copy image"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_clipboard_pastefile.js b/browser/base/content/test/general/browser_clipboard_pastefile.js
new file mode 100644
index 0000000000..f034883ef2
--- /dev/null
+++ b/browser/base/content/test/general/browser_clipboard_pastefile.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/. */
+
+// Test that (real) files can be pasted into chrome/content.
+// Pasting files should also hide all other data from content.
+
+function setClipboard(path) {
+ const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(path);
+
+ const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor("application/x-moz-file");
+ trans.setTransferData("application/x-moz-file", file);
+
+ trans.addDataFlavor("text/plain");
+ const str = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ str.data = "Alternate";
+ trans.setTransferData("text/plain", str);
+
+ // Write to clipboard.
+ Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.events.dataTransfer.mozFile.enabled", true]],
+ });
+
+ // Create a temporary file that will be pasted.
+ const file = await IOUtils.createUniqueFile(
+ PathUtils.tempDir,
+ "test-file.txt",
+ 0o600
+ );
+ await IOUtils.writeUTF8(file, "Hello World!");
+
+ // Put the data directly onto the native clipboard to make sure
+ // it isn't handled internally in Gecko somehow.
+ setClipboard(file);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/browser/browser/base/content/test/general/clipboard_pastefile.html"
+ );
+ let browser = tab.linkedBrowser;
+
+ let resultPromise = SpecialPowers.spawn(browser, [], function (arg) {
+ return new Promise(resolve => {
+ content.document.addEventListener("testresult", event => {
+ resolve(event.detail.result);
+ });
+ });
+ });
+
+ // Focus <input> in content
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.getElementById("input").focus();
+ });
+
+ // Paste file into <input> in content
+ await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
+
+ let result = await resultPromise;
+ is(result, PathUtils.filename(file), "Correctly pasted file in content");
+
+ var input = document.createElement("input");
+ document.documentElement.appendChild(input);
+ input.focus();
+
+ await new Promise((resolve, reject) => {
+ input.addEventListener(
+ "paste",
+ function (event) {
+ let dt = event.clipboardData;
+ is(dt.types.length, 3, "number of types");
+ ok(dt.types.includes("text/plain"), "text/plain exists in types");
+ ok(
+ dt.types.includes("application/x-moz-file"),
+ "application/x-moz-file exists in types"
+ );
+ is(dt.types[2], "Files", "Last type should be 'Files'");
+ ok(
+ dt.mozTypesAt(0).contains("text/plain"),
+ "text/plain exists in mozTypesAt"
+ );
+ is(
+ dt.getData("text/plain"),
+ "Alternate",
+ "text/plain returned in getData"
+ );
+ is(
+ dt.mozGetDataAt("text/plain", 0),
+ "Alternate",
+ "text/plain returned in mozGetDataAt"
+ );
+
+ ok(
+ dt.mozTypesAt(0).contains("application/x-moz-file"),
+ "application/x-moz-file exists in mozTypesAt"
+ );
+ let mozFile = dt.mozGetDataAt("application/x-moz-file", 0);
+
+ ok(
+ mozFile instanceof Ci.nsIFile,
+ "application/x-moz-file returned nsIFile with mozGetDataAt"
+ );
+
+ is(
+ mozFile.leafName,
+ PathUtils.filename(file),
+ "nsIFile has correct leafName"
+ );
+
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+
+ EventUtils.synthesizeKey("v", { accelKey: true });
+ });
+
+ input.remove();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await IOUtils.remove(file);
+});
diff --git a/browser/base/content/test/general/browser_contentAltClick.js b/browser/base/content/test/general/browser_contentAltClick.js
new file mode 100644
index 0000000000..5f659d3351
--- /dev/null
+++ b/browser/base/content/test/general/browser_contentAltClick.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 1109146.
+ * The tests opens a new tab and alt + clicks to download files
+ * and confirms those files are on the download list.
+ *
+ * The difference between this and the test "browser_contentAreaClick.js" is that
+ * the code path in e10s uses the ClickHandler actor instead of browser.js::contentAreaClick() util.
+ */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+});
+
+function setup() {
+ Services.prefs.setBoolPref("browser.altClickSave", true);
+
+ let testPage =
+ "data:text/html," +
+ '<p><a id="commonlink" href="http://mochi.test/moz/">Common link</a></p>' +
+ '<p><math id="mathlink" xmlns="http://www.w3.org/1998/Math/MathML" href="http://mochi.test/moz/"><mtext>MathML XLink</mtext></math></p>' +
+ '<p><svg id="svgxlink" xmlns="http://www.w3.org/2000/svg" width="100px" height="50px" version="1.1"><a xlink:type="simple" xlink:href="http://mochi.test/moz/"><text transform="translate(10, 25)">SVG XLink</text></a></svg></p><br>' +
+ '<span id="host"></span><script>document.getElementById("host").attachShadow({mode: "closed"}).appendChild(document.getElementById("commonlink").cloneNode(true));</script>' +
+ '<iframe id="frame" src="https://test2.example.com:443/browser/browser/base/content/test/general/file_with_link_to_http.html"></iframe>';
+
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, testPage);
+}
+
+async function clean_up() {
+ // Remove downloads.
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloads = await downloadList.getAll();
+ for (let download of downloads) {
+ await downloadList.remove(download);
+ await download.finalize(true);
+ }
+ // Remove download history.
+ await PlacesUtils.history.clear();
+
+ Services.prefs.clearUserPref("browser.altClickSave");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_task(async function test_alt_click() {
+ await setup();
+
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloads = [];
+ let downloadView;
+ // When 1 download has been attempted then resolve the promise.
+ let finishedAllDownloads = new Promise(resolve => {
+ downloadView = {
+ onDownloadAdded(aDownload) {
+ downloads.push(aDownload);
+ resolve();
+ },
+ };
+ });
+ await downloadList.addView(downloadView);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#commonlink",
+ { altKey: true },
+ gBrowser.selectedBrowser
+ );
+
+ // Wait for all downloads to be added to the download list.
+ await finishedAllDownloads;
+ await downloadList.removeView(downloadView);
+
+ is(downloads.length, 1, "1 downloads");
+ is(
+ downloads[0].source.url,
+ "http://mochi.test/moz/",
+ "Downloaded #commonlink element"
+ );
+
+ await clean_up();
+});
+
+add_task(async function test_alt_click_shadow_dom() {
+ await setup();
+
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloads = [];
+ let downloadView;
+ // When 1 download has been attempted then resolve the promise.
+ let finishedAllDownloads = new Promise(resolve => {
+ downloadView = {
+ onDownloadAdded(aDownload) {
+ downloads.push(aDownload);
+ resolve();
+ },
+ };
+ });
+ await downloadList.addView(downloadView);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#host",
+ { altKey: true },
+ gBrowser.selectedBrowser
+ );
+
+ // Wait for all downloads to be added to the download list.
+ await finishedAllDownloads;
+ await downloadList.removeView(downloadView);
+
+ is(downloads.length, 1, "1 downloads");
+ is(
+ downloads[0].source.url,
+ "http://mochi.test/moz/",
+ "Downloaded #commonlink element in shadow DOM."
+ );
+
+ await clean_up();
+});
+
+add_task(async function test_alt_click_on_xlinks() {
+ await setup();
+
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloads = [];
+ let downloadView;
+ // When all 2 downloads have been attempted then resolve the promise.
+ let finishedAllDownloads = new Promise(resolve => {
+ downloadView = {
+ onDownloadAdded(aDownload) {
+ downloads.push(aDownload);
+ if (downloads.length == 2) {
+ resolve();
+ }
+ },
+ };
+ });
+ await downloadList.addView(downloadView);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#mathlink",
+ { altKey: true },
+ gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#svgxlink",
+ { altKey: true },
+ gBrowser.selectedBrowser
+ );
+
+ // Wait for all downloads to be added to the download list.
+ await finishedAllDownloads;
+ await downloadList.removeView(downloadView);
+
+ is(downloads.length, 2, "2 downloads");
+ is(
+ downloads[0].source.url,
+ "http://mochi.test/moz/",
+ "Downloaded #mathlink element"
+ );
+ is(
+ downloads[1].source.url,
+ "http://mochi.test/moz/",
+ "Downloaded #svgxlink element"
+ );
+
+ await clean_up();
+});
+
+// Alt+Click a link in a frame from another domain as the outer document.
+add_task(async function test_alt_click_in_frame() {
+ await setup();
+
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloads = [];
+ let downloadView;
+ // When the download has been attempted, resolve the promise.
+ let finishedAllDownloads = new Promise(resolve => {
+ downloadView = {
+ onDownloadAdded(aDownload) {
+ downloads.push(aDownload);
+ resolve();
+ },
+ };
+ });
+
+ await downloadList.addView(downloadView);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#linkToExample",
+ { altKey: true },
+ gBrowser.selectedBrowser.browsingContext.children[0]
+ );
+
+ // Wait for all downloads to be added to the download list.
+ await finishedAllDownloads;
+ await downloadList.removeView(downloadView);
+
+ is(downloads.length, 1, "1 downloads");
+ is(
+ downloads[0].source.url,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/",
+ "Downloaded link in iframe."
+ );
+
+ await clean_up();
+});
diff --git a/browser/base/content/test/general/browser_ctrlTab.js b/browser/base/content/test/general/browser_ctrlTab.js
new file mode 100644
index 0000000000..7c4a7b6c23
--- /dev/null
+++ b/browser/base/content/test/general/browser_ctrlTab.js
@@ -0,0 +1,464 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.ctrlTab.sortByRecentlyUsed", true]],
+ });
+
+ BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.addTab(gBrowser);
+
+ // While doing this test, we should make sure the selected tab in the tab
+ // preview is not changed by mouse events. That may happen after closing
+ // the selected tab with ctrl+W. Disable all mouse events to prevent it.
+ for (let node of ctrlTab.previews) {
+ node.style.pointerEvents = "none";
+ }
+ registerCleanupFunction(function () {
+ for (let node of ctrlTab.previews) {
+ try {
+ node.style.removeProperty("pointer-events");
+ } catch (e) {}
+ }
+ });
+
+ checkTabs(4);
+
+ await ctrlTabTest([2], 1, 0);
+ await ctrlTabTest([2, 3, 1], 2, 2);
+ await ctrlTabTest([], 4, 2);
+
+ {
+ let selectedIndex = gBrowser.tabContainer.selectedIndex;
+ await pressCtrlTab();
+ await pressCtrlTab(true);
+ await releaseCtrl();
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ selectedIndex,
+ "Ctrl+Tab -> Ctrl+Shift+Tab keeps the selected tab"
+ );
+ }
+
+ {
+ info("test for bug 445369");
+ let tabs = gBrowser.tabs.length;
+ await pressCtrlTab();
+ await synthesizeCtrlW();
+ is(gBrowser.tabs.length, tabs - 1, "Ctrl+Tab -> Ctrl+W removes one tab");
+ await releaseCtrl();
+ }
+
+ {
+ info("test for bug 667314");
+ let tabs = gBrowser.tabs.length;
+ await pressCtrlTab();
+ await pressCtrlTab(true);
+ await synthesizeCtrlW();
+ is(
+ gBrowser.tabs.length,
+ tabs - 1,
+ "Ctrl+Tab -> Ctrl+W removes the selected tab"
+ );
+ await releaseCtrl();
+ }
+
+ BrowserTestUtils.addTab(gBrowser);
+ checkTabs(3);
+ await ctrlTabTest([2, 1, 0], 7, 1);
+
+ {
+ info("test for bug 1292049");
+ let tabToClose = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:buildconfig"
+ );
+ checkTabs(4);
+ selectTabs([0, 1, 2, 3]);
+
+ let promise = BrowserTestUtils.waitForSessionStoreUpdate(tabToClose);
+ BrowserTestUtils.removeTab(tabToClose);
+ await promise;
+ checkTabs(3);
+ undoCloseTab();
+ checkTabs(4);
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ 3,
+ "tab is selected after closing and restoring it"
+ );
+
+ await ctrlTabTest([], 1, 2);
+ }
+
+ {
+ info("test for bug 445369");
+ checkTabs(4);
+ selectTabs([1, 2, 0]);
+
+ let selectedTab = gBrowser.selectedTab;
+ let tabToRemove = gBrowser.tabs[1];
+
+ await pressCtrlTab();
+ await pressCtrlTab();
+ await synthesizeCtrlW();
+ ok(
+ !tabToRemove.parentNode,
+ "Ctrl+Tab*2 -> Ctrl+W removes the second most recently selected tab"
+ );
+
+ await pressCtrlTab(true);
+ await pressCtrlTab(true);
+ await releaseCtrl();
+ ok(
+ selectedTab.selected,
+ "Ctrl+Tab*2 -> Ctrl+W -> Ctrl+Shift+Tab*2 keeps the selected tab"
+ );
+ }
+ gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ checkTabs(2);
+
+ await ctrlTabTest([1], 1, 0);
+
+ gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ checkTabs(1);
+
+ {
+ info("test for bug 445768");
+ let focusedWindow = document.commandDispatcher.focusedWindow;
+ let eventConsumed = true;
+ let detectKeyEvent = function (event) {
+ eventConsumed = event.defaultPrevented;
+ };
+ document.addEventListener("keypress", detectKeyEvent);
+ await pressCtrlTab();
+ document.removeEventListener("keypress", detectKeyEvent);
+ ok(
+ eventConsumed,
+ "Ctrl+Tab consumed by the tabbed browser if one tab is open"
+ );
+ is(
+ focusedWindow,
+ document.commandDispatcher.focusedWindow,
+ "Ctrl+Tab doesn't change focus if one tab is open"
+ );
+ }
+
+ // eslint-disable-next-line no-lone-blocks
+ {
+ info("Bug 1731050: test hidden tabs");
+ checkTabs(1);
+ await BrowserTestUtils.addTab(gBrowser);
+ await BrowserTestUtils.addTab(gBrowser);
+ await BrowserTestUtils.addTab(gBrowser);
+ await BrowserTestUtils.addTab(gBrowser);
+ FirefoxViewHandler.tab = await BrowserTestUtils.addTab(gBrowser);
+
+ gBrowser.hideTab(FirefoxViewHandler.tab);
+ FirefoxViewHandler.openTab();
+ selectTabs([1, 2, 3, 4, 3]);
+ gBrowser.hideTab(gBrowser.tabs[4]);
+ selectTabs([2]);
+ gBrowser.hideTab(gBrowser.tabs[3]);
+
+ is(gBrowser.tabs[5].hidden, true, "Tab at index 5 is hidden");
+ is(gBrowser.tabs[4].hidden, true, "Tab at index 4 is hidden");
+ is(gBrowser.tabs[3].hidden, true, "Tab at index 3 is hidden");
+ is(gBrowser.tabs[2].hidden, false, "Tab at index 2 is still shown");
+ is(gBrowser.tabs[1].hidden, false, "Tab at index 1 is still shown");
+ is(gBrowser.tabs[0].hidden, false, "Tab at index 0 is still shown");
+
+ await ctrlTabTest([], 1, 1);
+ await ctrlTabTest([], 2, 0);
+ gBrowser.showTab(gBrowser.tabs[4]);
+ await ctrlTabTest([2], 3, 4);
+ await ctrlTabTest([], 4, 4);
+ gBrowser.showTab(gBrowser.tabs[3]);
+ await ctrlTabTest([], 4, 3);
+ await ctrlTabTest([], 6, 4);
+ FirefoxViewHandler.openTab();
+ // Fx View tab should be visible in the panel while selected.
+ await ctrlTabTest([], 5, 1);
+ // Fx View tab should no longer be visible.
+ await ctrlTabTest([], 1, 4);
+
+ for (let i = 5; i > 0; i--) {
+ await BrowserTestUtils.removeTab(gBrowser.tabs[i]);
+ }
+ FirefoxViewHandler.tab = null;
+ info("End hidden tabs test");
+ }
+
+ {
+ info("Bug 1293692: Test asynchronous tab previews");
+
+ checkTabs(1);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.pagethumbnails.capturing_disabled", false]],
+ });
+
+ await BrowserTestUtils.addTab(gBrowser);
+ await BrowserTestUtils.addTab(gBrowser);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `${getRootDirectory(gTestPath)}dummy_page.html`
+ );
+
+ info("Pressing Ctrl+Tab to open the panel");
+ ok(canOpen(), "Ctrl+Tab can open the preview panel");
+ let panelShown = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
+ await TestUtils.waitForTick();
+
+ let observedPreview = ctrlTab.previews[0];
+ is(observedPreview._tab, tab, "The observed preview is for the new tab");
+ ok(
+ !observedPreview._canvas.firstElementChild,
+ "The preview <canvas> does not exist yet"
+ );
+
+ let emptyCanvas = PageThumbs.createCanvas(window);
+ let emptyImageData = emptyCanvas
+ .getContext("2d")
+ .getImageData(0, 0, ctrlTab.canvasWidth, ctrlTab.canvasHeight)
+ .data.slice(0, 15)
+ .toString();
+
+ info("Waiting for the preview <canvas> to be loaded");
+ await BrowserTestUtils.waitForMutationCondition(
+ observedPreview._canvas,
+ {
+ childList: true,
+ attributes: true,
+ subtree: true,
+ },
+ () =>
+ HTMLCanvasElement.isInstance(observedPreview._canvas.firstElementChild)
+ );
+
+ // Ensure the image is not blank (see bug 1293692). The canvas shouldn't be
+ // rendered at all until it has image data, but this will allow us to catch
+ // any regressions in the future.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ emptyImageData !==
+ observedPreview._canvas.firstElementChild
+ .getContext("2d")
+ .getImageData(0, 0, ctrlTab.canvasWidth, ctrlTab.canvasHeight)
+ .data.slice(0, 15)
+ .toString(),
+ "The preview <canvas> should be filled with a thumbnail"
+ );
+
+ // Wait for the panel to be shown.
+ await panelShown;
+ ok(isOpen(), "The preview panel is open");
+
+ // Keep the same tab selected.
+ await pressCtrlTab(true);
+ await releaseCtrl();
+
+ // The next time the panel is open, our preview should now be an <img>, the
+ // thumbnail that was previously drawn in a <canvas> having been cached and
+ // now being immediately available for reuse.
+ info("Pressing Ctrl+Tab to open the panel again");
+ let imgExists = BrowserTestUtils.waitForMutationCondition(
+ observedPreview._canvas,
+ {
+ childList: true,
+ attributes: true,
+ subtree: true,
+ },
+ () => {
+ let img = observedPreview._canvas.firstElementChild;
+ return (
+ img &&
+ HTMLImageElement.isInstance(img) &&
+ img.src &&
+ img.complete &&
+ img.naturalWidth
+ );
+ }
+ );
+ let panelShownAgain = BrowserTestUtils.waitForEvent(
+ ctrlTab.panel,
+ "popupshown"
+ );
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
+
+ info("Waiting for the preview <img> to be loaded");
+ await imgExists;
+ ok(
+ true,
+ `The preview image is an <img> with src="${observedPreview._canvas.firstElementChild.src}"`
+ );
+ await panelShownAgain;
+ await releaseCtrl();
+
+ for (let i = gBrowser.tabs.length - 1; i > 0; i--) {
+ await BrowserTestUtils.removeTab(gBrowser.tabs[i]);
+ }
+ checkTabs(1);
+ }
+
+ /* private utility functions */
+
+ /**
+ * @return the number of times (Shift+)Ctrl+Tab was pressed
+ */
+ async function pressCtrlTab(aShiftKey = false) {
+ let promise;
+ if (!isOpen() && canOpen()) {
+ ok(
+ !aShiftKey,
+ "Shouldn't attempt to open the panel by pressing Shift+Ctrl+Tab"
+ );
+ info("Pressing Ctrl+Tab to open the panel");
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
+ } else {
+ info(
+ `Pressing ${aShiftKey ? "Shift+" : ""}Ctrl+Tab while the panel is open`
+ );
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_TAB", {
+ ctrlKey: true,
+ shiftKey: !!aShiftKey,
+ });
+ await promise;
+ if (document.activeElement == ctrlTab.showAllButton) {
+ info("Repeating keypress to skip over the 'List all tabs' button");
+ return 1 + (await pressCtrlTab(aShiftKey));
+ }
+ return 1;
+ }
+
+ async function releaseCtrl() {
+ let promise;
+ if (isOpen()) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popuphidden");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+ await promise;
+ }
+
+ async function synthesizeCtrlW() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose"
+ );
+ EventUtils.synthesizeKey("w", { ctrlKey: true });
+ await promise;
+ }
+
+ function isOpen() {
+ return ctrlTab.isOpen;
+ }
+
+ function canOpen() {
+ return (
+ Services.prefs.getBoolPref("browser.ctrlTab.sortByRecentlyUsed") &&
+ gBrowser.tabs.length > 2
+ );
+ }
+
+ function checkTabs(aTabs) {
+ is(gBrowser.tabs.length, aTabs, "number of open tabs should be " + aTabs);
+ }
+
+ function selectTabs(tabs) {
+ tabs.forEach(function (index) {
+ gBrowser.selectedTab = gBrowser.tabs[index];
+ });
+ }
+
+ async function ctrlTabTest(tabsToSelect, tabTimes, expectedIndex) {
+ selectTabs(tabsToSelect);
+
+ var indexStart = gBrowser.tabContainer.selectedIndex;
+ var tabCount = gBrowser.visibleTabs.length;
+ var normalized = tabTimes % tabCount;
+ var where =
+ normalized == 1
+ ? "back to the previously selected tab"
+ : normalized + " tabs back in most-recently-selected order";
+
+ // Add keyup listener to all content documents.
+ await Promise.all(
+ gBrowser.tabs.map(tab =>
+ SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ if (!content.windowGlobalChild?.isInProcess) {
+ content.window.addEventListener("keyup", () => {
+ content.window._ctrlTabTestKeyupHappend = true;
+ });
+ }
+ })
+ )
+ );
+
+ let numTimesPressed = 0;
+ for (let i = 0; i < tabTimes; i++) {
+ numTimesPressed += await pressCtrlTab();
+
+ if (tabCount > 2) {
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ indexStart,
+ "Selected tab doesn't change while tabbing"
+ );
+ }
+ }
+
+ if (tabCount > 2) {
+ ok(
+ isOpen(),
+ "With " + tabCount + " visible tabs, Ctrl+Tab opens the preview panel"
+ );
+
+ await releaseCtrl();
+
+ ok(!isOpen(), "Releasing Ctrl closes the preview panel");
+ } else {
+ ok(
+ !isOpen(),
+ "With " +
+ tabCount +
+ " visible tabs, Ctrl+Tab doesn't open the preview panel"
+ );
+ }
+
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ expectedIndex,
+ "With " +
+ tabCount +
+ " visible tabs and tab " +
+ indexStart +
+ " selected, Ctrl+Tab*" +
+ numTimesPressed +
+ " goes " +
+ where
+ );
+
+ const keyupEvents = await Promise.all(
+ gBrowser.tabs.map(tab =>
+ SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => !!content.window._ctrlTabTestKeyupHappend
+ )
+ )
+ );
+ ok(
+ keyupEvents.every(isKeyupHappned => !isKeyupHappned),
+ "Content document doesn't capture Keyup event during cycling tabs"
+ );
+ }
+});
diff --git a/browser/base/content/test/general/browser_datachoices_notification.js b/browser/base/content/test/general/browser_datachoices_notification.js
new file mode 100644
index 0000000000..eb2ec5ee37
--- /dev/null
+++ b/browser/base/content/test/general/browser_datachoices_notification.js
@@ -0,0 +1,287 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+var { TelemetryReportingPolicy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+);
+
+const PREF_BRANCH = "datareporting.policy.";
+const PREF_FIRST_RUN = "toolkit.telemetry.reportingpolicy.firstRun";
+const PREF_BYPASS_NOTIFICATION =
+ PREF_BRANCH + "dataSubmissionPolicyBypassNotification";
+const PREF_CURRENT_POLICY_VERSION = PREF_BRANCH + "currentPolicyVersion";
+const PREF_ACCEPTED_POLICY_VERSION =
+ PREF_BRANCH + "dataSubmissionPolicyAcceptedVersion";
+const PREF_ACCEPTED_POLICY_DATE =
+ PREF_BRANCH + "dataSubmissionPolicyNotifiedTime";
+
+const PREF_TELEMETRY_LOG_LEVEL = "toolkit.telemetry.log.level";
+
+const TEST_POLICY_VERSION = 37;
+
+function fakeShowPolicyTimeout(set, clear) {
+ let reportingPolicy = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+ ).Policy;
+ reportingPolicy.setShowInfobarTimeout = set;
+ reportingPolicy.clearShowInfobarTimeout = clear;
+}
+
+function sendSessionRestoredNotification() {
+ let reportingPolicy = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+ ).Policy;
+
+ reportingPolicy.fakeSessionRestoreNotification();
+}
+
+/**
+ * Wait for a tick.
+ */
+function promiseNextTick() {
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+/**
+ * Wait for a notification to be shown in a notification box.
+ * @param {Object} aNotificationBox The notification box.
+ * @return {Promise} Resolved when the notification is displayed.
+ */
+function promiseWaitForAlertActive(aNotificationBox) {
+ let deferred = PromiseUtils.defer();
+ aNotificationBox.stack.addEventListener(
+ "AlertActive",
+ function () {
+ deferred.resolve();
+ },
+ { once: true }
+ );
+ return deferred.promise;
+}
+
+/**
+ * Wait for a notification to be closed.
+ * @param {Object} aNotification The notification.
+ * @return {Promise} Resolved when the notification is closed.
+ */
+function promiseWaitForNotificationClose(aNotification) {
+ let deferred = PromiseUtils.defer();
+ waitForNotificationClose(aNotification, deferred.resolve);
+ return deferred.promise;
+}
+
+function triggerInfoBar(expectedTimeoutMs) {
+ let showInfobarCallback = null;
+ let timeoutMs = null;
+ fakeShowPolicyTimeout(
+ (callback, timeout) => {
+ showInfobarCallback = callback;
+ timeoutMs = timeout;
+ },
+ () => {}
+ );
+ sendSessionRestoredNotification();
+ Assert.ok(!!showInfobarCallback, "Must have a timer callback.");
+ if (expectedTimeoutMs !== undefined) {
+ Assert.equal(timeoutMs, expectedTimeoutMs, "Timeout should match");
+ }
+ showInfobarCallback();
+}
+
+var checkInfobarButton = async function (aNotification) {
+ // Check that the button on the data choices infobar does the right thing.
+ let buttons = aNotification.buttonContainer.getElementsByTagName("button");
+ Assert.equal(
+ buttons.length,
+ 1,
+ "There is 1 button in the data reporting notification."
+ );
+ let button = buttons[0];
+
+ // Click on the button.
+ button.click();
+
+ // Wait for the preferences panel to open.
+ await promiseNextTick();
+};
+
+add_setup(async function () {
+ const isFirstRun = Preferences.get(PREF_FIRST_RUN, true);
+ const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, true);
+ const currentPolicyVersion = Preferences.get(PREF_CURRENT_POLICY_VERSION, 1);
+
+ // Register a cleanup function to reset our preferences.
+ registerCleanupFunction(() => {
+ Preferences.set(PREF_FIRST_RUN, isFirstRun);
+ Preferences.set(PREF_BYPASS_NOTIFICATION, bypassNotification);
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, currentPolicyVersion);
+ Preferences.reset(PREF_TELEMETRY_LOG_LEVEL);
+
+ return closeAllNotifications();
+ });
+
+ // Don't skip the infobar visualisation.
+ Preferences.set(PREF_BYPASS_NOTIFICATION, false);
+ // Set the current policy version.
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, TEST_POLICY_VERSION);
+ // Ensure this isn't the first run, because then we open the first run page.
+ Preferences.set(PREF_FIRST_RUN, false);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+});
+
+function clearAcceptedPolicy() {
+ // Reset the accepted policy.
+ Preferences.reset(PREF_ACCEPTED_POLICY_VERSION);
+ Preferences.reset(PREF_ACCEPTED_POLICY_DATE);
+}
+
+function assertCoherentInitialState() {
+ // Make sure that we have a coherent initial state.
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0),
+ 0,
+ "No version should be set on init."
+ );
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_DATE, 0),
+ 0,
+ "No date should be set on init."
+ );
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "User not notified about datareporting policy."
+ );
+}
+
+add_task(async function test_single_window() {
+ clearAcceptedPolicy();
+
+ // Close all the notifications, then try to trigger the data choices infobar.
+ await closeAllNotifications();
+
+ assertCoherentInitialState();
+
+ let alertShownPromise = promiseWaitForAlertActive(gNotificationBox);
+ Assert.ok(
+ !TelemetryReportingPolicy.canUpload(),
+ "User should not be allowed to upload."
+ );
+
+ // Wait for the infobar to be displayed.
+ triggerInfoBar(10 * 1000);
+ await alertShownPromise;
+
+ Assert.equal(
+ gNotificationBox.allNotifications.length,
+ 1,
+ "Notification Displayed."
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "User should be allowed to upload now."
+ );
+
+ await promiseNextTick();
+ let promiseClosed = promiseWaitForNotificationClose(
+ gNotificationBox.currentNotification
+ );
+ await checkInfobarButton(gNotificationBox.currentNotification);
+ await promiseClosed;
+
+ Assert.equal(
+ gNotificationBox.allNotifications.length,
+ 0,
+ "No notifications remain."
+ );
+
+ // Check that we are still clear to upload and that the policy data is saved.
+ Assert.ok(TelemetryReportingPolicy.canUpload());
+ Assert.equal(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ true,
+ "User notified about datareporting policy."
+ );
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0),
+ TEST_POLICY_VERSION,
+ "Version pref set."
+ );
+ Assert.greater(
+ parseInt(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 10),
+ -1,
+ "Date pref set."
+ );
+});
+
+/* See bug 1571932
+add_task(async function test_multiple_windows() {
+ clearAcceptedPolicy();
+
+ // Close all the notifications, then try to trigger the data choices infobar.
+ await closeAllNotifications();
+
+ // Ensure we see the notification on all windows and that action on one window
+ // results in dismiss on every window.
+ let otherWindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ Assert.ok(
+ otherWindow.gNotificationBox,
+ "2nd window has a global notification box."
+ );
+
+ assertCoherentInitialState();
+
+ let showAlertPromises = [
+ promiseWaitForAlertActive(gNotificationBox),
+ promiseWaitForAlertActive(otherWindow.gNotificationBox),
+ ];
+
+ Assert.ok(
+ !TelemetryReportingPolicy.canUpload(),
+ "User should not be allowed to upload."
+ );
+
+ // Wait for the infobars.
+ triggerInfoBar(10 * 1000);
+ await Promise.all(showAlertPromises);
+
+ // Both notification were displayed. Close one and check that both gets closed.
+ let closeAlertPromises = [
+ promiseWaitForNotificationClose(gNotificationBox.currentNotification),
+ promiseWaitForNotificationClose(
+ otherWindow.gNotificationBox.currentNotification
+ ),
+ ];
+ gNotificationBox.currentNotification.close();
+ await Promise.all(closeAlertPromises);
+
+ // Close the second window we opened.
+ await BrowserTestUtils.closeWindow(otherWindow);
+
+ // Check that we are clear to upload and that the policy data us saved.
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "User should be allowed to upload now."
+ );
+ Assert.equal(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ true,
+ "User notified about datareporting policy."
+ );
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0),
+ TEST_POLICY_VERSION,
+ "Version pref set."
+ );
+ Assert.greater(
+ parseInt(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 10),
+ -1,
+ "Date pref set."
+ );
+});*/
diff --git a/browser/base/content/test/general/browser_documentnavigation.js b/browser/base/content/test/general/browser_documentnavigation.js
new file mode 100644
index 0000000000..8a4fd2ca6b
--- /dev/null
+++ b/browser/base/content/test/general/browser_documentnavigation.js
@@ -0,0 +1,493 @@
+/*
+ * This test checks that focus is adjusted properly in a browser when pressing F6 and Shift+F6.
+ * There are additional tests in dom/tests/mochitest/chrome/test_focus_docnav.xul which test
+ * non-browser cases.
+ */
+
+var testPage1 =
+ "data:text/html,<html id='html1'><body id='body1'><button id='button1'>Tab 1</button></body></html>";
+var testPage2 =
+ "data:text/html,<html id='html2'><body id='body2'><button id='button2'>Tab 2</button></body></html>";
+var testPage3 =
+ "data:text/html,<html id='html3'><body id='body3' contenteditable='true'><button id='button3'>Tab 3</button></body></html>";
+
+var fm = Services.focus;
+
+async function expectFocusOnF6(
+ backward,
+ expectedDocument,
+ expectedElement,
+ onContent,
+ desc
+) {
+ if (onContent) {
+ let success = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [expectedElement],
+ async function (expectedElementId) {
+ content.lastResult = "";
+ let contentExpectedElement =
+ content.document.getElementById(expectedElementId);
+ if (!contentExpectedElement) {
+ // Element not found, so look in the child frames.
+ for (let f = 0; f < content.frames.length; f++) {
+ if (content.frames[f].document.getElementById(expectedElementId)) {
+ contentExpectedElement = content.frames[f].document;
+ break;
+ }
+ }
+ } else if (contentExpectedElement.localName == "html") {
+ contentExpectedElement = contentExpectedElement.ownerDocument;
+ }
+
+ if (!contentExpectedElement) {
+ return null;
+ }
+
+ contentExpectedElement.addEventListener(
+ "focus",
+ function () {
+ let details =
+ Services.focus.focusedWindow.document.documentElement.id;
+ if (Services.focus.focusedElement) {
+ details += "," + Services.focus.focusedElement.id;
+ }
+
+ // Assign the result to a temporary place, to be used
+ // by the next spawn call.
+ content.lastResult = details;
+ },
+ { capture: true, once: true }
+ );
+
+ return !!contentExpectedElement;
+ }
+ );
+
+ ok(success, "expected element " + expectedElement + " was found");
+
+ EventUtils.synthesizeKey("VK_F6", { shiftKey: backward });
+
+ let expected = expectedDocument;
+ if (!expectedElement.startsWith("html")) {
+ expected += "," + expectedElement;
+ }
+
+ let result = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ await ContentTaskUtils.waitForCondition(() => content.lastResult);
+ return content.lastResult;
+ }
+ );
+ is(result, expected, desc + " child focus matches");
+ } else {
+ let focusPromise = BrowserTestUtils.waitForEvent(window, "focus", true);
+ EventUtils.synthesizeKey("VK_F6", { shiftKey: backward });
+ await focusPromise;
+ }
+
+ if (typeof expectedElement == "string") {
+ expectedElement = fm.focusedWindow.document.getElementById(expectedElement);
+ }
+
+ if (gMultiProcessBrowser && onContent) {
+ expectedDocument = "main-window";
+ expectedElement = gBrowser.selectedBrowser;
+ }
+
+ is(
+ fm.focusedWindow.document.documentElement.id,
+ expectedDocument,
+ desc + " document matches"
+ );
+ is(
+ fm.focusedElement,
+ expectedElement,
+ desc +
+ " element matches (wanted: " +
+ expectedElement.id +
+ " got: " +
+ fm.focusedElement.id +
+ ")"
+ );
+}
+
+// Load a page and navigate between it and the chrome window.
+add_task(async function () {
+ let page1Promise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ testPage1
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, testPage1);
+ await page1Promise;
+
+ // When the urlbar is focused, pressing F6 should focus the root of the content page.
+ gURLBar.focus();
+ await expectFocusOnF6(
+ false,
+ "html1",
+ "html1",
+ true,
+ "basic focus content page"
+ );
+
+ // When the content is focused, pressing F6 should focus the urlbar.
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "basic focus content page urlbar"
+ );
+
+ // When a button in content is focused, pressing F6 should focus the urlbar.
+ await expectFocusOnF6(
+ false,
+ "html1",
+ "html1",
+ true,
+ "basic focus content page with button focused"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ return content.document.getElementById("button1").focus();
+ });
+
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "basic focus content page with button focused urlbar"
+ );
+
+ // The document root should be focused, not the button
+ await expectFocusOnF6(
+ false,
+ "html1",
+ "html1",
+ true,
+ "basic focus again content page with button focused"
+ );
+
+ // Check to ensure that the root element is focused
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ Assert.ok(
+ content.document.activeElement == content.document.documentElement,
+ "basic focus again content page with button focused child root is focused"
+ );
+ });
+});
+
+// Open a second tab. Document focus should skip the background tab.
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage2);
+
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "basic focus content page and second tab urlbar"
+ );
+ await expectFocusOnF6(
+ false,
+ "html2",
+ "html2",
+ true,
+ "basic focus content page with second tab"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Shift+F6 should navigate backwards. There's only one document here so the effect
+// is the same.
+add_task(async function () {
+ gURLBar.focus();
+ await expectFocusOnF6(
+ true,
+ "html1",
+ "html1",
+ true,
+ "back focus content page"
+ );
+ await expectFocusOnF6(
+ true,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "back focus content page urlbar"
+ );
+});
+
+// Open the sidebar and navigate between the sidebar, content and top-level window
+add_task(async function () {
+ let sidebar = document.getElementById("sidebar");
+
+ let loadPromise = BrowserTestUtils.waitForEvent(sidebar, "load", true);
+ SidebarUI.toggle("viewBookmarksSidebar");
+ await loadPromise;
+
+ gURLBar.focus();
+ await expectFocusOnF6(
+ false,
+ "bookmarksPanel",
+ sidebar.contentDocument.getElementById("search-box").inputField,
+ false,
+ "focus with sidebar open sidebar"
+ );
+ await expectFocusOnF6(
+ false,
+ "html1",
+ "html1",
+ true,
+ "focus with sidebar open content"
+ );
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "focus with sidebar urlbar"
+ );
+
+ // Now go backwards
+ await expectFocusOnF6(
+ true,
+ "html1",
+ "html1",
+ true,
+ "back focus with sidebar open content"
+ );
+ await expectFocusOnF6(
+ true,
+ "bookmarksPanel",
+ sidebar.contentDocument.getElementById("search-box").inputField,
+ false,
+ "back focus with sidebar open sidebar"
+ );
+ await expectFocusOnF6(
+ true,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "back focus with sidebar urlbar"
+ );
+
+ SidebarUI.toggle("viewBookmarksSidebar");
+});
+
+// Navigate when the downloads panel is open
+add_task(async function test_download_focus() {
+ await pushPrefs(
+ ["accessibility.tabfocus", 7],
+ ["browser.download.autohideButton", false],
+ ["security.dialog_enable_delay", 0]
+ );
+ await promiseButtonShown("downloads-button");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown",
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("downloads-button"),
+ {}
+ );
+ await popupShownPromise;
+
+ gURLBar.focus();
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ document.getElementById("downloadsHistory"),
+ false,
+ "focus with downloads panel open panel"
+ );
+ await expectFocusOnF6(
+ false,
+ "html1",
+ "html1",
+ true,
+ "focus with downloads panel open"
+ );
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "focus downloads panel open urlbar"
+ );
+
+ // Now go backwards
+ await expectFocusOnF6(
+ true,
+ "html1",
+ "html1",
+ true,
+ "back focus with downloads panel open"
+ );
+ await expectFocusOnF6(
+ true,
+ "main-window",
+ document.getElementById("downloadsHistory"),
+ false,
+ "back focus with downloads panel open"
+ );
+ await expectFocusOnF6(
+ true,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "back focus downloads panel open urlbar"
+ );
+
+ let downloadsPopup = document.getElementById("downloadsPanel");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ downloadsPopup,
+ "popuphidden",
+ true
+ );
+ downloadsPopup.hidePopup();
+ await popupHiddenPromise;
+});
+
+// Navigation with a contenteditable body
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage3);
+
+ // The body should be focused when it is editable, not the root.
+ gURLBar.focus();
+ await expectFocusOnF6(
+ false,
+ "html3",
+ "body3",
+ true,
+ "focus with contenteditable body"
+ );
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "focus with contenteditable body urlbar"
+ );
+
+ // Now go backwards
+
+ await expectFocusOnF6(
+ false,
+ "html3",
+ "body3",
+ true,
+ "back focus with contenteditable body"
+ );
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "back focus with contenteditable body urlbar"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Navigation with a frameset loaded
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/base/content/test/general/file_documentnavigation_frameset.html"
+ );
+
+ gURLBar.focus();
+ await expectFocusOnF6(
+ false,
+ "htmlframe1",
+ "htmlframe1",
+ true,
+ "focus on frameset frame 0"
+ );
+ await expectFocusOnF6(
+ false,
+ "htmlframe2",
+ "htmlframe2",
+ true,
+ "focus on frameset frame 1"
+ );
+ await expectFocusOnF6(
+ false,
+ "htmlframe3",
+ "htmlframe3",
+ true,
+ "focus on frameset frame 2"
+ );
+ await expectFocusOnF6(
+ false,
+ "htmlframe4",
+ "htmlframe4",
+ true,
+ "focus on frameset frame 3"
+ );
+ await expectFocusOnF6(
+ false,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "focus on frameset frame urlbar"
+ );
+
+ await expectFocusOnF6(
+ true,
+ "htmlframe4",
+ "htmlframe4",
+ true,
+ "back focus on frameset frame 3"
+ );
+ await expectFocusOnF6(
+ true,
+ "htmlframe3",
+ "htmlframe3",
+ true,
+ "back focus on frameset frame 2"
+ );
+ await expectFocusOnF6(
+ true,
+ "htmlframe2",
+ "htmlframe2",
+ true,
+ "back focus on frameset frame 1"
+ );
+ await expectFocusOnF6(
+ true,
+ "htmlframe1",
+ "htmlframe1",
+ true,
+ "back focus on frameset frame 0"
+ );
+ await expectFocusOnF6(
+ true,
+ "main-window",
+ gURLBar.inputField,
+ false,
+ "back focus on frameset frame urlbar"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// XXXndeakin add tests for browsers inside of panels
+
+function promiseButtonShown(id) {
+ let dwu = window.windowUtils;
+ return TestUtils.waitForCondition(() => {
+ let target = document.getElementById(id);
+ let bounds = dwu.getBoundsWithoutFlushing(target);
+ return bounds.width > 0 && bounds.height > 0;
+ }, `Waiting for button ${id} to have non-0 size`);
+}
diff --git a/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js b/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js
new file mode 100644
index 0000000000..c96fa6cf7b
--- /dev/null
+++ b/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js
@@ -0,0 +1,237 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+// This test tends to trigger a race in the fullscreen time telemetry,
+// where the fullscreen enter and fullscreen exit events (which use the
+// same histogram ID) overlap. That causes TelemetryStopwatch to log an
+// error.
+SimpleTest.ignoreAllUncaughtExceptions(true);
+
+function listenOneEvent(aEvent, aListener) {
+ function listener(evt) {
+ removeEventListener(aEvent, listener);
+ aListener(evt);
+ }
+ addEventListener(aEvent, listener);
+}
+
+function queryFullscreenState(browser) {
+ return SpecialPowers.spawn(browser, [], () => {
+ return {
+ inDOMFullscreen: !!content.document.fullscreenElement,
+ inFullscreen: content.fullScreen,
+ };
+ });
+}
+
+function captureUnexpectedFullscreenChange() {
+ ok(false, "catched an unexpected fullscreen change");
+}
+
+const FS_CHANGE_DOM = 1 << 0;
+const FS_CHANGE_SIZE = 1 << 1;
+const FS_CHANGE_BOTH = FS_CHANGE_DOM | FS_CHANGE_SIZE;
+
+function waitForDocActivated(aBrowser) {
+ return SpecialPowers.spawn(aBrowser, [], () => {
+ return ContentTaskUtils.waitForCondition(
+ () => content.browsingContext.isActive && content.document.hasFocus()
+ );
+ });
+}
+
+function waitForFullscreenChanges(aBrowser, aFlags) {
+ return new Promise(resolve => {
+ let fullscreenData = null;
+ let sizemodeChanged = false;
+ function tryResolve() {
+ if (
+ (!(aFlags & FS_CHANGE_DOM) || fullscreenData) &&
+ (!(aFlags & FS_CHANGE_SIZE) || sizemodeChanged)
+ ) {
+ // In the platforms that support reporting occlusion state (e.g. Mac),
+ // enter/exit fullscreen mode will trigger docshell being set to
+ // non-activate and then set to activate back again.
+ // For those platform, we should wait until the docshell has been
+ // activated again, otherwise, the fullscreen request might be denied.
+ waitForDocActivated(aBrowser).then(() => {
+ if (!fullscreenData) {
+ queryFullscreenState(aBrowser).then(resolve);
+ } else {
+ resolve(fullscreenData);
+ }
+ });
+ }
+ }
+ if (aFlags & FS_CHANGE_SIZE) {
+ listenOneEvent("sizemodechange", () => {
+ sizemodeChanged = true;
+ tryResolve();
+ });
+ }
+ if (aFlags & FS_CHANGE_DOM) {
+ BrowserTestUtils.waitForContentEvent(aBrowser, "fullscreenchange").then(
+ async () => {
+ fullscreenData = await queryFullscreenState(aBrowser);
+ tryResolve();
+ }
+ );
+ }
+ });
+}
+
+var gTests = [
+ {
+ desc: "document method",
+ affectsFullscreenMode: false,
+ exitFunc: browser => {
+ SpecialPowers.spawn(browser, [], () => {
+ content.document.exitFullscreen();
+ });
+ },
+ },
+ {
+ desc: "escape key",
+ affectsFullscreenMode: false,
+ exitFunc: () => {
+ executeSoon(() => EventUtils.synthesizeKey("KEY_Escape"));
+ },
+ },
+ {
+ desc: "F11 key",
+ affectsFullscreenMode: true,
+ exitFunc() {
+ executeSoon(() => EventUtils.synthesizeKey("KEY_F11"));
+ },
+ },
+];
+
+function checkState(expectedStates, contentStates) {
+ is(
+ contentStates.inDOMFullscreen,
+ expectedStates.inDOMFullscreen,
+ "The DOM fullscreen state of the content should match"
+ );
+ // TODO window.fullScreen is not updated as soon as the fullscreen
+ // state flips in child process, hence checking it could cause
+ // anonying intermittent failure. As we just want to confirm the
+ // fullscreen state of the browser window, we can just check the
+ // that on the chrome window below.
+ // is(contentStates.inFullscreen, expectedStates.inFullscreen,
+ // "The fullscreen state of the content should match");
+ is(
+ !!document.fullscreenElement,
+ expectedStates.inDOMFullscreen,
+ "The DOM fullscreen state of the chrome should match"
+ );
+ is(
+ window.fullScreen,
+ expectedStates.inFullscreen,
+ "The fullscreen state of the chrome should match"
+ );
+}
+
+const kPage =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/" +
+ "base/content/test/general/dummy_page.html";
+
+add_task(async function () {
+ await pushPrefs(
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"]
+ );
+
+ registerCleanupFunction(async function () {
+ if (window.fullScreen) {
+ let fullscreenPromise = waitForFullscreenChanges(
+ gBrowser.selectedBrowser,
+ FS_CHANGE_SIZE
+ );
+ executeSoon(() => BrowserFullScreen());
+ await fullscreenPromise;
+ }
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: kPage,
+ });
+ let browser = tab.linkedBrowser;
+
+ // As requestFullscreen checks the active state of the docshell,
+ // wait for the document to be activated, just to be sure that
+ // the fullscreen request won't be denied.
+ await waitForDocActivated(browser);
+
+ for (let test of gTests) {
+ let contentStates;
+ info("Testing exit DOM fullscreen via " + test.desc);
+
+ contentStates = await queryFullscreenState(browser);
+ checkState({ inDOMFullscreen: false, inFullscreen: false }, contentStates);
+
+ /* DOM fullscreen without fullscreen mode */
+
+ info("> Enter DOM fullscreen");
+ let fullscreenPromise = waitForFullscreenChanges(browser, FS_CHANGE_BOTH);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.requestFullscreen();
+ });
+ contentStates = await fullscreenPromise;
+ checkState({ inDOMFullscreen: true, inFullscreen: true }, contentStates);
+
+ info("> Exit DOM fullscreen");
+ fullscreenPromise = waitForFullscreenChanges(browser, FS_CHANGE_BOTH);
+ test.exitFunc(browser);
+ contentStates = await fullscreenPromise;
+ checkState({ inDOMFullscreen: false, inFullscreen: false }, contentStates);
+
+ /* DOM fullscreen with fullscreen mode */
+
+ info("> Enter fullscreen mode");
+ // Need to be asynchronous because sizemodechange event could be
+ // dispatched synchronously, which would cause the event listener
+ // miss that event and wait infinitely.
+ fullscreenPromise = waitForFullscreenChanges(browser, FS_CHANGE_SIZE);
+ executeSoon(() => BrowserFullScreen());
+ contentStates = await fullscreenPromise;
+ checkState({ inDOMFullscreen: false, inFullscreen: true }, contentStates);
+
+ info("> Enter DOM fullscreen in fullscreen mode");
+ fullscreenPromise = waitForFullscreenChanges(browser, FS_CHANGE_DOM);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.requestFullscreen();
+ });
+ contentStates = await fullscreenPromise;
+ checkState({ inDOMFullscreen: true, inFullscreen: true }, contentStates);
+
+ info("> Exit DOM fullscreen in fullscreen mode");
+ fullscreenPromise = waitForFullscreenChanges(
+ browser,
+ test.affectsFullscreenMode ? FS_CHANGE_BOTH : FS_CHANGE_DOM
+ );
+ test.exitFunc(browser);
+ contentStates = await fullscreenPromise;
+ checkState(
+ {
+ inDOMFullscreen: false,
+ inFullscreen: !test.affectsFullscreenMode,
+ },
+ contentStates
+ );
+
+ /* Cleanup */
+
+ // Exit fullscreen mode if we are still in
+ if (window.fullScreen) {
+ info("> Cleanup");
+ fullscreenPromise = waitForFullscreenChanges(browser, FS_CHANGE_SIZE);
+ executeSoon(() => BrowserFullScreen());
+ await fullscreenPromise;
+ }
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_double_close_tab.js b/browser/base/content/test/general/browser_double_close_tab.js
new file mode 100644
index 0000000000..554aeb8077
--- /dev/null
+++ b/browser/base/content/test/general/browser_double_close_tab.js
@@ -0,0 +1,120 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
+var testTab;
+
+const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+function waitForDialog(callback) {
+ function onDialogLoaded(nodeOrDialogWindow) {
+ let node = CONTENT_PROMPT_SUBDIALOG
+ ? nodeOrDialogWindow.document.querySelector("dialog")
+ : nodeOrDialogWindow;
+ Services.obs.removeObserver(onDialogLoaded, "tabmodal-dialog-loaded");
+ Services.obs.removeObserver(onDialogLoaded, "common-dialog-loaded");
+ // Allow dialog's onLoad call to run to completion
+ Promise.resolve().then(() => callback(node));
+ }
+
+ // Listen for the dialog being created
+ Services.obs.addObserver(onDialogLoaded, "tabmodal-dialog-loaded");
+ Services.obs.addObserver(onDialogLoaded, "common-dialog-loaded");
+}
+
+function waitForDialogDestroyed(node, callback) {
+ // Now listen for the dialog going away again...
+ let observer = new MutationObserver(function (muts) {
+ if (!node.parentNode) {
+ ok(true, "Dialog is gone");
+ done();
+ }
+ });
+ observer.observe(node.parentNode, { childList: true });
+
+ if (CONTENT_PROMPT_SUBDIALOG) {
+ node.ownerGlobal.addEventListener("unload", done);
+ }
+
+ let failureTimeout = setTimeout(function () {
+ ok(false, "Dialog should have been destroyed");
+ done();
+ }, 10000);
+
+ function done() {
+ clearTimeout(failureTimeout);
+ observer.disconnect();
+ observer = null;
+
+ if (CONTENT_PROMPT_SUBDIALOG) {
+ node.ownerGlobal.removeEventListener("unload", done);
+ SimpleTest.executeSoon(callback);
+ } else {
+ callback();
+ }
+ }
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ // XXXgijs the reason this has nesting and callbacks rather than promises is
+ // that DOM promises resolve on the next tick. So they're scheduled
+ // in an event queue. So when we spin a new event queue for a modal dialog...
+ // everything gets messed up and the promise's .then callbacks never get
+ // called, despite resolve() being called just fine.
+ await new Promise(resolveOuter => {
+ waitForDialog(dialogNode => {
+ waitForDialogDestroyed(dialogNode, () => {
+ let doCompletion = () => setTimeout(resolveOuter, 0);
+ info("Now checking if dialog is destroyed");
+
+ if (CONTENT_PROMPT_SUBDIALOG) {
+ ok(
+ !dialogNode.ownerGlobal || dialogNode.ownerGlobal.closed,
+ "onbeforeunload dialog should be gone."
+ );
+ if (dialogNode.ownerGlobal && !dialogNode.ownerGlobal.closed) {
+ dialogNode.acceptDialog();
+ }
+ } else {
+ ok(!dialogNode.parentNode, "onbeforeunload dialog should be gone.");
+ if (dialogNode.parentNode) {
+ // Failed to remove onbeforeunload dialog, so do it ourselves:
+ let leaveBtn = dialogNode.querySelector(".tabmodalprompt-button0");
+ waitForDialogDestroyed(dialogNode, doCompletion);
+ EventUtils.synthesizeMouseAtCenter(leaveBtn, {});
+ return;
+ }
+ }
+
+ doCompletion();
+ });
+ // Click again:
+ testTab.closeButton.click();
+ });
+ // Click once:
+ testTab.closeButton.click();
+ });
+ await TestUtils.waitForCondition(() => !testTab.parentNode);
+ ok(!testTab.parentNode, "Tab should be closed completely");
+});
+
+registerCleanupFunction(async function () {
+ if (testTab.parentNode) {
+ // Remove the handler, or closing this tab will prove tricky:
+ try {
+ await SpecialPowers.spawn(testTab.linkedBrowser, [], function () {
+ content.window.onbeforeunload = null;
+ });
+ } catch (ex) {}
+ gBrowser.removeTab(testTab);
+ }
+});
diff --git a/browser/base/content/test/general/browser_drag.js b/browser/base/content/test/general/browser_drag.js
new file mode 100644
index 0000000000..04373e7ce2
--- /dev/null
+++ b/browser/base/content/test/general/browser_drag.js
@@ -0,0 +1,64 @@
+async function test() {
+ waitForExplicitFinish();
+
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // ---- Test dragging the proxy icon ---
+ var value = content.location.href;
+ var urlString = value + "\n" + content.document.title;
+ var htmlString = '<a href="' + value + '">' + value + "</a>";
+ var expected = [
+ [
+ { type: "text/x-moz-url", data: urlString },
+ { type: "text/uri-list", data: value },
+ { type: "text/plain", data: value },
+ { type: "text/html", data: htmlString },
+ ],
+ ];
+ // set the valid attribute so dropping is allowed
+ var oldstate = gURLBar.getAttribute("pageproxystate");
+ gURLBar.setPageProxyState("valid");
+ let result = await EventUtils.synthesizePlainDragAndCancel(
+ {
+ srcElement: document.getElementById("identity-icon-box"),
+ },
+ expected
+ );
+ ok(result === true, "dragging dataTransfer should be expected");
+ gURLBar.setPageProxyState(oldstate);
+ // Now, the identity information panel is opened by the proxy icon click.
+ // We need to close it for next tests.
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
+
+ // now test dragging onto a tab
+ var tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ var browser = gBrowser.getBrowserForTab(tab);
+
+ browser.addEventListener(
+ "load",
+ function () {
+ is(
+ browser.contentWindow.location,
+ "http://mochi.test:8888/",
+ "drop on tab"
+ );
+ gBrowser.removeTab(tab);
+ finish();
+ },
+ true
+ );
+
+ EventUtils.synthesizeDrop(
+ tab,
+ tab,
+ [[{ type: "text/uri-list", data: "http://mochi.test:8888/" }]],
+ "copy",
+ window
+ );
+}
diff --git a/browser/base/content/test/general/browser_duplicateIDs.js b/browser/base/content/test/general/browser_duplicateIDs.js
new file mode 100644
index 0000000000..b0c65c6af6
--- /dev/null
+++ b/browser/base/content/test/general/browser_duplicateIDs.js
@@ -0,0 +1,11 @@
+function test() {
+ var ids = {};
+ Array.prototype.forEach.call(
+ document.querySelectorAll("[id]"),
+ function (node) {
+ var id = node.id;
+ ok(!(id in ids), id + " should be unique");
+ ids[id] = null;
+ }
+ );
+}
diff --git a/browser/base/content/test/general/browser_findbarClose.js b/browser/base/content/test/general/browser_findbarClose.js
new file mode 100644
index 0000000000..e0fe2fcb98
--- /dev/null
+++ b/browser/base/content/test/general/browser_findbarClose.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests find bar auto-close behavior
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function findbar_test() {
+ let newTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ gBrowser.selectedTab = newTab;
+
+ let url = TEST_PATH + "test_bug628179.html";
+ let promise = BrowserTestUtils.browserLoaded(
+ newTab.linkedBrowser,
+ false,
+ url
+ );
+ BrowserTestUtils.loadURIString(newTab.linkedBrowser, url);
+ await promise;
+
+ await gFindBarPromise;
+ gFindBar.open();
+
+ await new ContentTask.spawn(newTab.linkedBrowser, null, async function () {
+ let iframe = content.document.getElementById("iframe");
+ let awaitLoad = ContentTaskUtils.waitForEvent(iframe, "load", false);
+ iframe.src = "https://example.org/";
+ await awaitLoad;
+ });
+
+ ok(
+ !gFindBar.hidden,
+ "the Find bar isn't hidden after the location of a subdocument changes"
+ );
+
+ let findBarClosePromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "findbarclose"
+ );
+ gFindBar.close();
+ await findBarClosePromise;
+
+ gBrowser.removeTab(newTab);
+});
diff --git a/browser/base/content/test/general/browser_focusonkeydown.js b/browser/base/content/test/general/browser_focusonkeydown.js
new file mode 100644
index 0000000000..9cf1f113f5
--- /dev/null
+++ b/browser/base/content/test/general/browser_focusonkeydown.js
@@ -0,0 +1,34 @@
+add_task(async function () {
+ let keyUps = 0;
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<body>"
+ );
+
+ gURLBar.focus();
+
+ window.addEventListener(
+ "keyup",
+ function (event) {
+ if (event.originalTarget == gURLBar.inputField) {
+ keyUps++;
+ }
+ },
+ { capture: true, once: true }
+ );
+
+ gURLBar.addEventListener(
+ "keydown",
+ function (event) {
+ gBrowser.selectedBrowser.focus();
+ },
+ { capture: true, once: true }
+ );
+
+ EventUtils.sendString("v");
+
+ is(keyUps, 1, "Key up fired at url bar");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_fullscreen-window-open.js b/browser/base/content/test/general/browser_fullscreen-window-open.js
new file mode 100644
index 0000000000..2b21e34e92
--- /dev/null
+++ b/browser/base/content/test/general/browser_fullscreen-window-open.js
@@ -0,0 +1,366 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const PREF_DISABLE_OPEN_NEW_WINDOW =
+ "browser.link.open_newwindow.disabled_in_fullscreen";
+const PREF_BLOCK_TOPLEVEL_DATA =
+ "security.data_uri.block_toplevel_data_uri_navigations";
+const isOSX = Services.appinfo.OS === "Darwin";
+
+const TEST_FILE = "file_fullscreen-window-open.html";
+const gHttpTestRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://127.0.0.1:8888/"
+);
+
+var newWin;
+var newBrowser;
+
+async function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, true);
+ Services.prefs.setBoolPref(PREF_BLOCK_TOPLEVEL_DATA, false);
+
+ newWin = await BrowserTestUtils.openNewBrowserWindow();
+ newBrowser = newWin.gBrowser;
+ await promiseTabLoadEvent(newBrowser.selectedTab, gHttpTestRoot + TEST_FILE);
+
+ // Enter browser fullscreen mode.
+ newWin.BrowserFullScreen();
+
+ runNextTest();
+}
+
+registerCleanupFunction(async function () {
+ // Exit browser fullscreen mode.
+ newWin.BrowserFullScreen();
+
+ await BrowserTestUtils.closeWindow(newWin);
+
+ Services.prefs.clearUserPref(PREF_DISABLE_OPEN_NEW_WINDOW);
+ Services.prefs.clearUserPref(PREF_BLOCK_TOPLEVEL_DATA);
+});
+
+var gTests = [
+ test_open,
+ test_open_with_size,
+ test_open_with_pos,
+ test_open_with_outerSize,
+ test_open_with_innerSize,
+ test_open_with_dialog,
+ test_open_when_open_new_window_by_pref,
+ test_open_with_pref_to_disable_in_fullscreen,
+ test_open_from_chrome,
+];
+
+function runNextTest() {
+ let testCase = gTests.shift();
+ if (testCase) {
+ executeSoon(testCase);
+ } else {
+ finish();
+ }
+}
+
+// Test for window.open() with no feature.
+function test_open() {
+ waitForTabOpen({
+ message: {
+ title: "test_open",
+ param: "",
+ },
+ finalizeFn() {},
+ });
+}
+
+// Test for window.open() with width/height.
+function test_open_with_size() {
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_size",
+ param: "width=400,height=400",
+ },
+ finalizeFn() {},
+ });
+}
+
+// Test for window.open() with top/left.
+function test_open_with_pos() {
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_pos",
+ param: "top=200,left=200",
+ },
+ finalizeFn() {},
+ });
+}
+
+// Test for window.open() with outerWidth/Height.
+function test_open_with_outerSize() {
+ let [outerWidth, outerHeight] = [newWin.outerWidth, newWin.outerHeight];
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_outerSize",
+ param: "outerWidth=200,outerHeight=200",
+ },
+ successFn() {
+ is(newWin.outerWidth, outerWidth, "Don't change window.outerWidth.");
+ is(newWin.outerHeight, outerHeight, "Don't change window.outerHeight.");
+ },
+ finalizeFn() {},
+ });
+}
+
+// Test for window.open() with innerWidth/Height.
+function test_open_with_innerSize() {
+ let [innerWidth, innerHeight] = [newWin.innerWidth, newWin.innerHeight];
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_innerSize",
+ param: "innerWidth=200,innerHeight=200",
+ },
+ successFn() {
+ is(newWin.innerWidth, innerWidth, "Don't change window.innerWidth.");
+ is(newWin.innerHeight, innerHeight, "Don't change window.innerHeight.");
+ },
+ finalizeFn() {},
+ });
+}
+
+// Test for window.open() with dialog.
+function test_open_with_dialog() {
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_dialog",
+ param: "dialog=yes",
+ },
+ finalizeFn() {},
+ });
+}
+
+// Test for window.open()
+// when "browser.link.open_newwindow" is nsIBrowserDOMWindow.OPEN_NEWWINDOW
+function test_open_when_open_new_window_by_pref() {
+ const PREF_NAME = "browser.link.open_newwindow";
+ Services.prefs.setIntPref(PREF_NAME, Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW);
+ is(
+ Services.prefs.getIntPref(PREF_NAME),
+ Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW,
+ PREF_NAME + " is nsIBrowserDOMWindow.OPEN_NEWWINDOW at this time"
+ );
+
+ waitForTabOpen({
+ message: {
+ title: "test_open_when_open_new_window_by_pref",
+ param: "width=400,height=400",
+ },
+ finalizeFn() {
+ Services.prefs.clearUserPref(PREF_NAME);
+ },
+ });
+}
+
+// Test for the pref, "browser.link.open_newwindow.disabled_in_fullscreen"
+function test_open_with_pref_to_disable_in_fullscreen() {
+ Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, false);
+
+ waitForWindowOpen({
+ message: {
+ title: "test_open_with_pref_disabled_in_fullscreen",
+ param: "width=400,height=400",
+ },
+ finalizeFn() {
+ Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, true);
+ },
+ });
+}
+
+// Test for window.open() called from chrome context.
+function test_open_from_chrome() {
+ waitForWindowOpenFromChrome({
+ message: {
+ title: "test_open_from_chrome",
+ param: "",
+ option: "noopener",
+ },
+ finalizeFn() {},
+ });
+}
+
+function waitForTabOpen(aOptions) {
+ let message = aOptions.message;
+
+ if (!message.title) {
+ ok(false, "Can't get message.title.");
+ aOptions.finalizeFn();
+ runNextTest();
+ return;
+ }
+
+ info("Running test: " + message.title);
+
+ let onTabOpen = function onTabOpen(aEvent) {
+ newBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true);
+
+ let tab = aEvent.target;
+ whenTabLoaded(tab, function () {
+ is(
+ tab.linkedBrowser.contentTitle,
+ message.title,
+ "Opened Tab is expected: " + message.title
+ );
+
+ if (aOptions.successFn) {
+ aOptions.successFn();
+ }
+
+ newBrowser.removeTab(tab);
+ finalize();
+ });
+ };
+ newBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true);
+
+ let finalize = function () {
+ aOptions.finalizeFn();
+ info("Finished: " + message.title);
+ runNextTest();
+ };
+
+ const URI =
+ "data:text/html;charset=utf-8,<!DOCTYPE html><html><head><title>" +
+ message.title +
+ "<%2Ftitle><%2Fhead><body><%2Fbody><%2Fhtml>";
+
+ executeWindowOpenInContent({
+ uri: URI,
+ title: message.title,
+ option: message.param,
+ });
+}
+
+function waitForWindowOpen(aOptions) {
+ let message = aOptions.message;
+ let url = aOptions.url || "about:blank";
+
+ if (!message.title) {
+ ok(false, "Can't get message.title");
+ aOptions.finalizeFn();
+ runNextTest();
+ return;
+ }
+
+ info("Running test: " + message.title);
+
+ let onFinalize = function () {
+ aOptions.finalizeFn();
+
+ info("Finished: " + message.title);
+ runNextTest();
+ };
+
+ let listener = new WindowListener(
+ message.title,
+ AppConstants.BROWSER_CHROME_URL,
+ {
+ onSuccess: aOptions.successFn,
+ onFinalize,
+ }
+ );
+ Services.wm.addListener(listener);
+
+ executeWindowOpenInContent({
+ uri: url,
+ title: message.title,
+ option: message.param,
+ });
+}
+
+function executeWindowOpenInContent(aParam) {
+ SpecialPowers.spawn(
+ newBrowser.selectedBrowser,
+ [JSON.stringify(aParam)],
+ async function (dataTestParam) {
+ let testElm = content.document.getElementById("test");
+ testElm.setAttribute("data-test-param", dataTestParam);
+ testElm.click();
+ }
+ );
+}
+
+function waitForWindowOpenFromChrome(aOptions) {
+ let message = aOptions.message;
+ let url = aOptions.url || "about:blank";
+
+ if (!message.title) {
+ ok(false, "Can't get message.title");
+ aOptions.finalizeFn();
+ runNextTest();
+ return;
+ }
+
+ info("Running test: " + message.title);
+
+ let onFinalize = function () {
+ aOptions.finalizeFn();
+
+ info("Finished: " + message.title);
+ runNextTest();
+ };
+
+ let listener = new WindowListener(
+ message.title,
+ AppConstants.BROWSER_CHROME_URL,
+ {
+ onSuccess: aOptions.successFn,
+ onFinalize,
+ }
+ );
+ Services.wm.addListener(listener);
+
+ newWin.open(url, message.title, message.option);
+}
+
+function WindowListener(aTitle, aUrl, aCallBackObj) {
+ this.test_title = aTitle;
+ this.test_url = aUrl;
+ this.callback_onSuccess = aCallBackObj.onSuccess;
+ this.callBack_onFinalize = aCallBackObj.onFinalize;
+}
+WindowListener.prototype = {
+ test_title: null,
+ test_url: null,
+ callback_onSuccess: null,
+ callBack_onFinalize: null,
+
+ onOpenWindow(aXULWindow) {
+ Services.wm.removeListener(this);
+
+ let domwindow = aXULWindow.docShell.domWindow;
+ let onLoad = aEvent => {
+ is(
+ domwindow.document.location.href,
+ this.test_url,
+ "Opened Window is expected: " + this.test_title
+ );
+ if (this.callback_onSuccess) {
+ this.callback_onSuccess();
+ }
+
+ domwindow.removeEventListener("load", onLoad, true);
+
+ // wait for trasition to fullscreen on OSX Lion later
+ if (isOSX) {
+ setTimeout(() => {
+ domwindow.close();
+ executeSoon(this.callBack_onFinalize);
+ }, 3000);
+ } else {
+ domwindow.close();
+ executeSoon(this.callBack_onFinalize);
+ }
+ };
+ domwindow.addEventListener("load", onLoad, true);
+ },
+ onCloseWindow(aXULWindow) {},
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowMediatorListener"]),
+};
diff --git a/browser/base/content/test/general/browser_gestureSupport.js b/browser/base/content/test/general/browser_gestureSupport.js
new file mode 100644
index 0000000000..d8f0331268
--- /dev/null
+++ b/browser/base/content/test/general/browser_gestureSupport.js
@@ -0,0 +1,1132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+// Simple gestures tests
+//
+// Some of these tests require the ability to disable the fact that the
+// Firefox chrome intentionally prevents "simple gesture" events from
+// reaching web content.
+
+var test_utils;
+var test_commandset;
+var test_prefBranch = "browser.gesture.";
+var test_normalTab;
+
+async function test() {
+ waitForExplicitFinish();
+
+ // Disable the default gestures support during this part of the test
+ gGestureSupport.init(false);
+
+ test_utils = window.windowUtils;
+
+ // Run the tests of "simple gesture" events generally
+ test_EnsureConstantsAreDisjoint();
+ test_TestEventListeners();
+ test_TestEventCreation();
+
+ // Reenable the default gestures support. The remaining tests target
+ // the Firefox gesture functionality.
+ gGestureSupport.init(true);
+
+ const aPage = "about:about";
+ test_normalTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ aPage,
+ true /* waitForLoad */
+ );
+
+ // Test Firefox's gestures support.
+ test_commandset = document.getElementById("mainCommandSet");
+ await test_swipeGestures();
+ await test_latchedGesture("pinch", "out", "in", "MozMagnifyGesture");
+ await test_thresholdGesture("pinch", "out", "in", "MozMagnifyGesture");
+ test_rotateGestures();
+}
+
+var test_eventCount = 0;
+var test_expectedType;
+var test_expectedDirection;
+var test_expectedDelta;
+var test_expectedModifiers;
+var test_expectedClickCount;
+var test_imageTab;
+
+function test_gestureListener(evt) {
+ is(
+ evt.type,
+ test_expectedType,
+ "evt.type (" + evt.type + ") does not match expected value"
+ );
+ is(
+ evt.target,
+ test_utils.elementFromPoint(60, 60, false, false),
+ "evt.target (" + evt.target + ") does not match expected value"
+ );
+ is(
+ evt.clientX,
+ 60,
+ "evt.clientX (" + evt.clientX + ") does not match expected value"
+ );
+ is(
+ evt.clientY,
+ 60,
+ "evt.clientY (" + evt.clientY + ") does not match expected value"
+ );
+ isnot(
+ evt.screenX,
+ 0,
+ "evt.screenX (" + evt.screenX + ") does not match expected value"
+ );
+ isnot(
+ evt.screenY,
+ 0,
+ "evt.screenY (" + evt.screenY + ") does not match expected value"
+ );
+
+ is(
+ evt.direction,
+ test_expectedDirection,
+ "evt.direction (" + evt.direction + ") does not match expected value"
+ );
+ is(
+ evt.delta,
+ test_expectedDelta,
+ "evt.delta (" + evt.delta + ") does not match expected value"
+ );
+
+ is(
+ evt.shiftKey,
+ (test_expectedModifiers & Event.SHIFT_MASK) != 0,
+ "evt.shiftKey did not match expected value"
+ );
+ is(
+ evt.ctrlKey,
+ (test_expectedModifiers & Event.CONTROL_MASK) != 0,
+ "evt.ctrlKey did not match expected value"
+ );
+ is(
+ evt.altKey,
+ (test_expectedModifiers & Event.ALT_MASK) != 0,
+ "evt.altKey did not match expected value"
+ );
+ is(
+ evt.metaKey,
+ (test_expectedModifiers & Event.META_MASK) != 0,
+ "evt.metaKey did not match expected value"
+ );
+
+ if (evt.type == "MozTapGesture") {
+ is(
+ evt.clickCount,
+ test_expectedClickCount,
+ "evt.clickCount does not match"
+ );
+ }
+
+ test_eventCount++;
+}
+
+function test_helper1(type, direction, delta, modifiers) {
+ // Setup the expected values
+ test_expectedType = type;
+ test_expectedDirection = direction;
+ test_expectedDelta = delta;
+ test_expectedModifiers = modifiers;
+
+ let expectedEventCount = test_eventCount + 1;
+
+ document.addEventListener(type, test_gestureListener, true);
+ test_utils.sendSimpleGestureEvent(type, 60, 60, direction, delta, modifiers);
+ document.removeEventListener(type, test_gestureListener, true);
+
+ is(
+ expectedEventCount,
+ test_eventCount,
+ "Event (" + type + ") was never received by event listener"
+ );
+}
+
+function test_clicks(type, clicks) {
+ // Setup the expected values
+ test_expectedType = type;
+ test_expectedDirection = 0;
+ test_expectedDelta = 0;
+ test_expectedModifiers = 0;
+ test_expectedClickCount = clicks;
+
+ let expectedEventCount = test_eventCount + 1;
+
+ document.addEventListener(type, test_gestureListener, true);
+ test_utils.sendSimpleGestureEvent(type, 60, 60, 0, 0, 0, clicks);
+ document.removeEventListener(type, test_gestureListener, true);
+
+ is(
+ expectedEventCount,
+ test_eventCount,
+ "Event (" + type + ") was never received by event listener"
+ );
+}
+
+function test_TestEventListeners() {
+ let e = test_helper1; // easier to type this name
+
+ // Swipe gesture animation events
+ e("MozSwipeGestureStart", 0, -0.7, 0);
+ e("MozSwipeGestureUpdate", 0, -0.4, 0);
+ e("MozSwipeGestureEnd", 0, 0, 0);
+ e("MozSwipeGestureStart", 0, 0.6, 0);
+ e("MozSwipeGestureUpdate", 0, 0.3, 0);
+ e("MozSwipeGestureEnd", 0, 1, 0);
+
+ // Swipe gesture event
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0);
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0);
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_UP, 0.0, 0);
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_DOWN, 0.0, 0);
+ e(
+ "MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_UP | SimpleGestureEvent.DIRECTION_LEFT,
+ 0.0,
+ 0
+ );
+ e(
+ "MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_DOWN | SimpleGestureEvent.DIRECTION_RIGHT,
+ 0.0,
+ 0
+ );
+ e(
+ "MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_UP | SimpleGestureEvent.DIRECTION_RIGHT,
+ 0.0,
+ 0
+ );
+ e(
+ "MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_DOWN | SimpleGestureEvent.DIRECTION_LEFT,
+ 0.0,
+ 0
+ );
+
+ // magnify gesture events
+ e("MozMagnifyGestureStart", 0, 50.0, 0);
+ e("MozMagnifyGestureUpdate", 0, -25.0, 0);
+ e("MozMagnifyGestureUpdate", 0, 5.0, 0);
+ e("MozMagnifyGesture", 0, 30.0, 0);
+
+ // rotate gesture events
+ e("MozRotateGestureStart", SimpleGestureEvent.ROTATION_CLOCKWISE, 33.0, 0);
+ e(
+ "MozRotateGestureUpdate",
+ SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE,
+ -13.0,
+ 0
+ );
+ e("MozRotateGestureUpdate", SimpleGestureEvent.ROTATION_CLOCKWISE, 13.0, 0);
+ e("MozRotateGesture", SimpleGestureEvent.ROTATION_CLOCKWISE, 33.0, 0);
+
+ // Tap and presstap gesture events
+ test_clicks("MozTapGesture", 1);
+ test_clicks("MozTapGesture", 2);
+ test_clicks("MozTapGesture", 3);
+ test_clicks("MozPressTapGesture", 1);
+
+ // simple delivery test for edgeui gestures
+ e("MozEdgeUIStarted", 0, 0, 0);
+ e("MozEdgeUICanceled", 0, 0, 0);
+ e("MozEdgeUICompleted", 0, 0, 0);
+
+ // event.shiftKey
+ let modifier = Event.SHIFT_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+
+ // event.metaKey
+ modifier = Event.META_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+
+ // event.altKey
+ modifier = Event.ALT_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+
+ // event.ctrlKey
+ modifier = Event.CONTROL_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+}
+
+function test_eventDispatchListener(evt) {
+ test_eventCount++;
+ evt.stopPropagation();
+}
+
+function test_helper2(
+ type,
+ direction,
+ delta,
+ altKey,
+ ctrlKey,
+ shiftKey,
+ metaKey
+) {
+ let event = null;
+ let successful;
+
+ try {
+ event = document.createEvent("SimpleGestureEvent");
+ successful = true;
+ } catch (ex) {
+ successful = false;
+ }
+ ok(successful, "Unable to create SimpleGestureEvent");
+
+ try {
+ event.initSimpleGestureEvent(
+ type,
+ true,
+ true,
+ window,
+ 1,
+ 10,
+ 10,
+ 10,
+ 10,
+ ctrlKey,
+ altKey,
+ shiftKey,
+ metaKey,
+ 1,
+ window,
+ 0,
+ direction,
+ delta,
+ 0
+ );
+ successful = true;
+ } catch (ex) {
+ successful = false;
+ }
+ ok(successful, "event.initSimpleGestureEvent should not fail");
+
+ // Make sure the event fields match the expected values
+ is(event.type, type, "Mismatch on evt.type");
+ is(event.direction, direction, "Mismatch on evt.direction");
+ is(event.delta, delta, "Mismatch on evt.delta");
+ is(event.altKey, altKey, "Mismatch on evt.altKey");
+ is(event.ctrlKey, ctrlKey, "Mismatch on evt.ctrlKey");
+ is(event.shiftKey, shiftKey, "Mismatch on evt.shiftKey");
+ is(event.metaKey, metaKey, "Mismatch on evt.metaKey");
+ is(event.view, window, "Mismatch on evt.view");
+ is(event.detail, 1, "Mismatch on evt.detail");
+ is(event.clientX, 10, "Mismatch on evt.clientX");
+ is(event.clientY, 10, "Mismatch on evt.clientY");
+ is(event.screenX, 10, "Mismatch on evt.screenX");
+ is(event.screenY, 10, "Mismatch on evt.screenY");
+ is(event.button, 1, "Mismatch on evt.button");
+ is(event.relatedTarget, window, "Mismatch on evt.relatedTarget");
+
+ // Test event dispatch
+ let expectedEventCount = test_eventCount + 1;
+ document.addEventListener(type, test_eventDispatchListener, true);
+ document.dispatchEvent(event);
+ document.removeEventListener(type, test_eventDispatchListener, true);
+ is(
+ expectedEventCount,
+ test_eventCount,
+ "Dispatched event was never received by listener"
+ );
+}
+
+function test_TestEventCreation() {
+ // Event creation
+ test_helper2(
+ "MozMagnifyGesture",
+ SimpleGestureEvent.DIRECTION_RIGHT,
+ 20.0,
+ true,
+ false,
+ true,
+ false
+ );
+ test_helper2(
+ "MozMagnifyGesture",
+ SimpleGestureEvent.DIRECTION_LEFT,
+ -20.0,
+ false,
+ true,
+ false,
+ true
+ );
+}
+
+function test_EnsureConstantsAreDisjoint() {
+ let up = SimpleGestureEvent.DIRECTION_UP;
+ let down = SimpleGestureEvent.DIRECTION_DOWN;
+ let left = SimpleGestureEvent.DIRECTION_LEFT;
+ let right = SimpleGestureEvent.DIRECTION_RIGHT;
+
+ let clockwise = SimpleGestureEvent.ROTATION_CLOCKWISE;
+ let cclockwise = SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE;
+
+ ok(up ^ down, "DIRECTION_UP and DIRECTION_DOWN are not bitwise disjoint");
+ ok(up ^ left, "DIRECTION_UP and DIRECTION_LEFT are not bitwise disjoint");
+ ok(up ^ right, "DIRECTION_UP and DIRECTION_RIGHT are not bitwise disjoint");
+ ok(down ^ left, "DIRECTION_DOWN and DIRECTION_LEFT are not bitwise disjoint");
+ ok(
+ down ^ right,
+ "DIRECTION_DOWN and DIRECTION_RIGHT are not bitwise disjoint"
+ );
+ ok(
+ left ^ right,
+ "DIRECTION_LEFT and DIRECTION_RIGHT are not bitwise disjoint"
+ );
+ ok(
+ clockwise ^ cclockwise,
+ "ROTATION_CLOCKWISE and ROTATION_COUNTERCLOCKWISE are not bitwise disjoint"
+ );
+}
+
+// Helper for test of latched event processing. Emits the actual
+// gesture events to test whether the commands associated with the
+// gesture will only trigger once for each direction of movement.
+async function test_emitLatchedEvents(eventPrefix, initialDelta, cmd) {
+ let cumulativeDelta = 0;
+ let isIncreasing = initialDelta > 0;
+
+ let expect = {};
+ // Reset the call counters and initialize expected values
+ for (let dir in cmd) {
+ cmd[dir].callCount = expect[dir] = 0;
+ }
+
+ let check = (aDir, aMsg) => ok(cmd[aDir].callCount == expect[aDir], aMsg);
+ let checkBoth = function (aNum, aInc, aDec) {
+ let prefix = "Step " + aNum + ": ";
+ check("inc", prefix + aInc);
+ check("dec", prefix + aDec);
+ };
+
+ // Send the "Start" event.
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Start",
+ 10,
+ 10,
+ 0,
+ initialDelta,
+ 0,
+ 0
+ );
+ cumulativeDelta += initialDelta;
+ if (isIncreasing) {
+ expect.inc++;
+ checkBoth(
+ 1,
+ "Increasing command was not triggered",
+ "Decreasing command was triggered"
+ );
+ } else {
+ expect.dec++;
+ checkBoth(
+ 1,
+ "Increasing command was triggered",
+ "Decreasing command was not triggered"
+ );
+ }
+
+ // Send random values in the same direction and ensure neither
+ // command triggers.
+ for (let i = 0; i < 5; i++) {
+ let delta = Math.random() * (isIncreasing ? 100 : -100);
+
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Update",
+ 10,
+ 10,
+ 0,
+ delta,
+ 0,
+ 0
+ );
+ cumulativeDelta += delta;
+ checkBoth(
+ 2,
+ "Increasing command was triggered",
+ "Decreasing command was triggered"
+ );
+ }
+
+ // Now go back in the opposite direction.
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Update",
+ 10,
+ 10,
+ 0,
+ -initialDelta,
+ 0,
+ 0
+ );
+ cumulativeDelta += -initialDelta;
+ if (isIncreasing) {
+ expect.dec++;
+ checkBoth(
+ 3,
+ "Increasing command was triggered",
+ "Decreasing command was not triggered"
+ );
+ } else {
+ expect.inc++;
+ checkBoth(
+ 3,
+ "Increasing command was not triggered",
+ "Decreasing command was triggered"
+ );
+ }
+
+ // Send random values in the opposite direction and ensure neither
+ // command triggers.
+ for (let i = 0; i < 5; i++) {
+ let delta = Math.random() * (isIncreasing ? -100 : 100);
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Update",
+ 10,
+ 10,
+ 0,
+ delta,
+ 0,
+ 0
+ );
+ cumulativeDelta += delta;
+ checkBoth(
+ 4,
+ "Increasing command was triggered",
+ "Decreasing command was triggered"
+ );
+ }
+
+ // Go back to the original direction. The original command should trigger.
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Update",
+ 10,
+ 10,
+ 0,
+ initialDelta,
+ 0,
+ 0
+ );
+ cumulativeDelta += initialDelta;
+ if (isIncreasing) {
+ expect.inc++;
+ checkBoth(
+ 5,
+ "Increasing command was not triggered",
+ "Decreasing command was triggered"
+ );
+ } else {
+ expect.dec++;
+ checkBoth(
+ 5,
+ "Increasing command was triggered",
+ "Decreasing command was not triggered"
+ );
+ }
+
+ // Send the wrap-up event. No commands should be triggered.
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix,
+ 10,
+ 10,
+ 0,
+ cumulativeDelta,
+ 0,
+ 0
+ );
+ checkBoth(
+ 6,
+ "Increasing command was triggered",
+ "Decreasing command was triggered"
+ );
+}
+
+function test_addCommand(prefName, id) {
+ let cmd = test_commandset.appendChild(document.createXULElement("command"));
+ cmd.setAttribute("id", id);
+ cmd.setAttribute("oncommand", "this.callCount++;");
+
+ cmd.origPrefName = prefName;
+ cmd.origPrefValue = Services.prefs.getCharPref(prefName);
+ Services.prefs.setCharPref(prefName, id);
+
+ return cmd;
+}
+
+function test_removeCommand(cmd) {
+ Services.prefs.setCharPref(cmd.origPrefName, cmd.origPrefValue);
+ test_commandset.removeChild(cmd);
+}
+
+// Test whether latched events are only called once per direction of motion.
+async function test_latchedGesture(gesture, inc, dec, eventPrefix) {
+ let branch = test_prefBranch + gesture + ".";
+
+ // Put the gesture into latched mode.
+ let oldLatchedValue = Services.prefs.getBoolPref(branch + "latched");
+ Services.prefs.setBoolPref(branch + "latched", true);
+
+ // Install the test commands for increasing and decreasing motion.
+ let cmd = {
+ inc: test_addCommand(branch + inc, "test:incMotion"),
+ dec: test_addCommand(branch + dec, "test:decMotion"),
+ };
+
+ // Test the gestures in each direction.
+ await test_emitLatchedEvents(eventPrefix, 500, cmd);
+ await test_emitLatchedEvents(eventPrefix, -500, cmd);
+
+ // Restore the gesture to its original configuration.
+ Services.prefs.setBoolPref(branch + "latched", oldLatchedValue);
+ for (let dir in cmd) {
+ test_removeCommand(cmd[dir]);
+ }
+}
+
+// Test whether non-latched events are triggered upon sufficient motion.
+async function test_thresholdGesture(gesture, inc, dec, eventPrefix) {
+ let branch = test_prefBranch + gesture + ".";
+
+ // Disable latched mode for this gesture.
+ let oldLatchedValue = Services.prefs.getBoolPref(branch + "latched");
+ Services.prefs.setBoolPref(branch + "latched", false);
+
+ // Set the triggering threshold value to 50.
+ let oldThresholdValue = Services.prefs.getIntPref(branch + "threshold");
+ Services.prefs.setIntPref(branch + "threshold", 50);
+
+ // Install the test commands for increasing and decreasing motion.
+ let cmdInc = test_addCommand(branch + inc, "test:incMotion");
+ let cmdDec = test_addCommand(branch + dec, "test:decMotion");
+
+ // Send the start event but stop short of triggering threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Start",
+ 10,
+ 10,
+ 0,
+ 49.5,
+ 0,
+ 0
+ );
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Now trigger the threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Update",
+ 10,
+ 10,
+ 0,
+ 1,
+ 0,
+ 0
+ );
+ ok(cmdInc.callCount == 1, "Increasing command was not triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // The tracking counter should go to zero. Go back the other way and
+ // stop short of triggering the threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Update",
+ 10,
+ 10,
+ 0,
+ -49.5,
+ 0,
+ 0
+ );
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Now cross the threshold and trigger the decreasing command.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix + "Update",
+ 10,
+ 10,
+ 0,
+ -1.5,
+ 0,
+ 0
+ );
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 1, "Decreasing command was not triggered");
+
+ // Send the wrap-up event. No commands should trigger.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ eventPrefix,
+ 0,
+ 0,
+ 0,
+ -0.5,
+ 0,
+ 0
+ );
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Restore the gesture to its original configuration.
+ Services.prefs.setBoolPref(branch + "latched", oldLatchedValue);
+ Services.prefs.setIntPref(branch + "threshold", oldThresholdValue);
+ test_removeCommand(cmdInc);
+ test_removeCommand(cmdDec);
+}
+
+async function test_swipeGestures() {
+ // easier to type names for the direction constants
+ let up = SimpleGestureEvent.DIRECTION_UP;
+ let down = SimpleGestureEvent.DIRECTION_DOWN;
+ let left = SimpleGestureEvent.DIRECTION_LEFT;
+ let right = SimpleGestureEvent.DIRECTION_RIGHT;
+
+ let branch = test_prefBranch + "swipe.";
+
+ // Install the test commands for the swipe gestures.
+ let cmdUp = test_addCommand(branch + "up", "test:swipeUp");
+ let cmdDown = test_addCommand(branch + "down", "test:swipeDown");
+ let cmdLeft = test_addCommand(branch + "left", "test:swipeLeft");
+ let cmdRight = test_addCommand(branch + "right", "test:swipeRight");
+
+ function resetCounts() {
+ cmdUp.callCount = 0;
+ cmdDown.callCount = 0;
+ cmdLeft.callCount = 0;
+ cmdRight.callCount = 0;
+ }
+
+ // UP
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ up,
+ 0,
+ 0,
+ 0
+ );
+ ok(cmdUp.callCount == 1, "Step 1: Up command was not triggered");
+ ok(cmdDown.callCount == 0, "Step 1: Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 1: Left command was triggered");
+ ok(cmdRight.callCount == 0, "Step 1: Right command was triggered");
+
+ // DOWN
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ down,
+ 0,
+ 0,
+ 0
+ );
+ ok(cmdUp.callCount == 0, "Step 2: Up command was triggered");
+ ok(cmdDown.callCount == 1, "Step 2: Down command was not triggered");
+ ok(cmdLeft.callCount == 0, "Step 2: Left command was triggered");
+ ok(cmdRight.callCount == 0, "Step 2: Right command was triggered");
+
+ // LEFT
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ left,
+ 0,
+ 0,
+ 0
+ );
+ ok(cmdUp.callCount == 0, "Step 3: Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 3: Down command was triggered");
+ ok(cmdLeft.callCount == 1, "Step 3: Left command was not triggered");
+ ok(cmdRight.callCount == 0, "Step 3: Right command was triggered");
+
+ // RIGHT
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ right,
+ 0,
+ 0,
+ 0
+ );
+ ok(cmdUp.callCount == 0, "Step 4: Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 4: Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 4: Left command was triggered");
+ ok(cmdRight.callCount == 1, "Step 4: Right command was not triggered");
+
+ // Make sure combinations do not trigger events.
+ let combos = [up | left, up | right, down | left, down | right];
+ for (let i = 0; i < combos.length; i++) {
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ combos[i],
+ 0,
+ 0,
+ 0
+ );
+ ok(cmdUp.callCount == 0, "Step 5-" + i + ": Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 5-" + i + ": Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 5-" + i + ": Left command was triggered");
+ ok(
+ cmdRight.callCount == 0,
+ "Step 5-" + i + ": Right command was triggered"
+ );
+ }
+
+ // Remove the test commands.
+ test_removeCommand(cmdUp);
+ test_removeCommand(cmdDown);
+ test_removeCommand(cmdLeft);
+ test_removeCommand(cmdRight);
+}
+
+function test_rotateHelperGetImageRotation(aImageElement) {
+ // Get the true image rotation from the transform matrix, bounded
+ // to 0 <= result < 360
+ let transformValue = content.window.getComputedStyle(aImageElement).transform;
+ if (transformValue == "none") {
+ return 0;
+ }
+
+ transformValue = transformValue.split("(")[1].split(")")[0].split(",");
+ var rotation = Math.round(
+ Math.atan2(transformValue[1], transformValue[0]) * (180 / Math.PI)
+ );
+ return rotation < 0 ? rotation + 360 : rotation;
+}
+
+async function test_rotateHelperOneGesture(
+ aImageElement,
+ aCurrentRotation,
+ aDirection,
+ aAmount,
+ aStop
+) {
+ if (aAmount <= 0 || aAmount > 90) {
+ // Bound to 0 < aAmount <= 90
+ return;
+ }
+
+ // easier to type names for the direction constants
+ let clockwise = SimpleGestureEvent.ROTATION_CLOCKWISE;
+
+ let delta = aAmount * (aDirection == clockwise ? 1 : -1);
+
+ // Kill transition time on image so test isn't wrong and doesn't take 10 seconds
+ aImageElement.style.transitionDuration = "0s";
+
+ // Start the gesture, perform an update, and force flush
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGestureStart",
+ 10,
+ 10,
+ aDirection,
+ 0.001,
+ 0,
+ 0
+ );
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGestureUpdate",
+ 10,
+ 10,
+ aDirection,
+ delta,
+ 0,
+ 0
+ );
+ aImageElement.clientTop;
+
+ // If stop, check intermediate
+ if (aStop) {
+ // Send near-zero-delta to stop, and force flush
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGestureUpdate",
+ 10,
+ 10,
+ aDirection,
+ 0.001,
+ 0,
+ 0
+ );
+ aImageElement.clientTop;
+
+ let stopExpectedRotation = (aCurrentRotation + delta) % 360;
+ if (stopExpectedRotation < 0) {
+ stopExpectedRotation += 360;
+ }
+
+ is(
+ stopExpectedRotation,
+ test_rotateHelperGetImageRotation(aImageElement),
+ "Image rotation at gesture stop/hold: expected=" +
+ stopExpectedRotation +
+ ", observed=" +
+ test_rotateHelperGetImageRotation(aImageElement) +
+ ", init=" +
+ aCurrentRotation +
+ ", amt=" +
+ aAmount +
+ ", dir=" +
+ (aDirection == clockwise ? "cl" : "ccl")
+ );
+ }
+ // End it and force flush
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGesture",
+ 10,
+ 10,
+ aDirection,
+ 0,
+ 0,
+ 0
+ );
+ aImageElement.clientTop;
+
+ let finalExpectedRotation;
+
+ if (aAmount < 45 && aStop) {
+ // Rotate a bit, then stop. Expect no change at end of gesture.
+ finalExpectedRotation = aCurrentRotation;
+ } else {
+ // Either not stopping (expect 90 degree change in aDirection), OR
+ // stopping but after 45, (expect 90 degree change in aDirection)
+ finalExpectedRotation =
+ (aCurrentRotation + (aDirection == clockwise ? 1 : -1) * 90) % 360;
+ if (finalExpectedRotation < 0) {
+ finalExpectedRotation += 360;
+ }
+ }
+
+ is(
+ finalExpectedRotation,
+ test_rotateHelperGetImageRotation(aImageElement),
+ "Image rotation gesture end: expected=" +
+ finalExpectedRotation +
+ ", observed=" +
+ test_rotateHelperGetImageRotation(aImageElement) +
+ ", init=" +
+ aCurrentRotation +
+ ", amt=" +
+ aAmount +
+ ", dir=" +
+ (aDirection == clockwise ? "cl" : "ccl")
+ );
+}
+
+async function test_rotateGesturesOnTab() {
+ gBrowser.selectedBrowser.removeEventListener(
+ "load",
+ test_rotateGesturesOnTab,
+ true
+ );
+
+ if (!ImageDocument.isInstance(content.document)) {
+ ok(false, "Image document failed to open for rotation testing");
+ gBrowser.removeTab(test_imageTab);
+ BrowserTestUtils.removeTab(test_normalTab);
+ test_imageTab = null;
+ test_normalTab = null;
+ finish();
+ return;
+ }
+
+ // easier to type names for the direction constants
+ let cl = SimpleGestureEvent.ROTATION_CLOCKWISE;
+ let ccl = SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE;
+
+ let imgElem =
+ content.document.body && content.document.body.firstElementChild;
+
+ if (!imgElem) {
+ ok(false, "Could not get image element on ImageDocument for rotation!");
+ gBrowser.removeTab(test_imageTab);
+ BrowserTestUtils.removeTab(test_normalTab);
+ test_imageTab = null;
+ test_normalTab = null;
+ finish();
+ return;
+ }
+
+ // Quick function to normalize rotation to 0 <= r < 360
+ var normRot = function (rotation) {
+ rotation = rotation % 360;
+ if (rotation < 0) {
+ rotation += 360;
+ }
+ return rotation;
+ };
+
+ for (var initRot = 0; initRot < 360; initRot += 90) {
+ // Test each case: at each 90 degree snap; cl/ccl;
+ // amount more or less than 45; stop and hold or don't (32 total tests)
+ // The amount added to the initRot is where it is expected to be
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 0),
+ cl,
+ 35,
+ true
+ );
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 0),
+ cl,
+ 35,
+ false
+ );
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 90),
+ cl,
+ 55,
+ true
+ );
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 180),
+ cl,
+ 55,
+ false
+ );
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 270),
+ ccl,
+ 35,
+ true
+ );
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 270),
+ ccl,
+ 35,
+ false
+ );
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 180),
+ ccl,
+ 55,
+ true
+ );
+ await test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 90),
+ ccl,
+ 55,
+ false
+ );
+
+ // Manually rotate it 90 degrees clockwise to prepare for next iteration,
+ // and force flush
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGestureStart",
+ 10,
+ 10,
+ cl,
+ 0.001,
+ 0,
+ 0
+ );
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGestureUpdate",
+ 10,
+ 10,
+ cl,
+ 90,
+ 0,
+ 0
+ );
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGestureUpdate",
+ 10,
+ 10,
+ cl,
+ 0.001,
+ 0,
+ 0
+ );
+ await synthesizeSimpleGestureEvent(
+ test_imageTab.linkedBrowser,
+ "MozRotateGesture",
+ 10,
+ 10,
+ cl,
+ 0,
+ 0,
+ 0
+ );
+ imgElem.clientTop;
+ }
+
+ gBrowser.removeTab(test_imageTab);
+ BrowserTestUtils.removeTab(test_normalTab);
+ test_imageTab = null;
+ test_normalTab = null;
+ finish();
+}
+
+function test_rotateGestures() {
+ test_imageTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "chrome://branding/content/about-logo.png"
+ );
+ gBrowser.selectedTab = test_imageTab;
+
+ gBrowser.selectedBrowser.addEventListener(
+ "load",
+ test_rotateGesturesOnTab,
+ true
+ );
+}
diff --git a/browser/base/content/test/general/browser_hide_removing.js b/browser/base/content/test/general/browser_hide_removing.js
new file mode 100644
index 0000000000..24079c22e6
--- /dev/null
+++ b/browser/base/content/test/general/browser_hide_removing.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Bug 587922: tabs don't get removed if they're hidden
+
+add_task(async function () {
+ // Add a tab that will get removed and hidden
+ let testTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ is(gBrowser.visibleTabs.length, 2, "just added a tab, so 2 tabs");
+ await BrowserTestUtils.switchTab(gBrowser, testTab);
+
+ let numVisBeforeHide, numVisAfterHide;
+
+ // We have to animate the tab removal in order to get an async
+ // tab close.
+ BrowserTestUtils.removeTab(testTab, { animate: true });
+
+ numVisBeforeHide = gBrowser.visibleTabs.length;
+ gBrowser.hideTab(testTab);
+ numVisAfterHide = gBrowser.visibleTabs.length;
+
+ is(numVisBeforeHide, 1, "animated remove has in 1 tab left");
+ is(numVisAfterHide, 1, "hiding a removing tab also has 1 tab");
+});
diff --git a/browser/base/content/test/general/browser_homeDrop.js b/browser/base/content/test/general/browser_homeDrop.js
new file mode 100644
index 0000000000..81dc48d3e4
--- /dev/null
+++ b/browser/base/content/test/general/browser_homeDrop.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setupHomeButton() {
+ // Put the home button in the pre-proton placement to test focus states.
+ CustomizableUI.addWidgetToArea(
+ "home-button",
+ "nav-bar",
+ CustomizableUI.getPlacementOfWidget("stop-reload-button").position + 1
+ );
+ CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
+ registerCleanupFunction(async function resetToolbar() {
+ await CustomizableUI.reset();
+ });
+});
+
+add_task(async function () {
+ let HOMEPAGE_PREF = "browser.startup.homepage";
+
+ await pushPrefs([HOMEPAGE_PREF, "about:mozilla"]);
+
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button
+ // that should be visible.
+ let dragSrcElement = document.getElementById("sidebar-button");
+ ok(dragSrcElement, "Sidebar button exists");
+ let homeButton = document.getElementById("home-button");
+ ok(homeButton, "home button present");
+
+ async function drop(dragData, homepage) {
+ let setHomepageDialogPromise =
+ BrowserTestUtils.promiseAlertDialogOpen("accept");
+ let setHomepagePromise = TestUtils.waitForPrefChange(
+ HOMEPAGE_PREF,
+ newVal => newVal == homepage
+ );
+
+ EventUtils.synthesizeDrop(
+ dragSrcElement,
+ homeButton,
+ dragData,
+ "copy",
+ window
+ );
+
+ // Ensure dnd suppression is cleared.
+ EventUtils.synthesizeMouseAtCenter(homeButton, { type: "mouseup" }, window);
+
+ await setHomepageDialogPromise;
+ ok(true, "dialog appeared in response to home button drop");
+
+ await setHomepagePromise;
+
+ let modified = Services.prefs.getStringPref(HOMEPAGE_PREF);
+ is(modified, homepage, "homepage is set correctly");
+ Services.prefs.setStringPref(HOMEPAGE_PREF, "about:mozilla;");
+ }
+
+ function dropInvalidURI() {
+ return new Promise(resolve => {
+ let consoleListener = {
+ observe(m) {
+ if (m.message.includes("NS_ERROR_DOM_BAD_URI")) {
+ ok(true, "drop was blocked");
+ resolve();
+ }
+ },
+ };
+ Services.console.registerListener(consoleListener);
+ registerCleanupFunction(function () {
+ Services.console.unregisterListener(consoleListener);
+ });
+
+ executeSoon(function () {
+ info("Attempting second drop, of a javascript: URI");
+ // The drop handler throws an exception when dragging URIs that inherit
+ // principal, e.g. javascript:
+ expectUncaughtException();
+ EventUtils.synthesizeDrop(
+ dragSrcElement,
+ homeButton,
+ [[{ type: "text/plain", data: "javascript:8888" }]],
+ "copy",
+ window
+ );
+ // Ensure dnd suppression is cleared.
+ EventUtils.synthesizeMouseAtCenter(
+ homeButton,
+ { type: "mouseup" },
+ window
+ );
+ });
+ });
+ }
+
+ await drop(
+ [[{ type: "text/plain", data: "http://mochi.test:8888/" }]],
+ "http://mochi.test:8888/"
+ );
+ await drop(
+ [
+ [
+ {
+ type: "text/plain",
+ data: "http://mochi.test:8888/\nhttp://mochi.test:8888/b\nhttp://mochi.test:8888/c",
+ },
+ ],
+ ],
+ "http://mochi.test:8888/|http://mochi.test:8888/b|http://mochi.test:8888/c"
+ );
+ await dropInvalidURI();
+});
diff --git a/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js b/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js
new file mode 100644
index 0000000000..1624a1514d
--- /dev/null
+++ b/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js
@@ -0,0 +1,48 @@
+"use strict";
+
+/**
+ * Verify that loading an invalid URI does not clobber a previously-loaded page's history
+ * entry, but that the invalid URI gets its own history entry instead. We're checking this
+ * using nsIWebNavigation's canGoBack, as well as actually going back and then checking
+ * canGoForward.
+ */
+add_task(async function checkBackFromInvalidURI() {
+ await pushPrefs(["keyword.enabled", false]);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots",
+ true
+ );
+ info("Loaded about:robots");
+
+ gURLBar.value = "::2600";
+
+ let promiseErrorPageLoaded = BrowserTestUtils.waitForErrorPage(
+ tab.linkedBrowser
+ );
+ gURLBar.handleCommand();
+ await promiseErrorPageLoaded;
+
+ ok(gBrowser.webNavigation.canGoBack, "Should be able to go back");
+ if (gBrowser.webNavigation.canGoBack) {
+ // Can't use DOMContentLoaded here because the page is bfcached. Can't use pageshow for
+ // the error page because it doesn't seem to fire for those.
+ let promiseOtherPageLoaded = BrowserTestUtils.waitForEvent(
+ tab.linkedBrowser,
+ "pageshow",
+ false,
+ // Be paranoid we *are* actually seeing this other page load, not some kind of race
+ // for if/when we do start firing pageshow for the error page...
+ function (e) {
+ return gBrowser.currentURI.spec == "about:robots";
+ }
+ );
+ gBrowser.goBack();
+ await promiseOtherPageLoaded;
+ ok(
+ gBrowser.webNavigation.canGoForward,
+ "Should be able to go forward from previous page."
+ );
+ }
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_lastAccessedTab.js b/browser/base/content/test/general/browser_lastAccessedTab.js
new file mode 100644
index 0000000000..631fcb3bfe
--- /dev/null
+++ b/browser/base/content/test/general/browser_lastAccessedTab.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// gBrowser.selectedTab.lastAccessed and Date.now() called from this test can't
+// run concurrently, and therefore don't always match exactly.
+const CURRENT_TIME_TOLERANCE_MS = 15;
+
+function isCurrent(tab, msg) {
+ const DIFF = Math.abs(Date.now() - tab.lastAccessed);
+ ok(DIFF <= CURRENT_TIME_TOLERANCE_MS, msg + " (difference: " + DIFF + ")");
+}
+
+function nextStep(fn) {
+ setTimeout(fn, CURRENT_TIME_TOLERANCE_MS + 10);
+}
+
+var originalTab;
+var newTab;
+
+function test() {
+ waitForExplicitFinish();
+ // This test assumes that time passes between operations. But if the precision
+ // is low enough, and the test fast enough, an operation, and a successive call
+ // to Date.now() will have the same time value.
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.reduceTimerPrecision", false]] },
+ function () {
+ originalTab = gBrowser.selectedTab;
+ nextStep(step2);
+ }
+ );
+}
+
+function step2() {
+ isCurrent(originalTab, "selected tab has the current timestamp");
+ newTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ nextStep(step3);
+}
+
+function step3() {
+ ok(newTab.lastAccessed < Date.now(), "new tab hasn't been selected so far");
+ gBrowser.selectedTab = newTab;
+ isCurrent(newTab, "new tab has the current timestamp after being selected");
+ nextStep(step4);
+}
+
+function step4() {
+ ok(
+ originalTab.lastAccessed < Date.now(),
+ "original tab has old timestamp after being deselected"
+ );
+ isCurrent(
+ newTab,
+ "new tab has the current timestamp since it's still selected"
+ );
+
+ gBrowser.removeTab(newTab);
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_menuButtonFitts.js b/browser/base/content/test/general/browser_menuButtonFitts.js
new file mode 100644
index 0000000000..f56f46eb6c
--- /dev/null
+++ b/browser/base/content/test/general/browser_menuButtonFitts.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+function getNavBarEndPosition() {
+ let navBar = document.getElementById("nav-bar");
+ let boundingRect = navBar.getBoundingClientRect();
+
+ // Find where the nav-bar is vertically.
+ let y = boundingRect.top + Math.floor(boundingRect.height / 2);
+ // Use the last pixel of the screen since it is maximized.
+ let x = boundingRect.width - 1;
+ return { x, y };
+}
+
+/**
+ * Clicking the right end of a maximized window should open the hamburger menu.
+ */
+add_task(async function test_clicking_hamburger_edge_fitts() {
+ if (window.windowState != window.STATE_MAXIMIZED) {
+ info(`Waiting for maximize, current state: ${window.windowState}`);
+ let resizeDone = BrowserTestUtils.waitForEvent(
+ window,
+ "resize",
+ false,
+ () => window.outerWidth >= screen.width - 1
+ );
+ let maximizeDone = BrowserTestUtils.waitForEvent(window, "sizemodechange");
+ window.maximize();
+ await maximizeDone;
+ await resizeDone;
+ }
+
+ is(window.windowState, window.STATE_MAXIMIZED, "should be maximized");
+
+ let { x, y } = getNavBarEndPosition();
+ info(`Clicking in ${x}, ${y}`);
+
+ let popupHiddenResolve;
+ let popupHiddenPromise = new Promise(resolve => {
+ popupHiddenResolve = resolve;
+ });
+ async function onPopupHidden() {
+ PanelUI.panel.removeEventListener("popuphidden", onPopupHidden);
+
+ info("Waiting for restore");
+
+ let restoreDone = BrowserTestUtils.waitForEvent(window, "sizemodechange");
+ window.restore();
+ await restoreDone;
+
+ popupHiddenResolve();
+ }
+ function onPopupShown() {
+ PanelUI.panel.removeEventListener("popupshown", onPopupShown);
+ ok(true, "Clicking at the far edge of the window opened the menu popup.");
+ PanelUI.panel.addEventListener("popuphidden", onPopupHidden);
+ PanelUI.hide();
+ }
+ registerCleanupFunction(function () {
+ PanelUI.panel.removeEventListener("popupshown", onPopupShown);
+ PanelUI.panel.removeEventListener("popuphidden", onPopupHidden);
+ });
+ PanelUI.panel.addEventListener("popupshown", onPopupShown);
+ EventUtils.synthesizeMouseAtPoint(x, y, {}, window);
+ await popupHiddenPromise;
+});
diff --git a/browser/base/content/test/general/browser_middleMouse_noJSPaste.js b/browser/base/content/test/general/browser_middleMouse_noJSPaste.js
new file mode 100644
index 0000000000..f023b78909
--- /dev/null
+++ b/browser/base/content/test/general/browser_middleMouse_noJSPaste.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const middleMousePastePref = "middlemouse.contentLoadURL";
+const autoScrollPref = "general.autoScroll";
+
+add_task(async function () {
+ await pushPrefs([middleMousePastePref, true], [autoScrollPref, false]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let url = "javascript:http://www.example.com/";
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ url,
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(url);
+ },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+
+ let middlePagePromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Middle click on the content area
+ info("Middle clicking");
+ await BrowserTestUtils.synthesizeMouse(
+ null,
+ 10,
+ 10,
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ await middlePagePromise;
+
+ is(
+ gBrowser.currentURI.spec,
+ url.replace(/^javascript:/, ""),
+ "url loaded by middle click doesn't include JS"
+ );
+
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_minimize.js b/browser/base/content/test/general/browser_minimize.js
new file mode 100644
index 0000000000..a57fea079c
--- /dev/null
+++ b/browser/base/content/test/general/browser_minimize.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ registerCleanupFunction(function () {
+ window.restore();
+ });
+ function isActive() {
+ return gBrowser.selectedTab.linkedBrowser.docShellIsActive;
+ }
+
+ ok(isActive(), "Docshell should be active when starting the test");
+ ok(!document.hidden, "Top level window should be visible");
+
+ info("Calling window.minimize");
+ let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ window.minimize();
+ await promiseSizeModeChange;
+ ok(!isActive(), "Docshell should be Inactive");
+ ok(document.hidden, "Top level window should be hidden");
+
+ info("Calling window.restore");
+ promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ window.restore();
+ // On Ubuntu `window.restore` doesn't seem to work, use a timer to make the
+ // test fail faster and more cleanly than with a test timeout.
+ await Promise.race([
+ promiseSizeModeChange,
+ new Promise((resolve, reject) =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ reject("timed out waiting for sizemodechange event");
+ }, 5000)
+ ),
+ ]);
+ // The sizemodechange event can sometimes be fired before the
+ // occlusionstatechange event, especially in chaos mode.
+ if (window.isFullyOccluded) {
+ await BrowserTestUtils.waitForEvent(window, "occlusionstatechange");
+ }
+ ok(isActive(), "Docshell should be active again");
+ ok(!document.hidden, "Top level window should be visible");
+});
diff --git a/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js b/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
new file mode 100644
index 0000000000..be3de519d6
--- /dev/null
+++ b/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const kURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+("data:text/html,<a href=''>Middle-click me</a>");
+
+/*
+ * Check that when manually opening content JS links in new tabs/windows,
+ * we use the correct principal, and we don't clear the URL bar.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(kURL, async function (browser) {
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ await SpecialPowers.spawn(browser, [], async function () {
+ let a = content.document.createElement("a");
+ // newTabPromise won't resolve until it has a URL that's not "about:blank".
+ // But doing document.open() from inside that same document does not change
+ // the URL of the docshell. So we need to do some URL change to cause
+ // newTabPromise to resolve, since the document is at about:blank the whole
+ // time, URL-wise. Navigating to '#' should do the trick without changing
+ // anything else about the document involved.
+ a.href =
+ "javascript:document.write('spoof'); location.href='#'; void(0);";
+ a.textContent = "Some link";
+ content.document.body.appendChild(a);
+ });
+ info("Added element");
+ await BrowserTestUtils.synthesizeMouseAtCenter("a", { button: 1 }, browser);
+ let newTab = await newTabPromise;
+ is(
+ newTab.linkedBrowser.contentPrincipal.origin,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ "Principal should be for example.com"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+ info(gURLBar.value);
+ isnot(gURLBar.value, "", "URL bar should not be empty.");
+ BrowserTestUtils.removeTab(newTab);
+ });
+});
diff --git a/browser/base/content/test/general/browser_newTabDrop.js b/browser/base/content/test/general/browser_newTabDrop.js
new file mode 100644
index 0000000000..d0e7f35c1f
--- /dev/null
+++ b/browser/base/content/test/general/browser_newTabDrop.js
@@ -0,0 +1,221 @@
+const ANY_URL = undefined;
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+registerCleanupFunction(async function cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+});
+
+add_task(async function test_setup() {
+ // This test opens multiple tabs and some confirm dialogs, that takes long.
+ requestLongerTimeout(2);
+
+ // Stop search-engine loads from hitting the network
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://example.com/",
+ search_url_get_params: "q={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+});
+
+// New Tab Button opens any link.
+add_task(async function single_url() {
+ await dropText("mochi.test/first", ["http://mochi.test/first"]);
+});
+add_task(async function single_url2() {
+ await dropText("mochi.test/second", ["http://mochi.test/second"]);
+});
+add_task(async function single_url3() {
+ await dropText("mochi.test/third", ["http://mochi.test/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("www.mochi.test/1\nmochi.test/2", [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.mochi.test/1",
+ "http://mochi.test/2",
+ ]);
+});
+
+// Multiple text/plain items, with single and multiple links.
+add_task(async function multiple_items_single_and_multiple_links() {
+ await drop(
+ [
+ [{ type: "text/plain", data: "mochi.test/5" }],
+ [{ type: "text/plain", data: "mochi.test/6\nmochi.test/7" }],
+ ],
+ ["http://mochi.test/5", "http://mochi.test/6", "http://mochi.test/7"]
+ );
+});
+
+// Single text/x-moz-url item, with multiple links.
+// "text/x-moz-url" has titles in even-numbered lines.
+add_task(async function single_moz_url_multiple_links() {
+ await drop(
+ [
+ [
+ {
+ type: "text/x-moz-url",
+ data: "mochi.test/8\nTITLE8\nmochi.test/9\nTITLE9",
+ },
+ ],
+ ],
+ ["http://mochi.test/8", "http://mochi.test/9"]
+ );
+});
+
+// Single item with multiple types.
+add_task(async function single_item_multiple_types() {
+ await drop(
+ [
+ [
+ { type: "text/plain", data: "mochi.test/10" },
+ { type: "text/x-moz-url", data: "mochi.test/11\nTITLE11" },
+ ],
+ ],
+ ["http://mochi.test/11"]
+ );
+});
+
+// Warn when too many URLs are dropped.
+add_task(async function multiple_tabs_under_max() {
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/multi" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://mochi.test/multi0",
+ "http://mochi.test/multi1",
+ "http://mochi.test/multi2",
+ "http://mochi.test/multi3",
+ "http://mochi.test/multi4",
+ ]);
+});
+add_task(async function multiple_tabs_over_max_accept() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("accept");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/accept" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://mochi.test/accept0",
+ "http://mochi.test/accept1",
+ "http://mochi.test/accept2",
+ "http://mochi.test/accept3",
+ "http://mochi.test/accept4",
+ ]);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+add_task(async function multiple_tabs_over_max_cancel() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/cancel" + i);
+ }
+ await dropText(urls.join("\n"), []);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+
+// Open URLs ignoring non-URL.
+add_task(async function multiple_urls() {
+ await dropText(
+ `
+ mochi.test/urls0
+ mochi.test/urls1
+ mochi.test/urls2
+ non url0
+ mochi.test/urls3
+ non url1
+ non url2
+`,
+ [
+ "http://mochi.test/urls0",
+ "http://mochi.test/urls1",
+ "http://mochi.test/urls2",
+ "http://mochi.test/urls3",
+ ]
+ );
+});
+
+// Open single search if there's no URL.
+add_task(async function multiple_text() {
+ await dropText(
+ `
+ non url0
+ non url1
+ non url2
+`,
+ [ANY_URL]
+ );
+});
+
+function dropText(text, expectedURLs) {
+ return drop([[{ type: "text/plain", data: text }]], expectedURLs);
+}
+
+async function drop(dragData, expectedURLs) {
+ let dragDataString = JSON.stringify(dragData);
+ info(
+ `Starting test for dragData:${dragDataString}; expectedURLs.length:${expectedURLs.length}`
+ );
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button
+ // that should be visible.
+ let dragSrcElement = document.getElementById("back-button");
+ ok(dragSrcElement, "Back button exists");
+ let newTabButton = document.getElementById(
+ gBrowser.tabContainer.hasAttribute("overflow")
+ ? "new-tab-button"
+ : "tabs-newtab-button"
+ );
+ ok(newTabButton, "New Tab button exists");
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(newTabButton, "drop");
+
+ let loadedPromises = expectedURLs.map(url =>
+ BrowserTestUtils.waitForNewTab(gBrowser, url, false, true)
+ );
+
+ EventUtils.synthesizeDrop(
+ dragSrcElement,
+ newTabButton,
+ dragData,
+ "link",
+ window
+ );
+
+ let tabs = await Promise.all(loadedPromises);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ await awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_newWindowDrop.js b/browser/base/content/test/general/browser_newWindowDrop.js
new file mode 100644
index 0000000000..243b691873
--- /dev/null
+++ b/browser/base/content/test/general/browser_newWindowDrop.js
@@ -0,0 +1,230 @@
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+add_task(async function test_setup() {
+ // Opening multiple windows on debug build takes too long time.
+ requestLongerTimeout(10);
+
+ // Stop search-engine loads from hitting the network
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://example.com/",
+ search_url_get_params: "q={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+
+ // Move New Window button to nav bar, to make it possible to drag and drop.
+ let { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+ );
+ let origPlacement = CustomizableUI.getPlacementOfWidget("new-window-button");
+ if (!origPlacement || origPlacement.area != CustomizableUI.AREA_NAVBAR) {
+ CustomizableUI.addWidgetToArea(
+ "new-window-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ CustomizableUI.ensureWidgetPlacedInWindow("new-window-button", window);
+ registerCleanupFunction(function () {
+ CustomizableUI.removeWidgetFromArea("new-window-button");
+ });
+ }
+
+ CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("sidebar-button")
+ );
+});
+
+// New Window Button opens any link.
+add_task(async function single_url() {
+ await dropText("mochi.test/first", ["http://mochi.test/first"]);
+});
+add_task(async function single_javascript() {
+ await dropText("javascript:'bad'", ["about:blank"]);
+});
+add_task(async function single_javascript_capital() {
+ await dropText("jAvascript:'bad'", ["about:blank"]);
+});
+add_task(async function single_url2() {
+ await dropText("mochi.test/second", ["http://mochi.test/second"]);
+});
+add_task(async function single_data_url() {
+ await dropText("data:text/html,bad", ["data:text/html,bad"]);
+});
+add_task(async function single_url3() {
+ await dropText("mochi.test/third", ["http://mochi.test/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("mochi.test/1\nmochi.test/2", [
+ "http://mochi.test/1",
+ "http://mochi.test/2",
+ ]);
+});
+add_task(async function multiple_urls_javascript() {
+ await dropText("javascript:'bad1'\nmochi.test/3", [
+ "about:blank",
+ "http://mochi.test/3",
+ ]);
+});
+add_task(async function multiple_urls_data() {
+ await dropText("mochi.test/4\ndata:text/html,bad1", [
+ "http://mochi.test/4",
+ "data:text/html,bad1",
+ ]);
+});
+
+// Multiple text/plain items, with single and multiple links.
+add_task(async function multiple_items_single_and_multiple_links() {
+ await drop(
+ [
+ [{ type: "text/plain", data: "mochi.test/5" }],
+ [{ type: "text/plain", data: "mochi.test/6\nmochi.test/7" }],
+ ],
+ ["http://mochi.test/5", "http://mochi.test/6", "http://mochi.test/7"]
+ );
+});
+
+// Single text/x-moz-url item, with multiple links.
+// "text/x-moz-url" has titles in even-numbered lines.
+add_task(async function single_moz_url_multiple_links() {
+ await drop(
+ [
+ [
+ {
+ type: "text/x-moz-url",
+ data: "mochi.test/8\nTITLE8\nmochi.test/9\nTITLE9",
+ },
+ ],
+ ],
+ ["http://mochi.test/8", "http://mochi.test/9"]
+ );
+});
+
+// Single item with multiple types.
+add_task(async function single_item_multiple_types() {
+ await drop(
+ [
+ [
+ { type: "text/plain", data: "mochi.test/10" },
+ { type: "text/x-moz-url", data: "mochi.test/11\nTITLE11" },
+ ],
+ ],
+ ["http://mochi.test/11"]
+ );
+});
+
+// Warn when too many URLs are dropped.
+add_task(async function multiple_tabs_under_max() {
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/multi" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://mochi.test/multi0",
+ "http://mochi.test/multi1",
+ "http://mochi.test/multi2",
+ "http://mochi.test/multi3",
+ "http://mochi.test/multi4",
+ ]);
+});
+add_task(async function multiple_tabs_over_max_accept() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("accept");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/accept" + i);
+ }
+ await dropText(
+ urls.join("\n"),
+ [
+ "http://mochi.test/accept0",
+ "http://mochi.test/accept1",
+ "http://mochi.test/accept2",
+ "http://mochi.test/accept3",
+ "http://mochi.test/accept4",
+ ],
+ true
+ );
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+add_task(async function multiple_tabs_over_max_cancel() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/cancel" + i);
+ }
+ await dropText(urls.join("\n"), [], true);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+
+function dropText(text, expectedURLs, ignoreFirstWindow = false) {
+ return drop(
+ [[{ type: "text/plain", data: text }]],
+ expectedURLs,
+ ignoreFirstWindow
+ );
+}
+
+async function drop(dragData, expectedURLs, ignoreFirstWindow = false) {
+ let dragDataString = JSON.stringify(dragData);
+ info(
+ `Starting test for dragData:${dragDataString}; expectedURLs.length:${expectedURLs.length}`
+ );
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button
+ // that should be visible.
+ let dragSrcElement = document.getElementById("sidebar-button");
+ ok(dragSrcElement, "Sidebar button exists");
+ let newWindowButton = document.getElementById("new-window-button");
+ ok(newWindowButton, "New Window button exists");
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(newWindowButton, "drop");
+
+ let loadedPromises = expectedURLs.map(url =>
+ BrowserTestUtils.waitForNewWindow({
+ url,
+ anyWindow: true,
+ maybeErrorPage: true,
+ })
+ );
+
+ EventUtils.synthesizeDrop(
+ dragSrcElement,
+ newWindowButton,
+ dragData,
+ "link",
+ window
+ );
+
+ let windows = await Promise.all(loadedPromises);
+ for (let window of windows) {
+ await BrowserTestUtils.closeWindow(window);
+ }
+
+ await awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_new_http_window_opened_from_file_tab.js b/browser/base/content/test/general/browser_new_http_window_opened_from_file_tab.js
new file mode 100644
index 0000000000..8e9f458073
--- /dev/null
+++ b/browser/base/content/test/general/browser_new_http_window_opened_from_file_tab.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const TEST_FILE = "file_with_link_to_http.html";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_HTTP = "http://example.org/";
+
+// Test for bug 1338375.
+add_task(async function () {
+ // Open file:// page.
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append(TEST_FILE);
+ const uriString = Services.io.newFileURI(dir).spec;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString);
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+ let browser = tab.linkedBrowser;
+
+ // Set pref to open in new window.
+ Services.prefs.setIntPref("browser.link.open_newwindow", 2);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.link.open_newwindow");
+ });
+
+ // Open new http window from JavaScript in file:// page and check that we get
+ // a new window with the correct page and features.
+ let promiseNewWindow = BrowserTestUtils.waitForNewWindow({ url: TEST_HTTP });
+ await SpecialPowers.spawn(browser, [TEST_HTTP], uri => {
+ content.open(uri, "_blank");
+ });
+ let win = await promiseNewWindow;
+ registerCleanupFunction(async function () {
+ await BrowserTestUtils.closeWindow(win);
+ });
+ ok(win, "Check that an http window loaded when using window.open.");
+ ok(
+ win.menubar.visible,
+ "Check that the menu bar on the new window is visible."
+ );
+ ok(
+ win.toolbar.visible,
+ "Check that the tool bar on the new window is visible."
+ );
+
+ // Open new http window from a link in file:// page and check that we get a
+ // new window with the correct page and features.
+ promiseNewWindow = BrowserTestUtils.waitForNewWindow({ url: TEST_HTTP });
+ await BrowserTestUtils.synthesizeMouseAtCenter("#linkToExample", {}, browser);
+ let win2 = await promiseNewWindow;
+ registerCleanupFunction(async function () {
+ await BrowserTestUtils.closeWindow(win2);
+ });
+ ok(win2, "Check that an http window loaded when using link.");
+ ok(
+ win2.menubar.visible,
+ "Check that the menu bar on the new window is visible."
+ );
+ ok(
+ win2.toolbar.visible,
+ "Check that the tool bar on the new window is visible."
+ );
+});
diff --git a/browser/base/content/test/general/browser_newwindow_focus.js b/browser/base/content/test/general/browser_newwindow_focus.js
new file mode 100644
index 0000000000..dbf99f1233
--- /dev/null
+++ b/browser/base/content/test/general/browser_newwindow_focus.js
@@ -0,0 +1,93 @@
+"use strict";
+
+/**
+ * These tests are for the auto-focus behaviour on the initial browser
+ * when a window is opened from content.
+ */
+
+const PAGE = `data:text/html,<a id="target" href="%23" onclick="window.open('http://www.example.com', '_blank', 'width=100,height=100');">Click me</a>`;
+
+/**
+ * Test that when a new window is opened from content, focus moves
+ * to the initial browser in that window once the window has finished
+ * painting.
+ */
+add_task(async function test_focus_browser() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: PAGE,
+ gBrowser,
+ },
+ async function (browser) {
+ let newWinPromise = BrowserTestUtils.domWindowOpenedAndLoaded(null);
+ let delayedStartupPromise = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("#target", {}, browser);
+ let newWin = await newWinPromise;
+ await BrowserTestUtils.waitForContentEvent(
+ newWin.gBrowser.selectedBrowser,
+ "MozAfterPaint"
+ );
+ await delayedStartupPromise;
+
+ let focusedElement = Services.focus.getFocusedElementForWindow(
+ newWin,
+ false,
+ {}
+ );
+
+ Assert.equal(
+ focusedElement,
+ newWin.gBrowser.selectedBrowser,
+ "Initial browser should be focused"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+ }
+ );
+});
+
+/**
+ * Test that when a new window is opened from content and focus
+ * shifts in that window before the content has a chance to paint
+ * that we _don't_ steal focus once content has painted.
+ */
+add_task(async function test_no_steal_focus() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: PAGE,
+ gBrowser,
+ },
+ async function (browser) {
+ let newWinPromise = BrowserTestUtils.domWindowOpenedAndLoaded(null);
+ let delayedStartupPromise = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("#target", {}, browser);
+ let newWin = await newWinPromise;
+
+ // Because we're switching focus, we shouldn't steal it once
+ // content paints.
+ newWin.gURLBar.focus();
+
+ await BrowserTestUtils.waitForContentEvent(
+ newWin.gBrowser.selectedBrowser,
+ "MozAfterPaint"
+ );
+ await delayedStartupPromise;
+
+ let focusedElement = Services.focus.getFocusedElementForWindow(
+ newWin,
+ false,
+ {}
+ );
+
+ Assert.equal(
+ focusedElement,
+ newWin.gURLBar.inputField,
+ "URLBar should be focused"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+ }
+ );
+});
diff --git a/browser/base/content/test/general/browser_plainTextLinks.js b/browser/base/content/test/general/browser_plainTextLinks.js
new file mode 100644
index 0000000000..706f21387c
--- /dev/null
+++ b/browser/base/content/test/general/browser_plainTextLinks.js
@@ -0,0 +1,237 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+function testExpected(expected, msg) {
+ is(
+ document.getElementById("context-openlinkincurrent").hidden,
+ expected,
+ msg
+ );
+}
+
+function testLinkExpected(expected, msg) {
+ is(gContextMenu.linkURL, expected, msg);
+}
+
+add_task(async function () {
+ const url =
+ "data:text/html;charset=UTF-8,Test For Non-Hyperlinked url selection";
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
+
+ // Initial setup of the content area.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function (arg) {
+ let doc = content.document;
+ let range = doc.createRange();
+ let selection = content.getSelection();
+
+ let mainDiv = doc.createElement("div");
+ let div = doc.createElement("div");
+ let div2 = doc.createElement("div");
+ let span1 = doc.createElement("span");
+ let span2 = doc.createElement("span");
+ let span3 = doc.createElement("span");
+ let span4 = doc.createElement("span");
+ let p1 = doc.createElement("p");
+ let p2 = doc.createElement("p");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ span1.textContent = "http://index.";
+ span2.textContent = "example.com example.com";
+ span3.textContent = " - Test";
+ span4.innerHTML =
+ "<a href='http://www.example.com'>http://www.example.com/example</a>";
+ p1.textContent = "mailto:test.com ftp.example.com";
+ p2.textContent = "example.com -";
+ div.appendChild(span1);
+ div.appendChild(span2);
+ div.appendChild(span3);
+ div.appendChild(span4);
+ div.appendChild(p1);
+ div.appendChild(p2);
+ let p3 = doc.createElement("p");
+ p3.textContent = "main.example.com";
+ div2.appendChild(p3);
+ mainDiv.appendChild(div);
+ mainDiv.appendChild(div2);
+ doc.body.appendChild(mainDiv);
+
+ function setSelection(el1, el2, index1, index2) {
+ while (el1.nodeType != el1.TEXT_NODE) {
+ el1 = el1.firstChild;
+ }
+ while (el2.nodeType != el1.TEXT_NODE) {
+ el2 = el2.firstChild;
+ }
+
+ selection.removeAllRanges();
+ range.setStart(el1, index1);
+ range.setEnd(el2, index2);
+ selection.addRange(range);
+
+ return range;
+ }
+
+ // Each of these tests creates a selection and returns a range within it.
+ content.tests = [
+ () => setSelection(span1.firstChild, span2.firstChild, 0, 11),
+ () => setSelection(span1.firstChild, span2.firstChild, 7, 11),
+ () => setSelection(span1.firstChild, span2.firstChild, 8, 11),
+ () => setSelection(span2.firstChild, span2.firstChild, 0, 11),
+ () => setSelection(span2.firstChild, span2.firstChild, 11, 23),
+ () => setSelection(span2.firstChild, span2.firstChild, 0, 10),
+ () => setSelection(span2.firstChild, span3.firstChild, 12, 7),
+ () => setSelection(span2.firstChild, span2.firstChild, 12, 19),
+ () => setSelection(p1.firstChild, p1.firstChild, 0, 15),
+ () => setSelection(p1.firstChild, p1.firstChild, 16, 31),
+ () => setSelection(p2.firstChild, p2.firstChild, 0, 14),
+ () => {
+ selection.selectAllChildren(div2);
+ return selection.getRangeAt(0);
+ },
+ () => {
+ selection.selectAllChildren(span4);
+ return selection.getRangeAt(0);
+ },
+ () => {
+ mainDiv.innerHTML = "(open-suse.ru)";
+ return setSelection(mainDiv, mainDiv, 1, 13);
+ },
+ () => setSelection(mainDiv, mainDiv, 1, 14),
+ ];
+ });
+
+ let checks = [
+ () =>
+ testExpected(
+ false,
+ "The link context menu should show for http://www.example.com"
+ ),
+ () =>
+ testExpected(
+ false,
+ "The link context menu should show for www.example.com"
+ ),
+ () =>
+ testExpected(
+ true,
+ "The link context menu should not show for ww.example.com"
+ ),
+ () => {
+ testExpected(false, "The link context menu should show for example.com");
+ testLinkExpected(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/",
+ "url for example.com selection should not prepend www"
+ );
+ },
+ () =>
+ testExpected(false, "The link context menu should show for example.com"),
+ () =>
+ testExpected(
+ true,
+ "Link options should not show for selection that's not at a word boundary"
+ ),
+ () =>
+ testExpected(
+ true,
+ "Link options should not show for selection that has whitespace"
+ ),
+ () =>
+ testExpected(
+ true,
+ "Link options should not show unless a url is selected"
+ ),
+ () => testExpected(true, "Link options should not show for mailto: links"),
+ () => {
+ testExpected(false, "Link options should show for ftp.example.com");
+ testLinkExpected(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://ftp.example.com/",
+ "ftp.example.com should be preceeded with http://"
+ );
+ },
+ () => testExpected(false, "Link options should show for www.example.com "),
+ () =>
+ testExpected(
+ false,
+ "Link options should show for triple-click selections"
+ ),
+ () =>
+ testLinkExpected(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.example.com/",
+ "Linkified text should open the correct link"
+ ),
+ () => {
+ testExpected(false, "Link options should show for open-suse.ru");
+ testLinkExpected(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://open-suse.ru/",
+ "Linkified text should open the correct link"
+ );
+ },
+ () =>
+ testExpected(true, "Link options should not show for 'open-suse.ru)'"),
+ ];
+
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+
+ for (let testid = 0; testid < checks.length; testid++) {
+ let menuPosition = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ testid }],
+ async function (arg) {
+ let range = content.tests[arg.testid]();
+
+ // Get the range of the selection and determine its coordinates. These
+ // coordinates will be returned to the parent process and the context menu
+ // will be opened at that location.
+ let rangeRect = range.getBoundingClientRect();
+ return [rangeRect.x + 3, rangeRect.y + 3];
+ }
+ );
+
+ // Trigger a mouse event until we receive the popupshown event.
+ let sawPopup = false;
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown",
+ false,
+ () => {
+ sawPopup = true;
+ return true;
+ }
+ );
+ while (!sawPopup) {
+ await BrowserTestUtils.synthesizeMouseAtPoint(
+ menuPosition[0],
+ menuPosition[1],
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ if (!sawPopup) {
+ await new Promise(r => setTimeout(r, 100));
+ }
+ }
+ await popupShownPromise;
+
+ checks[testid]();
+
+ // On Linux non-e10s it's possible the menu was closed by a focus-out event
+ // on the window. Work around this by calling hidePopup only if the menu
+ // hasn't been closed yet. See bug 1352709 comment 36.
+ if (contentAreaContextMenu.state === "closed") {
+ continue;
+ }
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ contentAreaContextMenu.hidePopup();
+ await popupHiddenPromise;
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_printpreview.js b/browser/base/content/test/general/browser_printpreview.js
new file mode 100644
index 0000000000..945e2bbd3a
--- /dev/null
+++ b/browser/base/content/test/general/browser_printpreview.js
@@ -0,0 +1,43 @@
+let ourTab;
+
+async function test() {
+ waitForExplicitFinish();
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true).then(
+ function (tab) {
+ ourTab = tab;
+ ok(
+ !document.querySelector(".printPreviewBrowser"),
+ "Should NOT be in print preview mode at starting this tests"
+ );
+ testClosePrintPreviewWithEscKey();
+ }
+ );
+}
+
+function tidyUp() {
+ BrowserTestUtils.removeTab(ourTab);
+ finish();
+}
+
+async function testClosePrintPreviewWithEscKey() {
+ await openPrintPreview();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await checkPrintPreviewClosed();
+ ok(true, "print preview mode should be finished by Esc key press");
+ tidyUp();
+}
+
+async function openPrintPreview() {
+ document.getElementById("cmd_print").doCommand();
+ await BrowserTestUtils.waitForCondition(() => {
+ let preview = document.querySelector(".printPreviewBrowser");
+ return preview && BrowserTestUtils.is_visible(preview);
+ });
+}
+
+async function checkPrintPreviewClosed() {
+ await BrowserTestUtils.waitForCondition(
+ () => !document.querySelector(".printPreviewBrowser")
+ );
+}
diff --git a/browser/base/content/test/general/browser_private_browsing_window.js b/browser/base/content/test/general/browser_private_browsing_window.js
new file mode 100644
index 0000000000..34a4c8bbf0
--- /dev/null
+++ b/browser/base/content/test/general/browser_private_browsing_window.js
@@ -0,0 +1,133 @@
+// Make sure that we can open private browsing windows
+
+function test() {
+ waitForExplicitFinish();
+ var nonPrivateWin = OpenBrowserWindow();
+ ok(
+ !PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin),
+ "OpenBrowserWindow() should open a normal window"
+ );
+ nonPrivateWin.close();
+
+ var privateWin = OpenBrowserWindow({ private: true });
+ ok(
+ PrivateBrowsingUtils.isWindowPrivate(privateWin),
+ "OpenBrowserWindow({private: true}) should open a private window"
+ );
+
+ nonPrivateWin = OpenBrowserWindow({ private: false });
+ ok(
+ !PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin),
+ "OpenBrowserWindow({private: false}) should open a normal window"
+ );
+ nonPrivateWin.close();
+
+ whenDelayedStartupFinished(privateWin, function () {
+ nonPrivateWin = privateWin.OpenBrowserWindow({ private: false });
+ ok(
+ !PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin),
+ "privateWin.OpenBrowserWindow({private: false}) should open a normal window"
+ );
+
+ nonPrivateWin.close();
+
+ [
+ {
+ normal: "menu_newNavigator",
+ private: "menu_newPrivateWindow",
+ accesskey: true,
+ },
+ {
+ normal: "appmenu_newNavigator",
+ private: "appmenu_newPrivateWindow",
+ accesskey: false,
+ },
+ ].forEach(function (menu) {
+ let newWindow = privateWin.document.getElementById(menu.normal);
+ let newPrivateWindow = privateWin.document.getElementById(menu.private);
+ if (newWindow && newPrivateWindow) {
+ ok(
+ !newPrivateWindow.hidden,
+ "New Private Window menu item should be hidden"
+ );
+ isnot(
+ newWindow.label,
+ newPrivateWindow.label,
+ "New Window's label shouldn't be overwritten"
+ );
+ if (menu.accesskey) {
+ isnot(
+ newWindow.accessKey,
+ newPrivateWindow.accessKey,
+ "New Window's accessKey shouldn't be overwritten"
+ );
+ }
+ isnot(
+ newWindow.command,
+ newPrivateWindow.command,
+ "New Window's command shouldn't be overwritten"
+ );
+ }
+ });
+
+ is(
+ privateWin.gBrowser.tabs[0].label,
+ "New Private Tab",
+ "New tabs in the private browsing windows should have 'New Private Tab' as the title."
+ );
+
+ privateWin.close();
+
+ Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true);
+ privateWin = OpenBrowserWindow({ private: true });
+ whenDelayedStartupFinished(privateWin, function () {
+ [
+ {
+ normal: "menu_newNavigator",
+ private: "menu_newPrivateWindow",
+ accessKey: true,
+ },
+ {
+ normal: "appmenu_newNavigator",
+ private: "appmenu_newPrivateWindow",
+ accessKey: false,
+ },
+ ].forEach(function (menu) {
+ let newWindow = privateWin.document.getElementById(menu.normal);
+ let newPrivateWindow = privateWin.document.getElementById(menu.private);
+ if (newWindow && newPrivateWindow) {
+ ok(
+ newPrivateWindow.hidden,
+ "New Private Window menu item should be hidden"
+ );
+ is(
+ newWindow.label,
+ newPrivateWindow.label,
+ "New Window's label should be overwritten"
+ );
+ if (menu.accesskey) {
+ is(
+ newWindow.accessKey,
+ newPrivateWindow.accessKey,
+ "New Window's accessKey should be overwritten"
+ );
+ }
+ is(
+ newWindow.command,
+ newPrivateWindow.command,
+ "New Window's command should be overwritten"
+ );
+ }
+ });
+
+ is(
+ privateWin.gBrowser.tabs[0].label,
+ "New Tab",
+ "Normal tab title is used also in the permanent private browsing mode."
+ );
+ privateWin.close();
+ Services.prefs.clearUserPref("browser.privatebrowsing.autostart");
+ finish();
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_private_no_prompt.js b/browser/base/content/test/general/browser_private_no_prompt.js
new file mode 100644
index 0000000000..d8c9f8e7b5
--- /dev/null
+++ b/browser/base/content/test/general/browser_private_no_prompt.js
@@ -0,0 +1,12 @@
+function test() {
+ waitForExplicitFinish();
+ var privateWin = OpenBrowserWindow({ private: true });
+
+ whenDelayedStartupFinished(privateWin, function () {
+ privateWin.BrowserOpenTab();
+ privateWin.BrowserTryToCloseWindow();
+ ok(true, "didn't prompt");
+
+ executeSoon(finish);
+ });
+}
diff --git a/browser/base/content/test/general/browser_refreshBlocker.js b/browser/base/content/test/general/browser_refreshBlocker.js
new file mode 100644
index 0000000000..0052282257
--- /dev/null
+++ b/browser/base/content/test/general/browser_refreshBlocker.js
@@ -0,0 +1,209 @@
+"use strict";
+
+const META_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/refresh_meta.sjs";
+const HEADER_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/refresh_header.sjs";
+const TARGET_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+const PREF = "accessibility.blockautorefresh";
+
+/**
+ * Goes into the content, and simulates a meta-refresh header at a very
+ * low level, and checks to see if it was blocked. This will always cancel
+ * the refresh, regardless of whether or not the refresh was blocked.
+ *
+ * @param browser (<xul:browser>)
+ * The browser to test for refreshing.
+ * @param expectRefresh (bool)
+ * Whether or not we expect the refresh attempt to succeed.
+ * @returns Promise
+ */
+async function attemptFakeRefresh(browser, expectRefresh) {
+ await SpecialPowers.spawn(
+ browser,
+ [expectRefresh],
+ async function (contentExpectRefresh) {
+ let URI = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
+ let refresher = docShell.QueryInterface(Ci.nsIRefreshURI);
+ refresher.refreshURI(URI, null, 0);
+
+ Assert.equal(
+ refresher.refreshPending,
+ contentExpectRefresh,
+ "Got the right refreshPending state"
+ );
+
+ if (refresher.refreshPending) {
+ // Cancel the pending refresh
+ refresher.cancelRefreshURITimers();
+ }
+
+ // The RefreshBlocker will wait until onLocationChange has
+ // been fired before it will show any notifications (see bug
+ // 1246291), so we cause this to occur manually here.
+ content.location = URI.spec + "#foo";
+ }
+ );
+}
+
+/**
+ * Tests that we can enable the blocking pref and block a refresh
+ * from occurring while showing a notification bar. Also tests that
+ * when we disable the pref, that refreshes can go through again.
+ */
+add_task(async function test_can_enable_and_block() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TARGET_PAGE,
+ },
+ async function (browser) {
+ // By default, we should be able to reload the page.
+ await attemptFakeRefresh(browser, true);
+
+ await pushPrefs(["accessibility.blockautorefresh", true]);
+
+ let notificationPromise = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "refresh-blocked"
+ );
+
+ await attemptFakeRefresh(browser, false);
+
+ await notificationPromise;
+
+ await pushPrefs(["accessibility.blockautorefresh", false]);
+
+ // Page reloads should go through again.
+ await attemptFakeRefresh(browser, true);
+ }
+ );
+});
+
+/**
+ * Attempts a "real" refresh by opening a tab, and then sending it to
+ * an SJS page that will attempt to cause a refresh. This will also pass
+ * a delay amount to the SJS page. The refresh should be blocked, and
+ * the notification should be shown. Once shown, the "Allow" button will
+ * be clicked, and the refresh will go through. Finally, the helper will
+ * close the tab and resolve the Promise.
+ *
+ * @param refreshPage (string)
+ * The SJS page to use. Use META_PAGE for the <meta> tag refresh
+ * case. Use HEADER_PAGE for the HTTP header case.
+ * @param delay (int)
+ * The amount, in ms, for the page to wait before attempting the
+ * refresh.
+ *
+ * @returns Promise
+ */
+async function testRealRefresh(refreshPage, delay) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function (browser) {
+ await pushPrefs(["accessibility.blockautorefresh", true]);
+
+ BrowserTestUtils.loadURIString(
+ browser,
+ refreshPage + "?p=" + TARGET_PAGE + "&d=" + delay
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Once browserLoaded resolves, all nsIWebProgressListener callbacks
+ // should have fired, so the notification should be visible.
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.currentNotification;
+
+ ok(notification, "Notification should be visible");
+ is(
+ notification.getAttribute("value"),
+ "refresh-blocked",
+ "Should be showing the right notification"
+ );
+
+ // Then click the button to allow the refresh.
+ let buttons = notification.buttonContainer.querySelectorAll(
+ ".notification-button"
+ );
+ is(buttons.length, 1, "Should have one button.");
+
+ // Prepare a Promise that should resolve when the refresh goes through
+ let refreshPromise = BrowserTestUtils.browserLoaded(browser);
+ buttons[0].click();
+
+ await refreshPromise;
+ }
+ );
+}
+
+/**
+ * Tests the meta-tag case for both short and longer delay times.
+ */
+add_task(async function test_can_allow_refresh() {
+ await testRealRefresh(META_PAGE, 0);
+ await testRealRefresh(META_PAGE, 100);
+ await testRealRefresh(META_PAGE, 500);
+});
+
+/**
+ * Tests that when a HTTP header case for both short and longer
+ * delay times.
+ */
+add_task(async function test_can_block_refresh_from_header() {
+ await testRealRefresh(HEADER_PAGE, 0);
+ await testRealRefresh(HEADER_PAGE, 100);
+ await testRealRefresh(HEADER_PAGE, 500);
+});
+
+/**
+ * Tests that we can update a notification when multiple reload/redirect
+ * attempts happen.
+ */
+add_task(async function test_can_update_notification() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function (browser) {
+ await pushPrefs(["accessibility.blockautorefresh", true]);
+
+ // First, attempt a redirect
+ BrowserTestUtils.loadURIString(
+ browser,
+ META_PAGE + "?d=0&p=" + TARGET_PAGE
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Once browserLoaded resolves, all nsIWebProgressListener callbacks
+ // should have fired, so the notification should be visible.
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.currentNotification;
+
+ let message = notification.messageText.querySelector("span");
+ is(
+ message.dataset.l10nId,
+ "refresh-blocked-redirect-label",
+ "Should be showing the redirect message"
+ );
+
+ // Next, attempt a refresh
+ await attemptFakeRefresh(browser, false);
+
+ message = notification.messageText.querySelector("span");
+ is(
+ message.dataset.l10nId,
+ "refresh-blocked-refresh-label",
+ "Should be showing the refresh message"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/general/browser_relatedTabs.js b/browser/base/content/test/general/browser_relatedTabs.js
new file mode 100644
index 0000000000..22ed8fbb1b
--- /dev/null
+++ b/browser/base/content/test/general/browser_relatedTabs.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/. */
+
+add_task(async function () {
+ is(gBrowser.tabs.length, 1, "one tab is open initially");
+
+ // Add several new tabs in sequence, interrupted by selecting a
+ // different tab, moving a tab around and closing a tab,
+ // returning a list of opened tabs for verifying the expected order.
+ // The new tab behaviour is documented in bug 465673
+ let tabs = [];
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+
+ function addTab(aURL, aReferrer) {
+ let referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ aReferrer
+ );
+ let tab = BrowserTestUtils.addTab(gBrowser, aURL, { referrerInfo });
+ tabs.push(tab);
+ return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ }
+
+ await addTab("http://mochi.test:8888/#0");
+ gBrowser.selectedTab = tabs[0];
+ await addTab("http://mochi.test:8888/#1");
+ await addTab("http://mochi.test:8888/#2", gBrowser.currentURI);
+ await addTab("http://mochi.test:8888/#3", gBrowser.currentURI);
+ gBrowser.selectedTab = tabs[tabs.length - 1];
+ gBrowser.selectedTab = tabs[0];
+ await addTab("http://mochi.test:8888/#4", gBrowser.currentURI);
+ gBrowser.selectedTab = tabs[3];
+ await addTab("http://mochi.test:8888/#5", gBrowser.currentURI);
+ gBrowser.removeTab(tabs.pop());
+ await addTab("about:blank", gBrowser.currentURI);
+ gBrowser.moveTabTo(gBrowser.selectedTab, 1);
+ await addTab("http://mochi.test:8888/#6", gBrowser.currentURI);
+ await addTab();
+ await addTab("http://mochi.test:8888/#7");
+
+ function testPosition(tabNum, expectedPosition, msg) {
+ is(
+ Array.prototype.indexOf.call(gBrowser.tabs, tabs[tabNum]),
+ expectedPosition,
+ msg
+ );
+ }
+
+ testPosition(0, 3, "tab without referrer was opened to the far right");
+ testPosition(1, 7, "tab without referrer was opened to the far right");
+ testPosition(2, 5, "tab with referrer opened immediately to the right");
+ testPosition(3, 1, "next tab with referrer opened further to the right");
+ testPosition(
+ 4,
+ 4,
+ "tab selection changed, tab opens immediately to the right"
+ );
+ testPosition(
+ 5,
+ 6,
+ "blank tab with referrer opens to the right of 3rd original tab where removed tab was"
+ );
+ testPosition(6, 2, "tab has moved, new tab opens immediately to the right");
+ testPosition(7, 8, "blank tab without referrer opens at the end");
+ testPosition(8, 9, "tab without referrer opens at the end");
+
+ tabs.forEach(gBrowser.removeTab, gBrowser);
+});
diff --git a/browser/base/content/test/general/browser_remoteTroubleshoot.js b/browser/base/content/test/general/browser_remoteTroubleshoot.js
new file mode 100644
index 0000000000..84722b2603
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { WebChannel } = ChromeUtils.importESModule(
+ "resource://gre/modules/WebChannel.sys.mjs"
+);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const TEST_URL_TAIL =
+ "example.com/browser/browser/base/content/test/general/test_remoteTroubleshoot.html";
+const TEST_URI_GOOD = Services.io.newURI("https://" + TEST_URL_TAIL);
+const TEST_URI_BAD = Services.io.newURI("http://" + TEST_URL_TAIL);
+const TEST_URI_GOOD_OBJECT = Services.io.newURI(
+ "https://" + TEST_URL_TAIL + "?object"
+);
+
+// Creates a one-shot web-channel for the test data to be sent back from the test page.
+function promiseChannelResponse(channelID, originOrPermission) {
+ return new Promise((resolve, reject) => {
+ let channel = new WebChannel(channelID, originOrPermission);
+ channel.listen((id, data, target) => {
+ channel.stopListening();
+ resolve(data);
+ });
+ });
+}
+
+// Loads the specified URI in a new tab and waits for it to send us data on our
+// test web-channel and resolves with that data.
+function promiseNewChannelResponse(uri) {
+ let channelPromise = promiseChannelResponse(
+ "test-remote-troubleshooting-backchannel",
+ uri
+ );
+ let tab = gBrowser.addTab(uri.spec, {
+ inBackground: false,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ return promiseTabLoaded(tab)
+ .then(() => channelPromise)
+ .then(data => {
+ gBrowser.removeTab(tab);
+ return data;
+ });
+}
+
+add_task(async function () {
+ // We haven't set a permission yet - so even the "good" URI should fail.
+ let got = await promiseNewChannelResponse(TEST_URI_GOOD);
+ // Should return an error.
+ Assert.ok(
+ got.message.errno === 2,
+ "should have failed with errno 2, no such channel"
+ );
+
+ // Add a permission manager entry for our URI.
+ PermissionTestUtils.add(
+ TEST_URI_GOOD,
+ "remote-troubleshooting",
+ Services.perms.ALLOW_ACTION
+ );
+ registerCleanupFunction(() => {
+ PermissionTestUtils.remove(TEST_URI_GOOD, "remote-troubleshooting");
+ });
+
+ // Try again - now we are expecting a response with the actual data.
+ got = await promiseNewChannelResponse(TEST_URI_GOOD);
+
+ // Check some keys we expect to always get.
+ Assert.ok(got.message.addons, "should have addons");
+ Assert.ok(got.message.graphics, "should have graphics");
+
+ // Check we have channel and build ID info:
+ Assert.equal(
+ got.message.application.buildID,
+ Services.appinfo.appBuildID,
+ "should have correct build ID"
+ );
+
+ let updateChannel = null;
+ try {
+ updateChannel = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+ ).UpdateUtils.UpdateChannel;
+ } catch (ex) {}
+ if (!updateChannel) {
+ Assert.ok(
+ !("updateChannel" in got.message.application),
+ "should not have update channel where not available."
+ );
+ } else {
+ Assert.equal(
+ got.message.application.updateChannel,
+ updateChannel,
+ "should have correct update channel."
+ );
+ }
+
+ // And check some keys we know we decline to return.
+ Assert.ok(
+ !got.message.modifiedPreferences,
+ "should not have a modifiedPreferences key"
+ );
+ Assert.ok(
+ !got.message.printingPreferences,
+ "should not have a printingPreferences key"
+ );
+ Assert.ok(!got.message.crashes, "should not have crash info");
+
+ // Now a http:// URI - should receive an error
+ got = await promiseNewChannelResponse(TEST_URI_BAD);
+ Assert.ok(
+ got.message.errno === 2,
+ "should have failed with errno 2, no such channel"
+ );
+
+ // Check that the page can send an object as well if it's in the whitelist
+ let webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+ let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+ let newWhitelist = origWhitelist + " https://example.com";
+ Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(webchannelWhitelistPref);
+ });
+ got = await promiseNewChannelResponse(TEST_URI_GOOD_OBJECT);
+ Assert.ok(got.message, "should have gotten some data back");
+});
diff --git a/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js b/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js
new file mode 100644
index 0000000000..3ae7c62105
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 makeInputStream(aString) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = aString;
+ return stream; // XPConnect will QI this to nsIInputStream for us.
+}
+
+add_task(async function test_remoteWebNavigation_postdata() {
+ let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+ let { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+ );
+
+ let server = new HttpServer();
+ server.start(-1);
+
+ await new Promise(resolve => {
+ server.registerPathHandler("/test", (request, response) => {
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ is(body, "success", "request body is correct");
+ is(request.method, "POST", "request was a post");
+ response.write("Received from POST: " + body);
+ resolve();
+ });
+
+ let i = server.identity;
+ let path =
+ i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort + "/test";
+
+ let postdata =
+ "Content-Length: 7\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "\r\n" +
+ "success";
+
+ openTrustedLinkIn(path, "tab", {
+ allowThirdPartyFixup: null,
+ postData: makeInputStream(postdata),
+ });
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await new Promise(resolve => {
+ server.stop(function () {
+ resolve();
+ });
+ });
+});
diff --git a/browser/base/content/test/general/browser_restore_isAppTab.js b/browser/base/content/test/general/browser_restore_isAppTab.js
new file mode 100644
index 0000000000..ab26342692
--- /dev/null
+++ b/browser/base/content/test/general/browser_restore_isAppTab.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+const DUMMY =
+ "https://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+function isBrowserAppTab(browser) {
+ return browser.browsingContext.isAppTab;
+}
+
+// Restarts the child process by crashing it then reloading the tab
+var restart = async function (browser) {
+ // If the tab isn't remote this would crash the main process so skip it
+ if (!browser.isRemoteBrowser) {
+ return;
+ }
+
+ // Make sure the main process has all of the current tab state before crashing
+ await TabStateFlusher.flush(browser);
+
+ await BrowserTestUtils.crashFrame(browser);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ SessionStore.reviveCrashedTab(tab);
+
+ await promiseTabLoaded(tab);
+};
+
+add_task(async function navigate() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:robots");
+ let browser = tab.linkedBrowser;
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.browserStopped(gBrowser);
+ let isAppTab = isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ BrowserTestUtils.loadURIString(gBrowser, DUMMY);
+ await BrowserTestUtils.browserStopped(gBrowser);
+ isAppTab = isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.unpinTab(tab);
+ isAppTab = isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ BrowserTestUtils.loadURIString(gBrowser, "about:robots");
+ await BrowserTestUtils.browserStopped(gBrowser);
+ isAppTab = isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function crash() {
+ if (!gMultiProcessBrowser || !AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ let tab = BrowserTestUtils.addTab(gBrowser, DUMMY);
+ let browser = tab.linkedBrowser;
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.browserStopped(gBrowser);
+ let isAppTab = isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ await restart(browser);
+ isAppTab = isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_save_link-perwindowpb.js b/browser/base/content/test/general/browser_save_link-perwindowpb.js
new file mode 100644
index 0000000000..4800c813b3
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_link-perwindowpb.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+// Trigger a save of a link in public mode, then trigger an identical save
+// in private mode and ensure that the second request is differentiated from
+// the first by checking that cookies set by the first response are not sent
+// during the second request.
+function triggerSave(aWindow, aCallback) {
+ info("started triggerSave");
+ var fileName;
+ let testBrowser = aWindow.gBrowser.selectedBrowser;
+ // This page sets a cookie if and only if a cookie does not exist yet
+ let testURI =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517-2.html";
+ BrowserTestUtils.loadURIString(testBrowser, testURI);
+ BrowserTestUtils.browserLoaded(testBrowser, false, testURI).then(() => {
+ waitForFocus(function () {
+ info("register to handle popupshown");
+ aWindow.document.addEventListener("popupshown", contextMenuOpened);
+
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#fff",
+ { type: "contextmenu", button: 2 },
+ testBrowser
+ );
+ info("right clicked!");
+ }, aWindow);
+ });
+
+ function contextMenuOpened(event) {
+ info("contextMenuOpened");
+ aWindow.document.removeEventListener("popupshown", contextMenuOpened);
+
+ // Create the folder the link will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function (fp) {
+ info("showCallback");
+ fileName = fp.defaultString;
+ info("fileName: " + fileName);
+ destFile.append(fileName);
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ info("done showCallback");
+ };
+
+ mockTransferCallback = function (downloadSuccess) {
+ info("mockTransferCallback");
+ onTransferComplete(aWindow, downloadSuccess, destDir);
+ destDir.remove(true);
+ ok(!destDir.exists(), "Destination dir should be removed");
+ ok(!destFile.exists(), "Destination file should be removed");
+ mockTransferCallback = null;
+ info("done mockTransferCallback");
+ };
+
+ // Select "Save Link As" option from context menu
+ var saveLinkCommand = aWindow.document.getElementById("context-savelink");
+ info("saveLinkCommand: " + saveLinkCommand);
+ saveLinkCommand.doCommand();
+
+ event.target.hidePopup();
+ info("popup hidden");
+ }
+
+ function onTransferComplete(aWindow2, downloadSuccess, destDir) {
+ ok(downloadSuccess, "Link should have been downloaded successfully");
+ aWindow2.close();
+
+ executeSoon(() => aCallback());
+ }
+}
+
+function test() {
+ info("Start the test");
+ waitForExplicitFinish();
+
+ var gNumSet = 0;
+ function testOnWindow(options, callback) {
+ info("testOnWindow(" + options + ")");
+ var win = OpenBrowserWindow(options);
+ info("got " + win);
+ whenDelayedStartupFinished(win, () => callback(win));
+ }
+
+ function whenDelayedStartupFinished(aWindow, aCallback) {
+ info("whenDelayedStartupFinished");
+ Services.obs.addObserver(function obs(aSubject, aTopic) {
+ info(
+ "whenDelayedStartupFinished, got topic: " +
+ aTopic +
+ ", got subject: " +
+ aSubject +
+ ", waiting for " +
+ aWindow
+ );
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(obs, aTopic);
+ executeSoon(aCallback);
+ info("whenDelayedStartupFinished found our window");
+ }
+ }, "browser-delayed-startup-finished");
+ }
+
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ info("Running the cleanup code");
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ Services.obs.removeObserver(observer, "http-on-examine-response");
+ info("Finished running the cleanup code");
+ });
+
+ function observer(subject, topic, state) {
+ info("observer called with " + topic);
+ if (topic == "http-on-modify-request") {
+ onModifyRequest(subject);
+ } else if (topic == "http-on-examine-response") {
+ onExamineResponse(subject);
+ }
+ }
+
+ function onExamineResponse(subject) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ info("onExamineResponse with " + channel.URI.spec);
+ if (
+ channel.URI.spec !=
+ "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.sjs"
+ ) {
+ info("returning");
+ return;
+ }
+ try {
+ let cookies = channel.getResponseHeader("set-cookie");
+ // From browser/base/content/test/general/bug792715.sjs, we receive a Set-Cookie
+ // header with foopy=1 when there are no cookies for that domain.
+ is(cookies, "foopy=1", "Cookie should be foopy=1");
+ gNumSet += 1;
+ info("gNumSet = " + gNumSet);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ info("onExamineResponse caught NOTAVAIL" + ex);
+ } else {
+ info("ionExamineResponse caught " + ex);
+ }
+ }
+ }
+
+ function onModifyRequest(subject) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ info("onModifyRequest with " + channel.URI.spec);
+ if (
+ channel.URI.spec !=
+ "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.sjs"
+ ) {
+ return;
+ }
+ try {
+ let cookies = channel.getRequestHeader("cookie");
+ info("cookies: " + cookies);
+ // From browser/base/content/test/general/bug792715.sjs, we should never send a
+ // cookie because we are making only 2 requests: one in public mode, and
+ // one in private mode.
+ throw new Error("We should never send a cookie in this test");
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ info("onModifyRequest caught NOTAVAIL" + ex);
+ } else {
+ info("ionModifyRequest caught " + ex);
+ }
+ }
+ }
+
+ Services.obs.addObserver(observer, "http-on-modify-request");
+ Services.obs.addObserver(observer, "http-on-examine-response");
+
+ testOnWindow(undefined, function (win) {
+ // The first save from a regular window sets a cookie.
+ triggerSave(win, function () {
+ is(gNumSet, 1, "1 cookie should be set");
+
+ // The second save from a private window also sets a cookie.
+ testOnWindow({ private: true }, function (win2) {
+ triggerSave(win2, function () {
+ is(gNumSet, 2, "2 cookies should be set");
+ finish();
+ });
+ });
+ });
+ });
+}
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+function createTemporarySaveDirectory() {
+ var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ info("create testsavedir!");
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ info("return from createTempSaveDir: " + saveDir.path);
+ return saveDir;
+}
diff --git a/browser/base/content/test/general/browser_save_link_when_window_navigates.js b/browser/base/content/test/general/browser_save_link_when_window_navigates.js
new file mode 100644
index 0000000000..49901e8bfa
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_link_when_window_navigates.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+const SAVE_PER_SITE_PREF = "browser.download.lastDir.savePerSite";
+const ALWAYS_DOWNLOAD_DIR_PREF = "browser.download.useDownloadDir";
+const ALWAYS_ASK_PREF = "browser.download.always_ask_before_handling_new_types";
+const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xhtml";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+function createTemporarySaveDirectory() {
+ var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ info("create testsavedir!");
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", saveDir);
+ info("return from createTempSaveDir: " + saveDir.path);
+ return saveDir;
+}
+
+function triggerSave(aWindow, aCallback) {
+ info(
+ "started triggerSave, persite downloads: " +
+ (Services.prefs.getBoolPref(SAVE_PER_SITE_PREF) ? "on" : "off")
+ );
+ var fileName;
+ let testBrowser = aWindow.gBrowser.selectedBrowser;
+ let testURI =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/navigating_window_with_download.html";
+
+ // Only observe the UTC dialog if it's enabled by pref
+ if (Services.prefs.getBoolPref(ALWAYS_ASK_PREF)) {
+ windowObserver.setCallback(onUCTDialog);
+ }
+
+ BrowserTestUtils.loadURIString(testBrowser, testURI);
+
+ // Create the folder the link will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function (fp) {
+ info("showCallback");
+ fileName = fp.defaultString;
+ info("fileName: " + fileName);
+ destFile.append(fileName);
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ info("done showCallback");
+ };
+
+ mockTransferCallback = function (downloadSuccess) {
+ info("mockTransferCallback");
+ onTransferComplete(aWindow, downloadSuccess, destDir);
+ destDir.remove(true);
+ ok(!destDir.exists(), "Destination dir should be removed");
+ ok(!destFile.exists(), "Destination file should be removed");
+ mockTransferCallback = null;
+ info("done mockTransferCallback");
+ };
+
+ function onUCTDialog(dialog) {
+ SpecialPowers.spawn(testBrowser, [], async () => {
+ content.document.querySelector("iframe").remove();
+ }).then(() => executeSoon(continueDownloading));
+ }
+
+ function continueDownloading() {
+ for (let win of Services.wm.getEnumerator("")) {
+ if (win.location && win.location.href == UCT_URI) {
+ win.document
+ .getElementById("unknownContentType")
+ ._fireButtonEvent("accept");
+ win.close();
+ return;
+ }
+ }
+ ok(false, "No Unknown Content Type dialog yet?");
+ }
+
+ function onTransferComplete(aWindow2, downloadSuccess) {
+ ok(downloadSuccess, "Link should have been downloaded successfully");
+ aWindow2.close();
+
+ executeSoon(aCallback);
+ }
+}
+
+var windowObserver = {
+ setCallback(aCallback) {
+ if (this._callback) {
+ ok(false, "Should only be dealing with one callback at a time.");
+ }
+ this._callback = aCallback;
+ },
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ let win = aSubject;
+
+ win.addEventListener(
+ "load",
+ function (event) {
+ if (win.location == UCT_URI) {
+ SimpleTest.executeSoon(function () {
+ if (windowObserver._callback) {
+ windowObserver._callback(win);
+ delete windowObserver._callback;
+ } else {
+ ok(false, "Unexpected UCT dialog!");
+ }
+ });
+ }
+ },
+ { once: true }
+ );
+ },
+};
+
+Services.ww.registerNotification(windowObserver);
+
+function test() {
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref(ALWAYS_ASK_PREF, false);
+
+ function testOnWindow(options, callback) {
+ info("testOnWindow(" + options + ")");
+ var win = OpenBrowserWindow(options);
+ info("got " + win);
+ whenDelayedStartupFinished(win, () => callback(win));
+ }
+
+ function whenDelayedStartupFinished(aWindow, aCallback) {
+ info("whenDelayedStartupFinished");
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ info(
+ "whenDelayedStartupFinished, got topic: " +
+ aTopic +
+ ", got subject: " +
+ aSubject +
+ ", waiting for " +
+ aWindow
+ );
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ info("whenDelayedStartupFinished found our window");
+ }
+ }, "browser-delayed-startup-finished");
+ }
+
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ info("Running the cleanup code");
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ Services.ww.unregisterNotification(windowObserver);
+ Services.prefs.clearUserPref(ALWAYS_DOWNLOAD_DIR_PREF);
+ Services.prefs.clearUserPref(SAVE_PER_SITE_PREF);
+ Services.prefs.clearUserPref(ALWAYS_ASK_PREF);
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ info("Finished running the cleanup code");
+ });
+
+ info(
+ `Running test with ${ALWAYS_ASK_PREF} set to ${Services.prefs.getBoolPref(
+ ALWAYS_ASK_PREF,
+ false
+ )}`
+ );
+ testOnWindow(undefined, function (win) {
+ let windowGonePromise = BrowserTestUtils.domWindowClosed(win);
+ Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, true);
+ triggerSave(win, async function () {
+ await windowGonePromise;
+ Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, false);
+ testOnWindow(undefined, function (win2) {
+ triggerSave(win2, finish);
+ });
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_save_private_link_perwindowpb.js b/browser/base/content/test/general/browser_save_private_link_perwindowpb.js
new file mode 100644
index 0000000000..8ede97e640
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_private_link_perwindowpb.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/. */
+
+function createTemporarySaveDirectory() {
+ var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ return saveDir;
+}
+
+function promiseNoCacheEntry(filename) {
+ return new Promise((resolve, reject) => {
+ Visitor.prototype = {
+ onCacheStorageInfo(num, consumption) {
+ info("disk storage contains " + num + " entries");
+ },
+ onCacheEntryInfo(uri) {
+ let urispec = uri.asciiSpec;
+ info(urispec);
+ is(
+ urispec.includes(filename),
+ false,
+ "web content present in disk cache"
+ );
+ },
+ onCacheEntryVisitCompleted() {
+ resolve();
+ },
+ };
+ function Visitor() {}
+
+ let storage = Services.cache2.diskCacheStorage(
+ Services.loadContextInfo.default
+ );
+ storage.asyncVisitStorage(new Visitor(), true /* Do walk entries */);
+ });
+}
+
+function promiseImageDownloaded() {
+ return new Promise((resolve, reject) => {
+ let fileName;
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ function onTransferComplete(downloadSuccess) {
+ ok(
+ downloadSuccess,
+ "Image file should have been downloaded successfully " + fileName
+ );
+
+ // Give the request a chance to finish and create a cache entry
+ resolve(fileName);
+ }
+
+ // Create the folder the image will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function (fp) {
+ fileName = fp.defaultString;
+ destFile.append(fileName);
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ };
+
+ mockTransferCallback = onTransferComplete;
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ mockTransferCallback = null;
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ destDir.remove(true);
+ });
+ });
+}
+
+add_task(async function () {
+ let testURI =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.html";
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ testURI
+ );
+
+ let contextMenu = privateWindow.document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ let popupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#img",
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ tab.linkedBrowser
+ );
+ await popupShown;
+
+ Services.cache2.clear();
+
+ let imageDownloaded = promiseImageDownloaded();
+ // Select "Save Image As" option from context menu
+ privateWindow.document.getElementById("context-saveimage").doCommand();
+
+ contextMenu.hidePopup();
+ await popupHidden;
+
+ // wait for image download
+ let fileName = await imageDownloaded;
+ await promiseNoCacheEntry(fileName);
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
diff --git a/browser/base/content/test/general/browser_save_video.js b/browser/base/content/test/general/browser_save_video.js
new file mode 100644
index 0000000000..5456ac240f
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_video.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+/**
+ * TestCase for bug 564387
+ * <https://bugzilla.mozilla.org/show_bug.cgi?id=564387>
+ */
+add_task(async function () {
+ var fileName;
+
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html"
+ );
+ await loadPromise;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#video1",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ info("context menu click on video1");
+
+ await popupShownPromise;
+
+ info("context menu opened on video1");
+
+ // Create the folder the video will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function (fp) {
+ fileName = fp.defaultString;
+ destFile.append(fileName);
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ };
+
+ let transferCompletePromise = new Promise(resolve => {
+ function onTransferComplete(downloadSuccess) {
+ ok(
+ downloadSuccess,
+ "Video file should have been downloaded successfully"
+ );
+
+ is(
+ fileName,
+ "web-video1-expectedName.ogv",
+ "Video file name is correctly retrieved from Content-Disposition http header"
+ );
+ resolve();
+ }
+
+ mockTransferCallback = onTransferComplete;
+ mockTransferRegisterer.register();
+ });
+
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ destDir.remove(true);
+ });
+
+ // Select "Save Video As" option from context menu
+ var saveVideoCommand = document.getElementById("context-savevideo");
+ saveVideoCommand.doCommand();
+ info("context-savevideo command executed");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+
+ await transferCompletePromise;
+});
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+function createTemporarySaveDirectory() {
+ var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ return saveDir;
+}
diff --git a/browser/base/content/test/general/browser_save_video_frame.js b/browser/base/content/test/general/browser_save_video_frame.js
new file mode 100644
index 0000000000..877c33bcd3
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_video_frame.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const VIDEO_URL =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html";
+
+/**
+ * mockTransfer.js provides a utility that lets us mock out
+ * the "Save File" dialog.
+ */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+/**
+ * Creates and returns an nsIFile for a new temporary save
+ * directory.
+ *
+ * @return nsIFile
+ */
+function createTemporarySaveDirectory() {
+ let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ return saveDir;
+}
+/**
+ * MockTransfer exposes a "mockTransferCallback" global which
+ * allows us to define a callback to be called once the mock file
+ * selector has selected where to save the file.
+ */
+function waitForTransferComplete() {
+ return new Promise(resolve => {
+ mockTransferCallback = () => {
+ ok(true, "Transfer completed");
+ mockTransferCallback = () => {};
+ resolve();
+ };
+ });
+}
+
+/**
+ * Loads a page with a <video> element, right-clicks it and chooses
+ * to save a frame screenshot to the disk. Completes once we've
+ * verified that the frame has been saved to disk.
+ */
+add_task(async function () {
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ // Create the folder the video will be saved into.
+ let destDir = createTemporarySaveDirectory();
+ let destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function (fp) {
+ destFile.append(fp.defaultString);
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ };
+
+ mockTransferRegisterer.register();
+
+ // Make sure that we clean these things up when we're done.
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ destDir.remove(true);
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ info("Loading video tab");
+ await promiseTabLoadEvent(tab, VIDEO_URL);
+ info("Video tab loaded.");
+
+ let context = document.getElementById("contentAreaContextMenu");
+ let popupPromise = promisePopupShown(context);
+
+ info("Synthesizing right-click on video element");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#video1",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ info("Waiting for popup to fire popupshown.");
+ await popupPromise;
+ info("Popup fired popupshown");
+
+ let saveSnapshotCommand = document.getElementById("context-video-saveimage");
+ let promiseTransfer = waitForTransferComplete();
+ info("Firing save snapshot command");
+ saveSnapshotCommand.doCommand();
+ context.hidePopup();
+ info("Waiting for transfer completion");
+ await promiseTransfer;
+ info("Transfer complete");
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_selectTabAtIndex.js b/browser/base/content/test/general/browser_selectTabAtIndex.js
new file mode 100644
index 0000000000..5d2e8c739e
--- /dev/null
+++ b/browser/base/content/test/general/browser_selectTabAtIndex.js
@@ -0,0 +1,89 @@
+"use strict";
+
+function test() {
+ const isLinux = navigator.platform.indexOf("Linux") == 0;
+
+ function assertTab(expectedTab) {
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ expectedTab,
+ `tab index ${expectedTab} should be selected`
+ );
+ }
+
+ function sendAccelKey(key) {
+ // Make sure the keystroke goes to chrome.
+ document.activeElement.blur();
+ EventUtils.synthesizeKey(key.toString(), {
+ altKey: isLinux,
+ accelKey: !isLinux,
+ });
+ }
+
+ function createTabs(count) {
+ for (let n = 0; n < count; n++) {
+ BrowserTestUtils.addTab(gBrowser);
+ }
+ }
+
+ function testKey(key, expectedTab) {
+ sendAccelKey(key);
+ assertTab(expectedTab);
+ }
+
+ function testIndex(index, expectedTab) {
+ gBrowser.selectTabAtIndex(index);
+ assertTab(expectedTab);
+ }
+
+ // Create fewer tabs than our 9 number keys.
+ is(gBrowser.tabs.length, 1, "should have 1 tab");
+ createTabs(4);
+ is(gBrowser.tabs.length, 5, "should have 5 tabs");
+
+ // Test keyboard shortcuts. Order tests so that no two test cases have the
+ // same expected tab in a row. This ensures that tab selection actually
+ // changed the selected tab.
+ testKey(8, 4);
+ testKey(1, 0);
+ testKey(2, 1);
+ testKey(4, 3);
+ testKey(9, 4);
+
+ // Test index selection.
+ testIndex(0, 0);
+ testIndex(4, 4);
+ testIndex(-5, 0);
+ testIndex(5, 4);
+ testIndex(-4, 1);
+ testIndex(1, 1);
+ testIndex(-1, 4);
+ testIndex(9, 4);
+
+ // Create more tabs than our 9 number keys.
+ createTabs(10);
+ is(gBrowser.tabs.length, 15, "should have 15 tabs");
+
+ // Test keyboard shortcuts.
+ testKey(2, 1);
+ testKey(1, 0);
+ testKey(4, 3);
+ testKey(8, 7);
+ testKey(9, 14);
+
+ // Test index selection.
+ testIndex(-15, 0);
+ testIndex(14, 14);
+ testIndex(-14, 1);
+ testIndex(15, 14);
+ testIndex(-1, 14);
+ testIndex(0, 0);
+ testIndex(1, 1);
+ testIndex(9, 9);
+
+ // Clean up tabs.
+ for (let n = 15; n > 1; n--) {
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ }
+ is(gBrowser.tabs.length, 1, "should have 1 tab");
+}
diff --git a/browser/base/content/test/general/browser_star_hsts.js b/browser/base/content/test/general/browser_star_hsts.js
new file mode 100644
index 0000000000..9452c61beb
--- /dev/null
+++ b/browser/base/content/test/general/browser_star_hsts.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+var secureURL =
+ "https://example.com/browser/browser/base/content/test/general/browser_star_hsts.sjs";
+var unsecureURL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/general/browser_star_hsts.sjs";
+
+add_task(async function test_star_redirect() {
+ registerCleanupFunction(async () => {
+ // Ensure to remove example.com from the HSTS list.
+ let sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+ sss.resetState(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ NetUtil.newURI("http://example.com/"),
+ Services.prefs.getBoolPref("privacy.partition.network_state")
+ ? { partitionKey: "(http,example.com)" }
+ : {}
+ );
+ await PlacesUtils.bookmarks.eraseEverything();
+ gBrowser.removeCurrentTab();
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ // This will add the page to the HSTS cache.
+ await promiseTabLoadEvent(tab, secureURL, secureURL);
+ // This should transparently be redirected to the secure page.
+ await promiseTabLoadEvent(tab, unsecureURL, secureURL);
+
+ await promiseStarState(BookmarkingUI.STATUS_UNSTARRED);
+
+ StarUI._createPanelIfNeeded();
+ let bookmarkPanel = document.getElementById("editBookmarkPanel");
+ let shownPromise = promisePopupShown(bookmarkPanel);
+ BookmarkingUI.star.click();
+ await shownPromise;
+
+ is(BookmarkingUI.status, BookmarkingUI.STATUS_STARRED, "The star is starred");
+});
+
+/**
+ * Waits for the star to reflect the expected state.
+ */
+function promiseStarState(aValue) {
+ return new Promise(resolve => {
+ let expectedStatus = aValue
+ ? BookmarkingUI.STATUS_STARRED
+ : BookmarkingUI.STATUS_UNSTARRED;
+ (function checkState() {
+ if (
+ BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING ||
+ BookmarkingUI.status != expectedStatus
+ ) {
+ info("Waiting for star button change.");
+ setTimeout(checkState, 1000);
+ } else {
+ resolve();
+ }
+ })();
+ });
+}
+
+/**
+ * Starts a load in an existing tab and waits for it to finish (via some event).
+ *
+ * @param aTab
+ * The tab to load into.
+ * @param aUrl
+ * The url to load.
+ * @param [optional] aFinalURL
+ * The url to wait for, same as aURL if not defined.
+ * @return {Promise} resolved when the event is handled.
+ */
+function promiseTabLoadEvent(aTab, aURL, aFinalURL) {
+ if (!aFinalURL) {
+ aFinalURL = aURL;
+ }
+
+ info("Wait for load tab event");
+ BrowserTestUtils.loadURIString(aTab.linkedBrowser, aURL);
+ return BrowserTestUtils.browserLoaded(aTab.linkedBrowser, false, aFinalURL);
+}
diff --git a/browser/base/content/test/general/browser_star_hsts.sjs b/browser/base/content/test/general/browser_star_hsts.sjs
new file mode 100644
index 0000000000..64c4235288
--- /dev/null
+++ b/browser/base/content/test/general/browser_star_hsts.sjs
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 handleRequest(request, response) {
+ let page = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>";
+ response.setStatusLine(request.httpVersion, "200", "OK");
+ response.setHeader("Strict-Transport-Security", "max-age=60");
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+}
diff --git a/browser/base/content/test/general/browser_storagePressure_notification.js b/browser/base/content/test/general/browser_storagePressure_notification.js
new file mode 100644
index 0000000000..dcafbe8bf9
--- /dev/null
+++ b/browser/base/content/test/general/browser_storagePressure_notification.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+async function notifyStoragePressure(usage = 100) {
+ let notifyPromise = TestUtils.topicObserved(
+ "QuotaManager::StoragePressure",
+ () => true
+ );
+ let usageWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance(
+ Ci.nsISupportsPRUint64
+ );
+ usageWrapper.data = usage;
+ Services.obs.notifyObservers(usageWrapper, "QuotaManager::StoragePressure");
+ return notifyPromise;
+}
+
+function openAboutPrefPromise(win) {
+ let promises = [
+ BrowserTestUtils.waitForLocationChange(
+ win.gBrowser,
+ "about:preferences#privacy"
+ ),
+ TestUtils.topicObserved("privacy-pane-loaded", () => true),
+ TestUtils.topicObserved("sync-pane-loaded", () => true),
+ ];
+ return Promise.all(promises);
+}
+add_setup(async function () {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ // Open a new tab to keep the window open.
+ await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "https://example.com"
+ );
+});
+
+// Test only displaying notification once within the given interval
+add_task(async function () {
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ const TEST_NOTIFICATION_INTERVAL_MS = 2000;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.storageManager.pressureNotification.minIntervalMS",
+ TEST_NOTIFICATION_INTERVAL_MS,
+ ],
+ ],
+ });
+ // Commenting this to see if we really need it
+ // await SpecialPowers.pushPrefEnv({set: [["privacy.reduceTimerPrecision", false]]});
+
+ await notifyStoragePressure();
+ await TestUtils.waitForCondition(() =>
+ win.gNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ )
+ );
+ let notification = win.gNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ );
+ is(
+ notification.localName,
+ "notification-message",
+ "Should display storage pressure notification"
+ );
+ notification.close();
+
+ await notifyStoragePressure();
+ notification = win.gNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ );
+ is(
+ notification,
+ null,
+ "Should not display storage pressure notification more than once within the given interval"
+ );
+
+ await new Promise(resolve =>
+ setTimeout(resolve, TEST_NOTIFICATION_INTERVAL_MS + 1)
+ );
+ await notifyStoragePressure();
+ await TestUtils.waitForCondition(() =>
+ win.gNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ )
+ );
+ notification = win.gNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ );
+ is(
+ notification.localName,
+ "notification-message",
+ "Should display storage pressure notification after the given interval"
+ );
+ notification.close();
+});
+
+// Test guiding user to the about:preferences when usage exceeds the given threshold
+add_task(async function () {
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.storageManager.pressureNotification.minIntervalMS", 0]],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "https://example.com"
+ );
+
+ const BYTES_IN_GIGABYTE = 1073741824;
+ const USAGE_THRESHOLD_BYTES =
+ BYTES_IN_GIGABYTE *
+ Services.prefs.getIntPref(
+ "browser.storageManager.pressureNotification.usageThresholdGB"
+ );
+ await notifyStoragePressure(USAGE_THRESHOLD_BYTES);
+ await TestUtils.waitForCondition(() =>
+ win.gNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ )
+ );
+ let notification = win.gNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ );
+ is(
+ notification.localName,
+ "notification-message",
+ "Should display storage pressure notification"
+ );
+ await new Promise(r => setTimeout(r, 1000));
+
+ let prefBtn = notification.buttonContainer.getElementsByTagName("button")[0];
+ ok(prefBtn, "Should have an open preferences button");
+ let aboutPrefPromise = openAboutPrefPromise(win);
+ EventUtils.synthesizeMouseAtCenter(prefBtn, {}, win);
+ await aboutPrefPromise;
+ let aboutPrefTab = win.gBrowser.selectedTab;
+ let prefDoc = win.gBrowser.selectedBrowser.contentDocument;
+ let siteDataGroup = prefDoc.getElementById("siteDataGroup");
+ is_element_visible(
+ siteDataGroup,
+ "Should open to the siteDataGroup section in about:preferences"
+ );
+ BrowserTestUtils.removeTab(aboutPrefTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test not displaying the 2nd notification if one is already being displayed
+add_task(async function () {
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ const TEST_NOTIFICATION_INTERVAL_MS = 0;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.storageManager.pressureNotification.minIntervalMS",
+ TEST_NOTIFICATION_INTERVAL_MS,
+ ],
+ ],
+ });
+
+ await notifyStoragePressure();
+ await notifyStoragePressure();
+ let allNotifications = win.gNotificationBox.allNotifications;
+ let pressureNotificationCount = 0;
+ allNotifications.forEach(notification => {
+ if (notification.getAttribute("value") == "storage-pressure-notification") {
+ pressureNotificationCount++;
+ }
+ });
+ is(
+ pressureNotificationCount,
+ 1,
+ "Should not display the 2nd notification when there is already one"
+ );
+ win.gNotificationBox.removeAllNotifications();
+});
+
+add_task(async function cleanup() {
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/general/browser_tabDrop.js b/browser/base/content/test/general/browser_tabDrop.js
new file mode 100644
index 0000000000..eddb405f46
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabDrop.js
@@ -0,0 +1,207 @@
+// TODO (Bug 1680996): Investigate why this test takes a long time.
+requestLongerTimeout(2);
+
+const ANY_URL = undefined;
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+registerCleanupFunction(async function cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+});
+
+add_task(async function test_setup() {
+ // Stop search-engine loads from hitting the network
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://example.com/",
+ search_url_get_params: "q={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+});
+
+add_task(async function single_url() {
+ await dropText("mochi.test/first", ["http://mochi.test/first"]);
+});
+add_task(async function single_javascript() {
+ await dropText("javascript:'bad'", []);
+});
+add_task(async function single_javascript_capital() {
+ await dropText("jAvascript:'bad'", []);
+});
+add_task(async function single_search() {
+ await dropText("search this", [ANY_URL]);
+});
+add_task(async function single_url2() {
+ await dropText("mochi.test/second", ["http://mochi.test/second"]);
+});
+add_task(async function single_data_url() {
+ await dropText("data:text/html,bad", []);
+});
+add_task(async function single_url3() {
+ await dropText("mochi.test/third", ["http://mochi.test/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("mochi.test/1\nmochi.test/2", [
+ "http://mochi.test/1",
+ "http://mochi.test/2",
+ ]);
+});
+add_task(async function multiple_urls_javascript() {
+ await dropText("javascript:'bad1'\nmochi.test/3", []);
+});
+add_task(async function multiple_urls_data() {
+ await dropText("mochi.test/4\ndata:text/html,bad1", []);
+});
+
+// Multiple text/plain items, with single and multiple links.
+add_task(async function multiple_items_single_and_multiple_links() {
+ await drop(
+ [
+ [{ type: "text/plain", data: "mochi.test/5" }],
+ [{ type: "text/plain", data: "mochi.test/6\nmochi.test/7" }],
+ ],
+ ["http://mochi.test/5", "http://mochi.test/6", "http://mochi.test/7"]
+ );
+});
+
+// Single text/x-moz-url item, with multiple links.
+// "text/x-moz-url" has titles in even-numbered lines.
+add_task(async function single_moz_url_multiple_links() {
+ await drop(
+ [
+ [
+ {
+ type: "text/x-moz-url",
+ data: "mochi.test/8\nTITLE8\nmochi.test/9\nTITLE9",
+ },
+ ],
+ ],
+ ["http://mochi.test/8", "http://mochi.test/9"]
+ );
+});
+
+// Single item with multiple types.
+add_task(async function single_item_multiple_types() {
+ await drop(
+ [
+ [
+ { type: "text/plain", data: "mochi.test/10" },
+ { type: "text/x-moz-url", data: "mochi.test/11\nTITLE11" },
+ ],
+ ],
+ ["http://mochi.test/11"]
+ );
+});
+
+// Warn when too many URLs are dropped.
+add_task(async function multiple_tabs_under_max() {
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/multi" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://mochi.test/multi0",
+ "http://mochi.test/multi1",
+ "http://mochi.test/multi2",
+ "http://mochi.test/multi3",
+ "http://mochi.test/multi4",
+ ]);
+});
+add_task(async function multiple_tabs_over_max_accept() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("accept");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/accept" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://mochi.test/accept0",
+ "http://mochi.test/accept1",
+ "http://mochi.test/accept2",
+ "http://mochi.test/accept3",
+ "http://mochi.test/accept4",
+ ]);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+add_task(async function multiple_tabs_over_max_cancel() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/cancel" + i);
+ }
+ await dropText(urls.join("\n"), []);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+
+function dropText(text, expectedURLs) {
+ return drop([[{ type: "text/plain", data: text }]], expectedURLs);
+}
+
+async function drop(dragData, expectedURLs) {
+ let dragDataString = JSON.stringify(dragData);
+ info(
+ `Starting test for dragData:${dragDataString}; expectedURLs.length:${expectedURLs.length}`
+ );
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "drop");
+
+ let loadedPromises = expectedURLs.map(url =>
+ BrowserTestUtils.waitForNewTab(gBrowser, url, false, true)
+ );
+
+ // A drop type of "link" onto an existing tab would normally trigger a
+ // load in that same tab, but tabbrowser code in _getDragTargetTab treats
+ // drops on the outer edges of a tab differently (loading a new tab
+ // instead). Make events created by synthesizeDrop have all of their
+ // coordinates set to 0 (screenX/screenY), so they're treated as drops
+ // on the outer edge of the tab, thus they open new tabs.
+ var event = {
+ clientX: 0,
+ clientY: 0,
+ screenX: 0,
+ screenY: 0,
+ };
+ EventUtils.synthesizeDrop(
+ gBrowser.selectedTab,
+ gBrowser.selectedTab,
+ dragData,
+ "link",
+ window,
+ undefined,
+ event
+ );
+
+ let tabs = await Promise.all(loadedPromises);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ await awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_tab_close_dependent_window.js b/browser/base/content/test/general/browser_tab_close_dependent_window.js
new file mode 100644
index 0000000000..a9b9c1d999
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_close_dependent_window.js
@@ -0,0 +1,35 @@
+"use strict";
+
+add_task(async function closing_tab_with_dependents_should_close_window() {
+ info("Opening window");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Opening tab with data URI");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ `data:text/html,<html%20onclick="W=window.open()"><body%20onbeforeunload="W.close()">`
+ );
+ info("Closing original tab in this window.");
+ BrowserTestUtils.removeTab(win.gBrowser.tabs[0]);
+ info("Clicking into the window");
+ let depTabOpened = BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "TabOpen"
+ );
+ await BrowserTestUtils.synthesizeMouse("html", 0, 0, {}, tab.linkedBrowser);
+
+ let openedTab = (await depTabOpened).target;
+ info("Got opened tab");
+
+ let windowClosedPromise = BrowserTestUtils.windowClosed(win);
+ BrowserTestUtils.removeTab(tab);
+ is(
+ Cu.isDeadWrapper(openedTab) || openedTab.linkedBrowser == null,
+ true,
+ "Opened tab should also have closed"
+ );
+ info(
+ "If we timeout now, the window failed to close - that shouldn't happen!"
+ );
+ await windowClosedPromise;
+});
diff --git a/browser/base/content/test/general/browser_tab_detach_restore.js b/browser/base/content/test/general/browser_tab_detach_restore.js
new file mode 100644
index 0000000000..d3f6a58aaa
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_detach_restore.js
@@ -0,0 +1,54 @@
+"use strict";
+
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+add_task(async function () {
+ let uri =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+ // Clear out the closed windows set to start
+ while (SessionStore.getClosedWindowCount() > 0) {
+ SessionStore.forgetClosedWindow(0);
+ }
+
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, uri);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, uri);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+
+ let key = tab.linkedBrowser.permanentKey;
+ let win = gBrowser.replaceTabWithWindow(tab);
+ await new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+
+ is(
+ win.gBrowser.selectedBrowser.permanentKey,
+ key,
+ "Should have properly copied the permanentKey"
+ );
+ await BrowserTestUtils.closeWindow(win);
+
+ is(
+ SessionStore.getClosedWindowCount(),
+ 1,
+ "Should have restore data for the closed window"
+ );
+
+ win = SessionStore.undoCloseWindow(0);
+ await BrowserTestUtils.waitForEvent(win, "load");
+ await BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "SSTabRestored"
+ );
+
+ is(win.gBrowser.tabs.length, 1, "Should have restored one tab");
+ is(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ uri,
+ "Should have restored the right page"
+ );
+
+ await promiseWindowClosed(win);
+});
diff --git a/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js b/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
new file mode 100644
index 0000000000..de4e17b97d
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
@@ -0,0 +1,423 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+const EVENTUTILS_URL =
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js";
+var EventUtils = {};
+
+Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils);
+
+/**
+ * Tests that tabs from Private Browsing windows cannot be dragged
+ * into non-private windows, and vice-versa.
+ */
+add_task(async function test_dragging_private_windows() {
+ let normalWin = await BrowserTestUtils.openNewBrowserWindow();
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let normalTab = await BrowserTestUtils.openNewForegroundTab(
+ normalWin.gBrowser
+ );
+ let privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ normalTab,
+ privateTab,
+ [[{ type: TAB_DROP_TYPE, data: normalTab }]],
+ null,
+ normalWin,
+ privateWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a normal tab to a private window"
+ );
+
+ effect = EventUtils.synthesizeDrop(
+ privateTab,
+ normalTab,
+ [[{ type: TAB_DROP_TYPE, data: privateTab }]],
+ null,
+ privateWin,
+ normalWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a private tab to a normal window"
+ );
+
+ normalWin.gBrowser.swapBrowsersAndCloseOther(normalTab, privateTab);
+ is(
+ normalWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a normal tab to a private tabbrowser"
+ );
+ is(
+ privateWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a normal tab in a private tabbrowser"
+ );
+
+ privateWin.gBrowser.swapBrowsersAndCloseOther(privateTab, normalTab);
+ is(
+ privateWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a private tab to a normal tabbrowser"
+ );
+ is(
+ normalWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a private tab in a normal tabbrowser"
+ );
+
+ await BrowserTestUtils.closeWindow(normalWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+/**
+ * Tests that tabs from e10s windows cannot be dragged into non-e10s
+ * windows, and vice-versa.
+ */
+add_task(async function test_dragging_e10s_windows() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ let remoteWin = await BrowserTestUtils.openNewBrowserWindow({ remote: true });
+ let nonRemoteWin = await BrowserTestUtils.openNewBrowserWindow({
+ remote: false,
+ fission: false,
+ });
+
+ let remoteTab = await BrowserTestUtils.openNewForegroundTab(
+ remoteWin.gBrowser
+ );
+ let nonRemoteTab = await BrowserTestUtils.openNewForegroundTab(
+ nonRemoteWin.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ remoteTab,
+ nonRemoteTab,
+ [[{ type: TAB_DROP_TYPE, data: remoteTab }]],
+ null,
+ remoteWin,
+ nonRemoteWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a remote tab to a non-e10s window"
+ );
+
+ effect = EventUtils.synthesizeDrop(
+ nonRemoteTab,
+ remoteTab,
+ [[{ type: TAB_DROP_TYPE, data: nonRemoteTab }]],
+ null,
+ nonRemoteWin,
+ remoteWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a non-remote tab to an e10s window"
+ );
+
+ remoteWin.gBrowser.swapBrowsersAndCloseOther(remoteTab, nonRemoteTab);
+ is(
+ remoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a normal tab to a private tabbrowser"
+ );
+ is(
+ nonRemoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a normal tab in a private tabbrowser"
+ );
+
+ nonRemoteWin.gBrowser.swapBrowsersAndCloseOther(nonRemoteTab, remoteTab);
+ is(
+ nonRemoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a private tab to a normal tabbrowser"
+ );
+ is(
+ remoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a private tab in a normal tabbrowser"
+ );
+
+ await BrowserTestUtils.closeWindow(remoteWin);
+ await BrowserTestUtils.closeWindow(nonRemoteWin);
+});
+
+/**
+ * Tests that tabs from fission windows cannot be dragged into non-fission
+ * windows, and vice-versa.
+ */
+add_task(async function test_dragging_fission_windows() {
+ let fissionWin = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ fission: true,
+ });
+ let nonFissionWin = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ fission: false,
+ });
+
+ let fissionTab = await BrowserTestUtils.openNewForegroundTab(
+ fissionWin.gBrowser
+ );
+ let nonFissionTab = await BrowserTestUtils.openNewForegroundTab(
+ nonFissionWin.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ fissionTab,
+ nonFissionTab,
+ [[{ type: TAB_DROP_TYPE, data: fissionTab }]],
+ null,
+ fissionWin,
+ nonFissionWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a fission tab to a non-fission window"
+ );
+
+ effect = EventUtils.synthesizeDrop(
+ nonFissionTab,
+ fissionTab,
+ [[{ type: TAB_DROP_TYPE, data: nonFissionTab }]],
+ null,
+ nonFissionWin,
+ fissionWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a non-fission tab to an fission window"
+ );
+
+ let swapOk = fissionWin.gBrowser.swapBrowsersAndCloseOther(
+ fissionTab,
+ nonFissionTab
+ );
+ is(
+ swapOk,
+ false,
+ "Returns false swapping fission tab to a non-fission tabbrowser"
+ );
+ is(
+ fissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a fission tab to a non-fission tabbrowser"
+ );
+ is(
+ nonFissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a fission tab in a non-fission tabbrowser"
+ );
+
+ swapOk = nonFissionWin.gBrowser.swapBrowsersAndCloseOther(
+ nonFissionTab,
+ fissionTab
+ );
+ is(
+ swapOk,
+ false,
+ "Returns false swapping non-fission tab to a fission tabbrowser"
+ );
+ is(
+ nonFissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a non-fission tab to a fission tabbrowser"
+ );
+ is(
+ fissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a non-fission tab in a fission tabbrowser"
+ );
+
+ await BrowserTestUtils.closeWindow(fissionWin);
+ await BrowserTestUtils.closeWindow(nonFissionWin);
+});
+
+/**
+ * Tests that remoteness-blacklisted tabs from e10s windows can
+ * be dragged between e10s windows.
+ */
+add_task(async function test_dragging_blacklisted() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ let remoteWin1 = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ });
+ remoteWin1.gBrowser.myID = "remoteWin1";
+ let remoteWin2 = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ });
+ remoteWin2.gBrowser.myID = "remoteWin2";
+
+ // Anything under chrome://mochitests/content/ will be blacklisted, and
+ // open in the parent process.
+ const BLACKLISTED_URL =
+ getRootDirectory(gTestPath) + "browser_tab_drag_drop_perwindow.js";
+ let blacklistedTab = await BrowserTestUtils.openNewForegroundTab(
+ remoteWin1.gBrowser,
+ BLACKLISTED_URL
+ );
+
+ ok(blacklistedTab.linkedBrowser, "Newly created tab should have a browser.");
+
+ ok(
+ !blacklistedTab.linkedBrowser.isRemoteBrowser,
+ `Expected a non-remote browser for URL: ${BLACKLISTED_URL}`
+ );
+
+ let otherTab = await BrowserTestUtils.openNewForegroundTab(
+ remoteWin2.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ blacklistedTab,
+ otherTab,
+ [[{ type: TAB_DROP_TYPE, data: blacklistedTab }]],
+ null,
+ remoteWin1,
+ remoteWin2
+ );
+ is(effect, "move", "Should be able to drag the blacklisted tab.");
+
+ // The synthesized drop should also do the work of swapping the
+ // browsers, so no need to call swapBrowsersAndCloseOther manually.
+
+ is(
+ remoteWin1.gBrowser.tabs.length,
+ 1,
+ "Should have moved the blacklisted tab out of this window."
+ );
+ is(
+ remoteWin2.gBrowser.tabs.length,
+ 3,
+ "Should have inserted the blacklisted tab into the other window."
+ );
+
+ // The currently selected tab in the second window should be the
+ // one we just dragged in.
+ let draggedBrowser = remoteWin2.gBrowser.selectedBrowser;
+ ok(
+ !draggedBrowser.isRemoteBrowser,
+ "The browser we just dragged in should not be remote."
+ );
+
+ is(
+ draggedBrowser.currentURI.spec,
+ BLACKLISTED_URL,
+ `Expected the URL of the dragged in tab to be ${BLACKLISTED_URL}`
+ );
+
+ await BrowserTestUtils.closeWindow(remoteWin1);
+ await BrowserTestUtils.closeWindow(remoteWin2);
+});
+
+/**
+ * Tests that tabs dragged between windows dispatch TabOpen and TabClose
+ * events with the appropriate adoption details.
+ */
+add_task(async function test_dragging_adoption_events() {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser);
+
+ let awaitCloseEvent = BrowserTestUtils.waitForEvent(tab1, "TabClose");
+ let awaitOpenEvent = BrowserTestUtils.waitForEvent(win2, "TabOpen");
+
+ let effect = EventUtils.synthesizeDrop(
+ tab1,
+ tab2,
+ [[{ type: TAB_DROP_TYPE, data: tab1 }]],
+ null,
+ win1,
+ win2
+ );
+ is(effect, "move", "Tab should be moved from win1 to win2.");
+
+ let closeEvent = await awaitCloseEvent;
+ let openEvent = await awaitOpenEvent;
+
+ is(openEvent.detail.adoptedTab, tab1, "New tab adopted old tab");
+ is(
+ closeEvent.detail.adoptedBy,
+ openEvent.target,
+ "Old tab adopted by new tab"
+ );
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
+
+/**
+ * Tests that per-site zoom settings remain active after a tab is
+ * dragged between windows.
+ */
+add_task(async function test_dragging_zoom_handling() {
+ const ZOOM_FACTOR = 1.62;
+
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ win2.FullZoom.setZoom(ZOOM_FACTOR);
+ is(
+ ZoomManager.getZoomForBrowser(tab2.linkedBrowser),
+ ZOOM_FACTOR,
+ "Original tab should have correct zoom factor"
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ tab2,
+ tab1,
+ [[{ type: TAB_DROP_TYPE, data: tab2 }]],
+ null,
+ win2,
+ win1
+ );
+ is(effect, "move", "Tab should be moved from win2 to win1.");
+
+ // Delay slightly to make sure we've finished executing any promise
+ // chains in the zoom code.
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ is(
+ ZoomManager.getZoomForBrowser(win1.gBrowser.selectedBrowser),
+ ZOOM_FACTOR,
+ "Dragged tab should have correct zoom factor"
+ );
+
+ win1.FullZoom.reset();
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/base/content/test/general/browser_tab_dragdrop.js b/browser/base/content/test/general/browser_tab_dragdrop.js
new file mode 100644
index 0000000000..9ea05842f2
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop.js
@@ -0,0 +1,257 @@
+// Swaps the content of tab a into tab b and then closes tab a.
+function swapTabsAndCloseOther(a, b) {
+ gBrowser.swapBrowsersAndCloseOther(gBrowser.tabs[b], gBrowser.tabs[a]);
+}
+
+// Mirrors the effect of the above function on an array.
+function swapArrayContentsAndRemoveOther(arr, a, b) {
+ arr[b] = arr[a];
+ arr.splice(a, 1);
+}
+
+function checkBrowserIds(expected) {
+ is(
+ gBrowser.tabs.length,
+ expected.length,
+ "Should have the right number of tabs."
+ );
+
+ for (let [i, tab] of gBrowser.tabs.entries()) {
+ is(
+ tab.linkedBrowser.browserId,
+ expected[i],
+ `Tab ${i} should have the right browser ID.`
+ );
+ is(
+ tab.linkedBrowser.browserId,
+ tab.linkedBrowser.browsingContext.browserId,
+ `Browser for tab ${i} has the same browserId as its BrowsingContext`
+ );
+ }
+}
+
+var getClicks = function (tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ return content.wrappedJSObject.clicks;
+ });
+};
+
+var clickTest = async function (tab) {
+ let clicks = await getClicks(tab);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let target = content.document.body;
+ let rect = target.getBoundingClientRect();
+ let left = (rect.left + rect.right) / 2;
+ let top = (rect.top + rect.bottom) / 2;
+
+ let utils = content.windowUtils;
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+
+ let newClicks = await getClicks(tab);
+ is(newClicks, clicks + 1, "adding 1 more click on BODY");
+};
+
+function loadURI(tab, url) {
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+}
+
+// Creates a framescript which caches the current object value from the plugin
+// in the page. checkObjectValue below verifies that the framescript is still
+// active for the browser and that the cached value matches that from the plugin
+// in the page which tells us the plugin hasn't been reinitialized.
+async function cacheObjectValue(browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ let plugin = content.document.getElementById("p").wrappedJSObject;
+ info(`plugin is ${plugin}`);
+ let win = content.document.defaultView;
+ info(`win is ${win}`);
+ win.objectValue = plugin.getObjectValue();
+ info(`got objectValue: ${win.objectValue}`);
+ });
+}
+
+// Note, can't run this via registerCleanupFunction because it needs the
+// browser to still be alive and have a messageManager.
+async function cleanupObjectValue(browser) {
+ info("entered cleanupObjectValue");
+ await SpecialPowers.spawn(browser, [], () => {
+ info("in cleanup function");
+ let win = content.document.defaultView;
+ info(`about to delete objectValue: ${win.objectValue}`);
+ delete win.objectValue;
+ });
+ info("exiting cleanupObjectValue");
+}
+
+// See the notes for cacheObjectValue above.
+async function checkObjectValue(browser) {
+ let data = await SpecialPowers.spawn(browser, [], () => {
+ let plugin = content.document.getElementById("p").wrappedJSObject;
+ let win = content.document.defaultView;
+ let result, exception;
+ try {
+ result = plugin.checkObjectValue(win.objectValue);
+ } catch (e) {
+ exception = e.toString();
+ }
+ return {
+ result,
+ exception,
+ };
+ });
+
+ if (data.result === null) {
+ ok(false, "checkObjectValue threw an exception: " + data.exception);
+ throw new Error(data.exception);
+ } else {
+ return data.result;
+ }
+}
+
+add_task(async function () {
+ // create a few tabs
+ let tabs = [
+ gBrowser.tabs[0],
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true }),
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true }),
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true }),
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true }),
+ ];
+
+ // Initially 0 1 2 3 4
+ await loadURI(
+ tabs[1],
+ "data:text/html;charset=utf-8,<title>tab1</title><body>tab1<iframe>"
+ );
+ await loadURI(tabs[2], "data:text/plain;charset=utf-8,tab2");
+ await loadURI(
+ tabs[3],
+ "data:text/html;charset=utf-8,<title>tab3</title><body>tab3<iframe>"
+ );
+ await loadURI(
+ tabs[4],
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/general/browser_tab_dragdrop_embed.html"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tabs[3]);
+
+ let browserIds = tabs.map(t => t.linkedBrowser.browserId);
+ checkBrowserIds(browserIds);
+
+ is(gBrowser.tabs[1], tabs[1], "tab1");
+ is(gBrowser.tabs[2], tabs[2], "tab2");
+ is(gBrowser.tabs[3], tabs[3], "tab3");
+ is(gBrowser.tabs[4], tabs[4], "tab4");
+
+ swapTabsAndCloseOther(2, 3); // now: 0 1 2 4
+ // Tab 2 is gone (what was tab 3 is displaying its content).
+ tabs.splice(2, 1);
+ swapArrayContentsAndRemoveOther(browserIds, 2, 3);
+
+ is(gBrowser.tabs[1], tabs[1], "tab1");
+ is(gBrowser.tabs[2], tabs[2], "tab2");
+ is(gBrowser.tabs[3], tabs[3], "tab4");
+
+ checkBrowserIds(browserIds);
+
+ info("about to cacheObjectValue");
+ await cacheObjectValue(tabs[3].linkedBrowser);
+ info("just finished cacheObjectValue");
+
+ swapTabsAndCloseOther(3, 2); // now: 0 1 4
+ tabs.splice(3, 1);
+ swapArrayContentsAndRemoveOther(browserIds, 3, 2);
+
+ is(
+ Array.prototype.indexOf.call(gBrowser.tabs, gBrowser.selectedTab),
+ 2,
+ "The third tab should be selected"
+ );
+
+ checkBrowserIds(browserIds);
+
+ ok(
+ await checkObjectValue(gBrowser.tabs[2].linkedBrowser),
+ "same plugin instance"
+ );
+
+ is(gBrowser.tabs[1], tabs[1], "tab1");
+ is(gBrowser.tabs[2], tabs[2], "tab4");
+
+ let clicks = await getClicks(gBrowser.tabs[2]);
+ is(clicks, 0, "no click on BODY so far");
+ await clickTest(gBrowser.tabs[2]);
+
+ swapTabsAndCloseOther(2, 1); // now: 0 4
+ tabs.splice(2, 1);
+ swapArrayContentsAndRemoveOther(browserIds, 2, 1);
+
+ is(gBrowser.tabs[1], tabs[1], "tab4");
+
+ checkBrowserIds(browserIds);
+
+ ok(
+ await checkObjectValue(gBrowser.tabs[1].linkedBrowser),
+ "same plugin instance"
+ );
+ await cleanupObjectValue(gBrowser.tabs[1].linkedBrowser);
+
+ await clickTest(gBrowser.tabs[1]);
+
+ // Load a new document (about:blank) in tab4, then detach that tab into a new window.
+ // In the new window, navigate back to the original document and click on its <body>,
+ // verify that its onclick was called.
+ is(
+ Array.prototype.indexOf.call(gBrowser.tabs, gBrowser.selectedTab),
+ 1,
+ "The second tab should be selected"
+ );
+ is(
+ gBrowser.tabs[1],
+ tabs[1],
+ "The second tab in gBrowser.tabs should be equal to the second tab in our array"
+ );
+ is(
+ gBrowser.selectedTab,
+ tabs[1],
+ "The second tab in our array is the selected tab"
+ );
+ await loadURI(tabs[1], "about:blank");
+ let key = tabs[1].linkedBrowser.permanentKey;
+
+ checkBrowserIds(browserIds);
+
+ let win = gBrowser.replaceTabWithWindow(tabs[1]);
+ await new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+
+ let newWinBrowserId = browserIds[1];
+ browserIds.splice(1, 1);
+ checkBrowserIds(browserIds);
+
+ // Verify that the original window now only has the initial tab left in it.
+ is(gBrowser.tabs[0], tabs[0], "tab0");
+ is(gBrowser.tabs[0].linkedBrowser.currentURI.spec, "about:blank", "tab0 uri");
+
+ let tab = win.gBrowser.tabs[0];
+ is(tab.linkedBrowser.permanentKey, key, "Should have kept the key");
+ is(tab.linkedBrowser.browserId, newWinBrowserId, "Should have kept the ID");
+ is(
+ tab.linkedBrowser.browserId,
+ tab.linkedBrowser.browsingContext.browserId,
+ "Should have kept the ID"
+ );
+
+ let awaitPageShow = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ win.gBrowser.goBack();
+ await awaitPageShow;
+
+ await clickTest(tab);
+ promiseWindowClosed(win);
+});
diff --git a/browser/base/content/test/general/browser_tab_dragdrop2.js b/browser/base/content/test/general/browser_tab_dragdrop2.js
new file mode 100644
index 0000000000..9c589922f5
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop2.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const ROOT = getRootDirectory(gTestPath);
+const URI = ROOT + "browser_tab_dragdrop2_frame1.xhtml";
+
+// Load the test page (which runs some child popup tests) in a new window.
+// After the tests were run, tear off the tab into a new window and run popup
+// tests a second time. We don't care about tests results, exceptions and
+// crashes will be caught.
+add_task(async function () {
+ // Open a new window.
+ let args = "chrome,all,dialog=no";
+ let win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ args,
+ URI
+ );
+
+ // Wait until the tests were run.
+ await promiseTestsDone(win);
+ ok(true, "tests succeeded");
+
+ // Create a second tab so that we can move the original one out.
+ BrowserTestUtils.addTab(win.gBrowser, "about:blank", { skipAnimation: true });
+
+ // Tear off the original tab.
+ let browser = win.gBrowser.selectedBrowser;
+ let tabClosed = BrowserTestUtils.waitForEvent(browser, "pagehide", true);
+ let win2 = win.gBrowser.replaceTabWithWindow(win.gBrowser.tabs[0]);
+
+ // Add a 'TestsDone' event listener to ensure that the docShells is properly
+ // swapped to the new window instead of the page being loaded again. If this
+ // works fine we should *NOT* see a TestsDone event.
+ let onTestsDone = () => ok(false, "shouldn't run tests when tearing off");
+ win2.addEventListener("TestsDone", onTestsDone);
+
+ // Wait until the original tab is gone and the new window is ready.
+ await Promise.all([tabClosed, promiseDelayedStartupFinished(win2)]);
+
+ // Remove the 'TestsDone' event listener as now
+ // we're kicking off a new test run manually.
+ win2.removeEventListener("TestsDone", onTestsDone);
+
+ // Run tests once again.
+ let promise = promiseTestsDone(win2);
+ let browser2 = win2.gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser2, [], async () => {
+ content.test_panels();
+ });
+ await promise;
+ ok(true, "tests succeeded a second time");
+
+ // Cleanup.
+ await promiseWindowClosed(win2);
+ await promiseWindowClosed(win);
+});
+
+function promiseTestsDone(win) {
+ return BrowserTestUtils.waitForEvent(win, "TestsDone");
+}
+
+function promiseDelayedStartupFinished(win) {
+ return new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+}
diff --git a/browser/base/content/test/general/browser_tab_dragdrop2_frame1.xhtml b/browser/base/content/test/general/browser_tab_dragdrop2_frame1.xhtml
new file mode 100644
index 0000000000..d64f37c289
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop2_frame1.xhtml
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+ XUL Widget Test for panels
+ -->
+<window title="Titlebar" width="200" height="200"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<tree id="tree" seltype="single" width="100" height="100">
+ <treecols>
+ <treecol flex="1"/>
+ <treecol flex="1"/>
+ </treecols>
+ <treechildren id="treechildren">
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ </treechildren>
+</tree>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+var currentTest = null;
+
+var i, waitSteps;
+var my_debug = false;
+function test_panels()
+{
+ i = waitSteps = 0;
+ checkTreeCoords();
+
+ addEventListener("popupshown", popupShown, false);
+ addEventListener("popuphidden", nextTest, false);
+ return nextTest();
+}
+
+function nextTest()
+{
+ ok(true,"popuphidden " + i)
+ if (i == tests.length) {
+ let details = {bubbles: true, cancelable: false};
+ document.dispatchEvent(new CustomEvent("TestsDone", details));
+ return i;
+ }
+
+ currentTest = tests[i];
+ var panel = createPanel(currentTest.attrs);
+ SimpleTest.waitForFocus(() => currentTest.test(panel));
+ return i;
+}
+
+function popupShown(event)
+{
+ var panel = event.target;
+ if (waitSteps > 0 && navigator.platform.includes("Linux") &&
+ panel.screenY == 210) {
+ waitSteps--;
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ setTimeout(popupShown, 10, event);
+ return;
+ }
+ ++i;
+
+ currentTest.result(currentTest.testname + " ", panel);
+ panel.hidePopup();
+}
+
+function createPanel(attrs)
+{
+ var panel = document.createXULElement("panel");
+ for (var a in attrs) {
+ panel.setAttribute(a, attrs[a]);
+ }
+
+ var button = document.createXULElement("button");
+ panel.appendChild(button);
+ button.label = "OK";
+ button.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0; height: 40px; width: 120px;");
+ panel.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;");
+ return document.documentElement.appendChild(panel);
+}
+
+function checkTreeCoords()
+{
+ var tree = $("tree");
+ var treechildren = $("treechildren");
+ tree.currentIndex = 0;
+ tree.scrollToRow(0);
+ synthesizeMouse(treechildren, 10, tree.rowHeight + 2, { });
+
+ tree.scrollToRow(2);
+ synthesizeMouse(treechildren, 10, tree.rowHeight + 2, { });
+}
+
+var tests = [
+ {
+ testname: "normal panel",
+ attrs: { },
+ test(panel) {
+ panel.openPopupAtScreen(200, 210);
+ },
+ result(testname, panel) {
+ if (my_debug) alert(testname);
+ panel.getBoundingClientRect();
+ }
+ },
+ {
+ // only noautohide panels support titlebars, so one shouldn't be shown here
+ testname: "autohide panel with titlebar",
+ attrs: { titlebar: "normal" },
+ test(panel) {
+ panel.openPopupAtScreen(200, 210);
+ },
+ result(testname, panel) {
+ if (my_debug) alert(testname);
+ panel.getBoundingClientRect();
+ }
+ },
+ {
+ testname: "noautohide panel with titlebar",
+ attrs: { noautohide: true, titlebar: "normal" },
+ test(panel) {
+ waitSteps = 25;
+ panel.openPopupAtScreen(200, 210);
+ },
+ result(testname, panel) {
+ if (my_debug) alert(testname);
+ panel.getBoundingClientRect();
+
+ synthesizeMouse(panel, 10, 10, { type: "mousemove" });
+
+ var tree = $("tree");
+ tree.currentIndex = 0;
+ panel.appendChild(tree);
+ checkTreeCoords();
+ }
+ }
+];
+
+SimpleTest.waitForFocus(test_panels);
+
+]]>
+</script>
+
+</window>
diff --git a/browser/base/content/test/general/browser_tab_dragdrop_embed.html b/browser/base/content/test/general/browser_tab_dragdrop_embed.html
new file mode 100644
index 0000000000..bad0650693
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop_embed.html
@@ -0,0 +1,2 @@
+<body onload="clicks=0" onclick="++clicks">
+ <embed type="application/x-test" allowscriptaccess="always" allowfullscreen="true" wmode="window" width="640" height="480" id="p"></embed>
diff --git a/browser/base/content/test/general/browser_tabfocus.js b/browser/base/content/test/general/browser_tabfocus.js
new file mode 100644
index 0000000000..b057a504e5
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabfocus.js
@@ -0,0 +1,811 @@
+/*
+ * This test checks that focus is adjusted properly when switching tabs.
+ */
+
+var testPage1 =
+ "<html id='html1'><body id='body1'><button id='button1'>Tab 1</button></body></html>";
+var testPage2 =
+ "<html id='html2'><body id='body2'><button id='button2'>Tab 2</button></body></html>";
+var testPage3 =
+ "<html id='html3'><body id='body3'><button id='button3'>Tab 3</button></body></html>";
+
+const fm = Services.focus;
+
+function EventStore() {
+ this["main-window"] = [];
+ this.window1 = [];
+ this.window2 = [];
+}
+
+EventStore.prototype = {
+ push(event) {
+ if (event.includes("browser1") || event.includes("browser2")) {
+ this["main-window"].push(event);
+ } else if (event.includes("1")) {
+ this.window1.push(event);
+ } else if (event.includes("2")) {
+ this.window2.push(event);
+ } else {
+ this["main-window"].push(event);
+ }
+ },
+};
+
+var tab1 = null;
+var tab2 = null;
+var browser1 = null;
+var browser2 = null;
+var _lastfocus;
+var _lastfocuswindow = null;
+var actualEvents = new EventStore();
+var expectedEvents = new EventStore();
+var currentTestName = "";
+var _expectedElement = null;
+var _expectedWindow = null;
+
+var currentPromiseResolver = null;
+
+function getFocusedElementForBrowser(browser, dontCheckExtraFocus = false) {
+ return SpecialPowers.spawn(
+ browser,
+ [dontCheckExtraFocus],
+ dontCheckExtraFocusChild => {
+ let focusedWindow = {};
+ let node = Services.focus.getFocusedElementForWindow(
+ content,
+ false,
+ focusedWindow
+ );
+ let details = "Focus is " + (node ? node.id : "<none>");
+
+ /* Check focus manager properties. Add an error onto the string if they are
+ not what is expected which will cause matching to fail in the parent process. */
+ let doc = content.document;
+ if (!dontCheckExtraFocusChild) {
+ if (Services.focus.focusedElement != node) {
+ details += "<ERROR: focusedElement doesn't match>";
+ }
+ if (
+ Services.focus.focusedWindow &&
+ Services.focus.focusedWindow != content
+ ) {
+ details += "<ERROR: focusedWindow doesn't match>";
+ }
+ if ((Services.focus.focusedWindow == content) != doc.hasFocus()) {
+ details += "<ERROR: child hasFocus() is not correct>";
+ }
+ if (
+ (Services.focus.focusedElement &&
+ doc.activeElement != Services.focus.focusedElement) ||
+ (!Services.focus.focusedElement && doc.activeElement != doc.body)
+ ) {
+ details += "<ERROR: child activeElement is not correct>";
+ }
+ }
+ return details;
+ }
+ );
+}
+
+function focusInChild(event) {
+ function getWindowDocId(target) {
+ return String(target.location).includes("1") ? "window1" : "window2";
+ }
+
+ // Stop the shim code from seeing this event process.
+ event.stopImmediatePropagation();
+
+ var id;
+ if (event.target instanceof Ci.nsIDOMWindow) {
+ id = getWindowDocId(event.originalTarget) + "-window";
+ } else if (event.target.nodeType == event.target.DOCUMENT_NODE) {
+ id = getWindowDocId(event.originalTarget) + "-document";
+ } else {
+ id = event.originalTarget.id;
+ }
+
+ let window = event.target.ownerGlobal;
+ if (!window._eventsOccurred) {
+ window._eventsOccurred = [];
+ }
+ window._eventsOccurred.push(event.type + ": " + id);
+ return true;
+}
+
+function focusElementInChild(elementid, elementtype) {
+ let browser = elementid.includes("1") ? browser1 : browser2;
+ return SpecialPowers.spawn(browser, [elementid, elementtype], (id, type) => {
+ content.document.getElementById(id)[type]();
+ });
+}
+
+add_task(async function () {
+ tab1 = BrowserTestUtils.addTab(gBrowser);
+ browser1 = gBrowser.getBrowserForTab(tab1);
+
+ tab2 = BrowserTestUtils.addTab(gBrowser);
+ browser2 = gBrowser.getBrowserForTab(tab2);
+
+ await promiseTabLoadEvent(tab1, "data:text/html," + escape(testPage1));
+ await promiseTabLoadEvent(tab2, "data:text/html," + escape(testPage2));
+
+ gURLBar.focus();
+ await SimpleTest.promiseFocus();
+
+ // In these listeners, focusInChild is used to cache details about the event
+ // on a temporary on the window (window._eventsOccurred), so that it can be
+ // retrieved later within compareFocusResults. focusInChild always returns true.
+ // compareFocusResults is called each time event occurs to check that the
+ // right events happened.
+ let listenersToRemove = [];
+ listenersToRemove.push(
+ BrowserTestUtils.addContentEventListener(
+ browser1,
+ "focus",
+ compareFocusResults,
+ { capture: true },
+ focusInChild
+ )
+ );
+ listenersToRemove.push(
+ BrowserTestUtils.addContentEventListener(
+ browser1,
+ "blur",
+ compareFocusResults,
+ { capture: true },
+ focusInChild
+ )
+ );
+ listenersToRemove.push(
+ BrowserTestUtils.addContentEventListener(
+ browser2,
+ "focus",
+ compareFocusResults,
+ { capture: true },
+ focusInChild
+ )
+ );
+ listenersToRemove.push(
+ BrowserTestUtils.addContentEventListener(
+ browser2,
+ "blur",
+ compareFocusResults,
+ { capture: true },
+ focusInChild
+ )
+ );
+
+ // Get the content processes to do something, so that we can better
+ // ensure that the listeners added above will have actually been added
+ // in the tabs.
+ await SpecialPowers.spawn(browser1, [], () => {});
+ await SpecialPowers.spawn(browser2, [], () => {});
+
+ _lastfocus = "urlbar";
+ _lastfocuswindow = "main-window";
+
+ window.addEventListener("focus", _browser_tabfocus_test_eventOccured, true);
+ window.addEventListener("blur", _browser_tabfocus_test_eventOccured, true);
+
+ // make sure that the focus initially starts out blank
+ var focusedWindow = {};
+
+ let focused = await getFocusedElementForBrowser(browser1);
+ is(focused, "Focus is <none>", "initial focus in tab 1");
+
+ focused = await getFocusedElementForBrowser(browser2);
+ is(focused, "Focus is <none>", "initial focus in tab 2");
+
+ is(
+ document.activeElement,
+ gURLBar.inputField,
+ "focus after loading two tabs"
+ );
+
+ await expectFocusShiftAfterTabSwitch(
+ tab2,
+ "window2",
+ null,
+ true,
+ "after tab change, focus in new tab"
+ );
+
+ focused = await getFocusedElementForBrowser(browser2);
+ is(
+ focused,
+ "Focus is <none>",
+ "focusedElement after tab change, focus in new tab"
+ );
+
+ // switching tabs when nothing in the new tab is focused
+ // should focus the browser
+ await expectFocusShiftAfterTabSwitch(
+ tab1,
+ "window1",
+ null,
+ true,
+ "after tab change, focus in original tab"
+ );
+
+ focused = await getFocusedElementForBrowser(browser1);
+ is(
+ focused,
+ "Focus is <none>",
+ "focusedElement after tab change, focus in original tab"
+ );
+
+ // focusing a button in the current tab should focus it
+ await expectFocusShift(
+ () => focusElementInChild("button1", "focus"),
+ "window1",
+ "button1",
+ true,
+ "after button focused"
+ );
+
+ focused = await getFocusedElementForBrowser(browser1);
+ is(
+ focused,
+ "Focus is button1",
+ "focusedElement in first browser after button focused"
+ );
+
+ // focusing a button in a background tab should not change the actual
+ // focus, but should set the focus that would be in that background tab to
+ // that button.
+ await expectFocusShift(
+ () => focusElementInChild("button2", "focus"),
+ "window1",
+ "button1",
+ false,
+ "after button focus in unfocused tab"
+ );
+
+ focused = await getFocusedElementForBrowser(browser1, false);
+ is(
+ focused,
+ "Focus is button1",
+ "focusedElement in first browser after button focus in unfocused tab"
+ );
+ focused = await getFocusedElementForBrowser(browser2, true);
+ is(
+ focused,
+ "Focus is button2",
+ "focusedElement in second browser after button focus in unfocused tab"
+ );
+
+ // switching tabs should now make the button in the other tab focused
+ await expectFocusShiftAfterTabSwitch(
+ tab2,
+ "window2",
+ "button2",
+ true,
+ "after tab change with button focused"
+ );
+
+ // blurring an element in a background tab should not change the active
+ // focus, but should clear the focus in that tab.
+ await expectFocusShift(
+ () => focusElementInChild("button1", "blur"),
+ "window2",
+ "button2",
+ false,
+ "focusedWindow after blur in unfocused tab"
+ );
+
+ focused = await getFocusedElementForBrowser(browser1, true);
+ is(
+ focused,
+ "Focus is <none>",
+ "focusedElement in first browser after focus in unfocused tab"
+ );
+ focused = await getFocusedElementForBrowser(browser2, false);
+ is(
+ focused,
+ "Focus is button2",
+ "focusedElement in second browser after focus in unfocused tab"
+ );
+
+ // When focus is in the tab bar, it should be retained there
+ await expectFocusShift(
+ () => gBrowser.selectedTab.focus(),
+ "main-window",
+ "tab2",
+ true,
+ "focusing tab element"
+ );
+ await expectFocusShiftAfterTabSwitch(
+ tab1,
+ "main-window",
+ "tab1",
+ true,
+ "tab change when selected tab element was focused"
+ );
+
+ let switchWaiter = new Promise((resolve, reject) => {
+ gBrowser.addEventListener(
+ "TabSwitchDone",
+ function () {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+
+ await expectFocusShiftAfterTabSwitch(
+ tab2,
+ "main-window",
+ "tab2",
+ true,
+ "another tab change when selected tab element was focused"
+ );
+
+ // Wait for the paint on the second browser so that any post tab-switching
+ // stuff has time to complete before blurring the tab. Otherwise, the
+ // _adjustFocusAfterTabSwitch in tabbrowser gets confused and isn't sure
+ // what tab is really focused.
+ await switchWaiter;
+
+ await expectFocusShift(
+ () => gBrowser.selectedTab.blur(),
+ "main-window",
+ null,
+ true,
+ "blurring tab element"
+ );
+
+ // focusing the url field should switch active focus away from the browser but
+ // not clear what would be the focus in the browser
+ await focusElementInChild("button1", "focus");
+
+ await expectFocusShift(
+ () => gURLBar.focus(),
+ "main-window",
+ "urlbar",
+ true,
+ "focusedWindow after url field focused"
+ );
+ focused = await getFocusedElementForBrowser(browser1, true);
+ is(
+ focused,
+ "Focus is button1",
+ "focusedElement after url field focused, first browser"
+ );
+ focused = await getFocusedElementForBrowser(browser2, true);
+ is(
+ focused,
+ "Focus is button2",
+ "focusedElement after url field focused, second browser"
+ );
+
+ await expectFocusShift(
+ () => gURLBar.blur(),
+ "main-window",
+ null,
+ true,
+ "blurring url field"
+ );
+
+ // when a chrome element is focused, switching tabs to a tab with a button
+ // with the current focus should focus the button
+ await expectFocusShiftAfterTabSwitch(
+ tab1,
+ "window1",
+ "button1",
+ true,
+ "after tab change, focus in url field, button focused in new tab"
+ );
+
+ focused = await getFocusedElementForBrowser(browser1, false);
+ is(
+ focused,
+ "Focus is button1",
+ "after switch tab, focus in unfocused tab, first browser"
+ );
+ focused = await getFocusedElementForBrowser(browser2, true);
+ is(
+ focused,
+ "Focus is button2",
+ "after switch tab, focus in unfocused tab, second browser"
+ );
+
+ // blurring an element in the current tab should clear the active focus
+ await expectFocusShift(
+ () => focusElementInChild("button1", "blur"),
+ "window1",
+ null,
+ true,
+ "after blur in focused tab"
+ );
+
+ focused = await getFocusedElementForBrowser(browser1, false);
+ is(
+ focused,
+ "Focus is <none>",
+ "focusedWindow after blur in focused tab, child"
+ );
+ focusedWindow = {};
+ is(
+ fm.getFocusedElementForWindow(window, false, focusedWindow),
+ browser1,
+ "focusedElement after blur in focused tab, parent"
+ );
+
+ // blurring an non-focused url field should have no effect
+ await expectFocusShift(
+ () => gURLBar.blur(),
+ "window1",
+ null,
+ false,
+ "after blur in unfocused url field"
+ );
+
+ focusedWindow = {};
+ is(
+ fm.getFocusedElementForWindow(window, false, focusedWindow),
+ browser1,
+ "focusedElement after blur in unfocused url field"
+ );
+
+ // switch focus to a tab with a currently focused element
+ await expectFocusShiftAfterTabSwitch(
+ tab2,
+ "window2",
+ "button2",
+ true,
+ "after switch from unfocused to focused tab"
+ );
+ focused = await getFocusedElementForBrowser(browser2, true);
+ is(
+ focused,
+ "Focus is button2",
+ "focusedElement after switch from unfocused to focused tab"
+ );
+
+ // clearing focus on the chrome window should switch the focus to the
+ // chrome window
+ await expectFocusShift(
+ () => fm.clearFocus(window),
+ "main-window",
+ null,
+ true,
+ "after switch to chrome with no focused element"
+ );
+
+ focusedWindow = {};
+ is(
+ fm.getFocusedElementForWindow(window, false, focusedWindow),
+ null,
+ "focusedElement after switch to chrome with no focused element"
+ );
+
+ // switch focus to another tab when neither have an active focus
+ await expectFocusShiftAfterTabSwitch(
+ tab1,
+ "window1",
+ null,
+ true,
+ "focusedWindow after tab switch from no focus to no focus"
+ );
+
+ focused = await getFocusedElementForBrowser(browser1, false);
+ is(
+ focused,
+ "Focus is <none>",
+ "after tab switch from no focus to no focus, first browser"
+ );
+ focused = await getFocusedElementForBrowser(browser2, true);
+ is(
+ focused,
+ "Focus is button2",
+ "after tab switch from no focus to no focus, second browser"
+ );
+
+ // next, check whether navigating forward, focusing the urlbar and then
+ // navigating back maintains the focus in the urlbar.
+ await expectFocusShift(
+ () => focusElementInChild("button1", "focus"),
+ "window1",
+ "button1",
+ true,
+ "focus button"
+ );
+
+ await promiseTabLoadEvent(tab1, "data:text/html," + escape(testPage3));
+
+ // now go back again
+ gURLBar.focus();
+
+ await new Promise((resolve, reject) => {
+ BrowserTestUtils.waitForContentEvent(
+ window.gBrowser.selectedBrowser,
+ "pageshow",
+ true
+ ).then(() => resolve());
+ document.getElementById("Browser:Back").doCommand();
+ });
+
+ is(
+ window.document.activeElement,
+ gURLBar.inputField,
+ "urlbar still focused after navigating back"
+ );
+
+ for (let listener of listenersToRemove) {
+ listener();
+ }
+
+ window.removeEventListener(
+ "focus",
+ _browser_tabfocus_test_eventOccured,
+ true
+ );
+ window.removeEventListener("blur", _browser_tabfocus_test_eventOccured, true);
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+
+ finish();
+});
+
+function _browser_tabfocus_test_eventOccured(event) {
+ function getWindowDocId(target) {
+ if (
+ target == browser1.contentWindow ||
+ target == browser1.contentDocument
+ ) {
+ return "window1";
+ }
+ if (
+ target == browser2.contentWindow ||
+ target == browser2.contentDocument
+ ) {
+ return "window2";
+ }
+ return "main-window";
+ }
+
+ var id;
+
+ if (Window.isInstance(event.target)) {
+ id = getWindowDocId(event.originalTarget) + "-window";
+ } else if (Document.isInstance(event.target)) {
+ id = getWindowDocId(event.originalTarget) + "-document";
+ } else if (
+ event.target.id == "urlbar" &&
+ event.originalTarget.localName == "input"
+ ) {
+ id = "urlbar";
+ } else if (event.originalTarget.localName == "browser") {
+ id = event.originalTarget == browser1 ? "browser1" : "browser2";
+ } else if (event.originalTarget.localName == "tab") {
+ id = event.originalTarget == tab1 ? "tab1" : "tab2";
+ } else {
+ id = event.originalTarget.id;
+ }
+
+ actualEvents.push(event.type + ": " + id);
+ compareFocusResults();
+}
+
+function getId(element) {
+ if (!element) {
+ return null;
+ }
+
+ if (element.localName == "browser") {
+ return element == browser1 ? "browser1" : "browser2";
+ }
+
+ if (element.localName == "tab") {
+ return element == tab1 ? "tab1" : "tab2";
+ }
+
+ return element.localName == "input" ? "urlbar" : element.id;
+}
+
+async function compareFocusResults() {
+ if (!currentPromiseResolver) {
+ return;
+ }
+
+ // Get the events that occurred in each child browser and store them
+ // in 'actualEvents'. This is a global so if different calls to
+ // compareFocusResults occur together, whichever one happens to get
+ // called first after pulling all the events from the child will
+ // perform the matching.
+ let events = await SpecialPowers.spawn(browser1, [], () => {
+ let eventsOccurred = content._eventsOccurred;
+ content._eventsOccurred = [];
+ return eventsOccurred || [];
+ });
+ actualEvents.window1.push(...events);
+
+ events = await SpecialPowers.spawn(browser2, [], () => {
+ let eventsOccurred = content._eventsOccurred;
+ content._eventsOccurred = [];
+ return eventsOccurred || [];
+ });
+ actualEvents.window2.push(...events);
+
+ // Another call to compareFocusResults may have happened in the meantime.
+ // If currentPromiseResolver is null, then that call was successful so no
+ // need to check the events again.
+ if (!currentPromiseResolver) {
+ return;
+ }
+
+ let winIds = ["main-window", "window1", "window2"];
+
+ for (let winId of winIds) {
+ if (actualEvents[winId].length < expectedEvents[winId].length) {
+ return;
+ }
+ }
+
+ for (let winId of winIds) {
+ for (let e = 0; e < expectedEvents.length; e++) {
+ is(
+ actualEvents[winId][e],
+ expectedEvents[winId][e],
+ currentTestName + " events [event " + e + "]"
+ );
+ }
+ actualEvents[winId] = [];
+ }
+
+ let matchWindow = window;
+ is(_expectedWindow, "main-window", "main-window is always expected");
+ if (_expectedWindow == "main-window") {
+ // The browser window's body doesn't have an id set usually - set one now
+ // so it can be used for id comparisons below.
+ matchWindow.document.body.id = "main-window-body";
+ }
+
+ var focusedElement = fm.focusedElement;
+ is(
+ getId(focusedElement),
+ _expectedElement,
+ currentTestName + " focusedElement"
+ );
+
+ is(fm.focusedWindow, matchWindow, currentTestName + " focusedWindow");
+ var focusedWindow = {};
+ is(
+ getId(fm.getFocusedElementForWindow(matchWindow, false, focusedWindow)),
+ _expectedElement,
+ currentTestName + " getFocusedElementForWindow"
+ );
+ is(
+ focusedWindow.value,
+ matchWindow,
+ currentTestName + " getFocusedElementForWindow frame"
+ );
+ is(matchWindow.document.hasFocus(), true, currentTestName + " hasFocus");
+ var expectedActive = _expectedElement;
+ if (!expectedActive) {
+ expectedActive = getId(matchWindow.document.body);
+ }
+ is(
+ getId(matchWindow.document.activeElement),
+ expectedActive,
+ currentTestName + " activeElement"
+ );
+
+ currentPromiseResolver();
+ currentPromiseResolver = null;
+}
+
+async function expectFocusShiftAfterTabSwitch(
+ tab,
+ expectedWindow,
+ expectedElement,
+ focusChanged,
+ testid
+) {
+ let tabSwitchPromise = null;
+ await expectFocusShift(
+ () => {
+ tabSwitchPromise = BrowserTestUtils.switchTab(gBrowser, tab);
+ },
+ expectedWindow,
+ expectedElement,
+ focusChanged,
+ testid
+ );
+ await tabSwitchPromise;
+}
+
+async function expectFocusShift(
+ callback,
+ expectedWindow,
+ expectedElement,
+ focusChanged,
+ testid
+) {
+ currentPromiseResolver = null;
+ currentTestName = testid;
+
+ expectedEvents = new EventStore();
+
+ if (focusChanged) {
+ _expectedElement = expectedElement;
+ _expectedWindow = expectedWindow;
+
+ // When the content is in a child process, the expected element in the chrome window
+ // will always be the urlbar or a browser element.
+ if (_expectedWindow == "window1") {
+ _expectedElement = "browser1";
+ } else if (_expectedWindow == "window2") {
+ _expectedElement = "browser2";
+ }
+ _expectedWindow = "main-window";
+
+ if (
+ _lastfocuswindow != "main-window" &&
+ _lastfocuswindow != expectedWindow
+ ) {
+ let browserid = _lastfocuswindow == "window1" ? "browser1" : "browser2";
+ expectedEvents.push("blur: " + browserid);
+ }
+
+ var newElementIsFocused =
+ expectedElement && !expectedElement.startsWith("html");
+ if (
+ newElementIsFocused &&
+ _lastfocuswindow != "main-window" &&
+ expectedWindow == "main-window"
+ ) {
+ // When switching from a child to a chrome element, the focus on the element will arrive first.
+ expectedEvents.push("focus: " + expectedElement);
+ newElementIsFocused = false;
+ }
+
+ if (_lastfocus && _lastfocus != _expectedElement) {
+ expectedEvents.push("blur: " + _lastfocus);
+ }
+
+ if (_lastfocuswindow && _lastfocuswindow != expectedWindow) {
+ if (_lastfocuswindow != "main-window") {
+ expectedEvents.push("blur: " + _lastfocuswindow + "-document");
+ expectedEvents.push("blur: " + _lastfocuswindow + "-window");
+ }
+ }
+
+ if (expectedWindow && _lastfocuswindow != expectedWindow) {
+ if (expectedWindow != "main-window") {
+ let browserid = expectedWindow == "window1" ? "browser1" : "browser2";
+ expectedEvents.push("focus: " + browserid);
+ }
+
+ if (expectedWindow != "main-window") {
+ expectedEvents.push("focus: " + expectedWindow + "-document");
+ expectedEvents.push("focus: " + expectedWindow + "-window");
+ }
+ }
+
+ if (newElementIsFocused) {
+ expectedEvents.push("focus: " + expectedElement);
+ }
+
+ _lastfocus = expectedElement;
+ _lastfocuswindow = expectedWindow;
+ }
+
+ // No events are expected, so return immediately. If events do occur, the following
+ // tests will fail.
+ if (
+ expectedEvents["main-window"].length +
+ expectedEvents.window1.length +
+ expectedEvents.window2.length ==
+ 0
+ ) {
+ await callback();
+ return undefined;
+ }
+
+ return new Promise(resolve => {
+ currentPromiseResolver = resolve;
+ callback();
+ });
+}
diff --git a/browser/base/content/test/general/browser_tabs_close_beforeunload.js b/browser/base/content/test/general/browser_tabs_close_beforeunload.js
new file mode 100644
index 0000000000..0534250970
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabs_close_beforeunload.js
@@ -0,0 +1,69 @@
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+});
+
+const FIRST_TAB =
+ getRootDirectory(gTestPath) + "close_beforeunload_opens_second_tab.html";
+const SECOND_TAB = getRootDirectory(gTestPath) + "close_beforeunload.html";
+
+add_task(async function () {
+ info("Opening first tab");
+ let firstTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FIRST_TAB
+ );
+ let secondTabLoadedPromise;
+ let secondTab;
+ let tabOpened = new Promise(resolve => {
+ info("Adding tabopen listener");
+ gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ function tabOpenListener(e) {
+ info("Got tabopen, removing listener and waiting for load");
+ gBrowser.tabContainer.removeEventListener(
+ "TabOpen",
+ tabOpenListener,
+ false,
+ false
+ );
+ secondTab = e.target;
+ secondTabLoadedPromise = BrowserTestUtils.browserLoaded(
+ secondTab.linkedBrowser,
+ false,
+ SECOND_TAB
+ );
+ resolve();
+ },
+ false,
+ false
+ );
+ });
+ info("Opening second tab using a click");
+ await SpecialPowers.spawn(firstTab.linkedBrowser, [""], async function () {
+ content.document.getElementsByTagName("a")[0].click();
+ });
+ info("Waiting for the second tab to be opened");
+ await tabOpened;
+ info("Waiting for the load in that tab to finish");
+ await secondTabLoadedPromise;
+
+ let closeBtn = secondTab.closeButton;
+ info("closing second tab (which will self-close in beforeunload)");
+ closeBtn.click();
+ ok(
+ secondTab.closing,
+ "Second tab should be marked as closing synchronously."
+ );
+ ok(!secondTab.linkedBrowser, "Second tab's browser should be dead");
+ ok(!firstTab.closing, "First tab should not be closing");
+ ok(firstTab.linkedBrowser, "First tab's browser should be alive");
+ info("closing first tab");
+ BrowserTestUtils.removeTab(firstTab);
+
+ ok(firstTab.closing, "First tab should be marked as closing");
+ ok(!firstTab.linkedBrowser, "First tab's browser should be dead");
+});
diff --git a/browser/base/content/test/general/browser_tabs_isActive.js b/browser/base/content/test/general/browser_tabs_isActive.js
new file mode 100644
index 0000000000..3d485b01c1
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabs_isActive.js
@@ -0,0 +1,235 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// Test for the docshell active state of local and remote browsers.
+
+const kTestPage =
+ "https://example.org/browser/browser/base/content/test/general/dummy_page.html";
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener(
+ "TabSwitchDone",
+ function () {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function getParentTabState(aTab) {
+ return aTab.linkedBrowser.docShellIsActive;
+}
+
+function getChildTabState(aTab) {
+ return ContentTask.spawn(
+ aTab.linkedBrowser,
+ null,
+ () => content.browsingContext.isActive
+ );
+}
+
+function checkState(parentSide, childSide, value, message) {
+ is(parentSide, value, message + " (parent side)");
+ is(childSide, value, message + " (child side)");
+}
+
+function waitForMs(aMs) {
+ return new Promise(resolve => {
+ setTimeout(done, aMs);
+ function done() {
+ resolve(true);
+ }
+ });
+}
+
+add_task(async function () {
+ let url = kTestPage;
+ let originalTab = gBrowser.selectedTab; // test tab
+ let newTab = BrowserTestUtils.addTab(gBrowser, url, { skipAnimation: true });
+ let parentSide, childSide;
+
+ // new tab added but not selected checks
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "newly added " + url + " tab is not active"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(parentSide, childSide, true, "original tab is active initially");
+
+ // select the newly added tab and wait for TabSwitchDone event
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ await tabSwitchedPromise;
+
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ ok(
+ newTab.linkedBrowser.isRemoteBrowser,
+ "for testing we need a remote tab"
+ );
+ }
+
+ // check active state of both tabs
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ true,
+ "newly added " + url + " tab is active after selection"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "original tab is not active while unselected"
+ );
+
+ // switch back to the original test tab and wait for TabSwitchDone event
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = originalTab;
+ await tabSwitchedPromise;
+
+ // check active state of both tabs
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "newly added " + url + " tab is not active after switch back"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(
+ parentSide,
+ childSide,
+ true,
+ "original tab is active again after switch back"
+ );
+
+ // switch to the new tab and wait for TabSwitchDone event
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ await tabSwitchedPromise;
+
+ // check active state of both tabs
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ true,
+ "newly added " + url + " tab is not active after switch back"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "original tab is active again after switch back"
+ );
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(async function () {
+ let url = "about:about";
+ let originalTab = gBrowser.selectedTab; // test tab
+ let newTab = BrowserTestUtils.addTab(gBrowser, url, { skipAnimation: true });
+ let parentSide, childSide;
+
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "newly added " + url + " tab is not active"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(parentSide, childSide, true, "original tab is active initially");
+
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ await tabSwitchedPromise;
+
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ ok(
+ !newTab.linkedBrowser.isRemoteBrowser,
+ "for testing we need a local tab"
+ );
+ }
+
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ true,
+ "newly added " + url + " tab is active after selection"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "original tab is not active while unselected"
+ );
+
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = originalTab;
+ await tabSwitchedPromise;
+
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "newly added " + url + " tab is not active after switch back"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(
+ parentSide,
+ childSide,
+ true,
+ "original tab is active again after switch back"
+ );
+
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ await tabSwitchedPromise;
+
+ parentSide = getParentTabState(newTab);
+ childSide = await getChildTabState(newTab);
+ checkState(
+ parentSide,
+ childSide,
+ true,
+ "newly added " + url + " tab is not active after switch back"
+ );
+ parentSide = getParentTabState(originalTab);
+ childSide = await getChildTabState(originalTab);
+ checkState(
+ parentSide,
+ childSide,
+ false,
+ "original tab is active again after switch back"
+ );
+
+ gBrowser.removeTab(newTab);
+});
diff --git a/browser/base/content/test/general/browser_tabs_owner.js b/browser/base/content/test/general/browser_tabs_owner.js
new file mode 100644
index 0000000000..4a32da12f1
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabs_owner.js
@@ -0,0 +1,40 @@
+function test() {
+ BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.addTab(gBrowser);
+
+ var owner;
+
+ is(gBrowser.tabs.length, 4, "4 tabs are open");
+
+ owner = gBrowser.selectedTab = gBrowser.tabs[2];
+ BrowserOpenTab();
+ is(gBrowser.selectedTab, gBrowser.tabs[4], "newly opened tab is selected");
+ gBrowser.removeCurrentTab();
+ is(gBrowser.selectedTab, owner, "owner is selected");
+
+ owner = gBrowser.selectedTab;
+ BrowserOpenTab();
+ gBrowser.selectedTab = gBrowser.tabs[1];
+ gBrowser.selectedTab = gBrowser.tabs[4];
+ gBrowser.removeCurrentTab();
+ isnot(
+ gBrowser.selectedTab,
+ owner,
+ "selecting a different tab clears the owner relation"
+ );
+
+ owner = gBrowser.selectedTab;
+ BrowserOpenTab();
+ gBrowser.moveTabTo(gBrowser.selectedTab, 0);
+ gBrowser.removeCurrentTab();
+ is(
+ gBrowser.selectedTab,
+ owner,
+ "owner relationship persists when tab is moved"
+ );
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
diff --git a/browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js b/browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js
new file mode 100644
index 0000000000..d5144b47b0
--- /dev/null
+++ b/browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const OPEN_LOCATION_PREF = "browser.link.open_newwindow";
+const NON_REMOTE_PAGE = "about:welcomeback";
+
+requestLongerTimeout(2);
+
+function insertAndClickAnchor(browser) {
+ return SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML = `
+ <a href="http://example.com/" target="_blank" rel="opener" id="testAnchor">Open a window</a>
+ `;
+
+ let element = content.document.getElementById("testAnchor");
+ element.click();
+ });
+}
+
+/**
+ * Takes some browser in some window, and forces that browser
+ * to become non-remote, and then navigates it to a page that
+ * we're not supposed to be displaying remotely. Returns a
+ * Promise that resolves when the browser is no longer remote.
+ */
+function prepareNonRemoteBrowser(aWindow, browser) {
+ BrowserTestUtils.loadURIString(browser, NON_REMOTE_PAGE);
+ return BrowserTestUtils.browserLoaded(browser);
+}
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(OPEN_LOCATION_PREF);
+});
+
+/**
+ * Test that if we open a new tab from a link in a non-remote
+ * browser in an e10s window, that the new tab will load properly.
+ */
+add_task(async function test_new_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.skip_html_fragment_assertion", true]],
+ });
+
+ let normalWindow = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ });
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ private: true,
+ });
+
+ for (let testWindow of [normalWindow, privateWindow]) {
+ await promiseWaitForFocus(testWindow);
+ let testBrowser = testWindow.gBrowser.selectedBrowser;
+ info("Preparing non-remote browser");
+ await prepareNonRemoteBrowser(testWindow, testBrowser);
+ info("Non-remote browser prepared");
+
+ let tabOpenEventPromise = waitForNewTabEvent(testWindow.gBrowser);
+ await insertAndClickAnchor(testBrowser);
+
+ let newTab = (await tabOpenEventPromise).target;
+ await promiseTabLoadEvent(newTab);
+
+ // insertAndClickAnchor causes an open to a web page which
+ // means that the tab should eventually become remote.
+ ok(
+ newTab.linkedBrowser.isRemoteBrowser,
+ "The opened browser never became remote."
+ );
+
+ testWindow.gBrowser.removeTab(newTab);
+ }
+
+ normalWindow.close();
+ privateWindow.close();
+});
+
+/**
+ * Test that if we open a new window from a link in a non-remote
+ * browser in an e10s window, that the new window is not an e10s
+ * window. Also tests with a private browsing window.
+ */
+add_task(async function test_new_window() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.skip_html_fragment_assertion", true]],
+ });
+
+ let normalWindow = await BrowserTestUtils.openNewBrowserWindow(
+ {
+ remote: true,
+ },
+ true
+ );
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow(
+ {
+ remote: true,
+ private: true,
+ },
+ true
+ );
+
+ // Fiddle with the prefs so that we open target="_blank" links
+ // in new windows instead of new tabs.
+ Services.prefs.setIntPref(
+ OPEN_LOCATION_PREF,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW
+ );
+
+ for (let testWindow of [normalWindow, privateWindow]) {
+ await promiseWaitForFocus(testWindow);
+ let testBrowser = testWindow.gBrowser.selectedBrowser;
+ await prepareNonRemoteBrowser(testWindow, testBrowser);
+
+ await insertAndClickAnchor(testBrowser);
+
+ // Click on the link in the browser, and wait for the new window.
+ let [newWindow] = await TestUtils.topicObserved(
+ "browser-delayed-startup-finished"
+ );
+
+ is(
+ PrivateBrowsingUtils.isWindowPrivate(testWindow),
+ PrivateBrowsingUtils.isWindowPrivate(newWindow),
+ "Private browsing state of new window does not match the original!"
+ );
+
+ let newTab = newWindow.gBrowser.selectedTab;
+
+ await promiseTabLoadEvent(newTab);
+
+ // insertAndClickAnchor causes an open to a web page which
+ // means that the tab should eventually become remote.
+ ok(
+ newTab.linkedBrowser.isRemoteBrowser,
+ "The opened browser never became remote."
+ );
+ newWindow.close();
+ }
+
+ normalWindow.close();
+ privateWindow.close();
+});
diff --git a/browser/base/content/test/general/browser_typeAheadFind.js b/browser/base/content/test/general/browser_typeAheadFind.js
new file mode 100644
index 0000000000..d68de34333
--- /dev/null
+++ b/browser/base/content/test/general/browser_typeAheadFind.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 () {
+ let testWindow = await BrowserTestUtils.openNewBrowserWindow();
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ testWindow.gBrowser.selectedTab.focus();
+
+ BrowserTestUtils.loadURIString(
+ testWindow.gBrowser,
+ "data:text/html,<h1>A Page</h1>"
+ );
+ await BrowserTestUtils.browserLoaded(testWindow.gBrowser.selectedBrowser);
+
+ await SimpleTest.promiseFocus(testWindow.gBrowser.selectedBrowser);
+
+ ok(!testWindow.gFindBarInitialized, "find bar is not initialized");
+
+ let findBarOpenPromise = BrowserTestUtils.waitForEvent(
+ testWindow.gBrowser,
+ "findbaropen"
+ );
+ EventUtils.synthesizeKey("/", {}, testWindow);
+ await findBarOpenPromise;
+
+ ok(testWindow.gFindBarInitialized, "find bar is now initialized");
+
+ await BrowserTestUtils.closeWindow(testWindow);
+});
diff --git a/browser/base/content/test/general/browser_unknownContentType_title.js b/browser/base/content/test/general/browser_unknownContentType_title.js
new file mode 100644
index 0000000000..be55f06fae
--- /dev/null
+++ b/browser/base/content/test/general/browser_unknownContentType_title.js
@@ -0,0 +1,88 @@
+const url =
+ "data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3Ctitle%3ETest%20Page%3C%2Ftitle%3E%3C%2Fhead%3E%3C%2Fhtml%3E";
+const unknown_url =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/general/unknownContentType_file.pif";
+
+function waitForNewWindow() {
+ return new Promise(resolve => {
+ let listener = win => {
+ Services.obs.removeObserver(listener, "toplevel-window-ready");
+ win.addEventListener("load", () => {
+ resolve(win);
+ });
+ };
+
+ Services.obs.addObserver(listener, "toplevel-window-ready");
+ });
+}
+
+add_setup(async function () {
+ let tmpDir = PathUtils.join(
+ PathUtils.tempDir,
+ "testsavedir" + Math.floor(Math.random() * 2 ** 32)
+ );
+ // Create this dir if it doesn't exist (ignores existing dirs)
+ await IOUtils.makeDirectory(tmpDir);
+ registerCleanupFunction(async function () {
+ try {
+ await IOUtils.remove(tmpDir, { recursive: true });
+ } catch (e) {
+ console.error(e);
+ }
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+});
+
+add_task(async function unknownContentType_title_with_pref_enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", true]],
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url));
+ let browser = tab.linkedBrowser;
+ await promiseTabLoaded(gBrowser.selectedTab);
+
+ is(gBrowser.contentTitle, "Test Page", "Should have the right title.");
+
+ BrowserTestUtils.loadURIString(browser, unknown_url);
+ let win = await waitForNewWindow();
+ is(
+ win.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialog."
+ );
+ is(gBrowser.contentTitle, "Test Page", "Should still have the right title.");
+
+ win.close();
+ await promiseWaitForFocus(window);
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function unknownContentType_title_with_pref_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url));
+ let browser = tab.linkedBrowser;
+ await promiseTabLoaded(gBrowser.selectedTab);
+
+ is(gBrowser.contentTitle, "Test Page", "Should have the right title.");
+
+ BrowserTestUtils.loadURIString(browser, unknown_url);
+ // If the pref is disabled, then the downloads panel should open right away
+ // since there is no UCT window prompt to block it.
+ let waitForPanelShown = BrowserTestUtils.waitForCondition(() => {
+ return DownloadsPanel.isPanelShowing;
+ }).then(() => "panel-shown");
+
+ let panelShown = await waitForPanelShown;
+ is(panelShown, "panel-shown", "The downloads panel is shown");
+ is(gBrowser.contentTitle, "Test Page", "Should still have the right title.");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_unloaddialogs.js b/browser/base/content/test/general/browser_unloaddialogs.js
new file mode 100644
index 0000000000..7e0b48392b
--- /dev/null
+++ b/browser/base/content/test/general/browser_unloaddialogs.js
@@ -0,0 +1,40 @@
+var testUrls = [
+ "data:text/html,<script>" +
+ "function handle(evt) {" +
+ "evt.target.removeEventListener(evt.type, handle, true);" +
+ "try { alert('This should NOT appear'); } catch(e) { }" +
+ "}" +
+ "window.addEventListener('pagehide', handle, true);" +
+ "window.addEventListener('beforeunload', handle, true);" +
+ "window.addEventListener('unload', handle, true);" +
+ "</script><body>Testing alert during pagehide/beforeunload/unload</body>",
+ "data:text/html,<script>" +
+ "function handle(evt) {" +
+ "evt.target.removeEventListener(evt.type, handle, true);" +
+ "try { prompt('This should NOT appear'); } catch(e) { }" +
+ "}" +
+ "window.addEventListener('pagehide', handle, true);" +
+ "window.addEventListener('beforeunload', handle, true);" +
+ "window.addEventListener('unload', handle, true);" +
+ "</script><body>Testing prompt during pagehide/beforeunload/unload</body>",
+ "data:text/html,<script>" +
+ "function handle(evt) {" +
+ "evt.target.removeEventListener(evt.type, handle, true);" +
+ "try { confirm('This should NOT appear'); } catch(e) { }" +
+ "}" +
+ "window.addEventListener('pagehide', handle, true);" +
+ "window.addEventListener('beforeunload', handle, true);" +
+ "window.addEventListener('unload', handle, true);" +
+ "</script><body>Testing confirm during pagehide/beforeunload/unload</body>",
+];
+
+add_task(async function () {
+ for (let url of testUrls) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ ok(true, "Loaded page " + url);
+ // Wait one turn of the event loop before closing, so everything settles.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ BrowserTestUtils.removeTab(tab);
+ ok(true, "Closed page " + url + " without timeout");
+ }
+});
diff --git a/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js b/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js
new file mode 100644
index 0000000000..6c62670e6f
--- /dev/null
+++ b/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js
@@ -0,0 +1,60 @@
+function wait_while_tab_is_busy() {
+ return new Promise(resolve => {
+ let progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ gBrowser.removeProgressListener(this);
+ setTimeout(resolve, 0);
+ }
+ },
+ };
+ gBrowser.addProgressListener(progressListener);
+ });
+}
+
+// This function waits for the tab to stop being busy instead of waiting for it
+// to load, since the _elementsForViewSource change happens at that time.
+var with_new_tab_opened = async function (options, taskFn) {
+ let busyPromise = wait_while_tab_is_busy();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ options.gBrowser,
+ options.url,
+ false
+ );
+ await busyPromise;
+ await taskFn(tab.linkedBrowser);
+ gBrowser.removeTab(tab);
+};
+
+add_task(async function test_regular_page() {
+ function test_expect_view_source_enabled(browser) {
+ for (let element of [...XULBrowserWindow._elementsForViewSource]) {
+ ok(!element.hasAttribute("disabled"), "View Source should be enabled");
+ }
+ }
+
+ await with_new_tab_opened(
+ {
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com",
+ },
+ test_expect_view_source_enabled
+ );
+});
+
+add_task(async function test_view_source_page() {
+ function test_expect_view_source_disabled(browser) {
+ for (let element of [...XULBrowserWindow._elementsForViewSource]) {
+ ok(element.hasAttribute("disabled"), "View Source should be disabled");
+ }
+ }
+
+ await with_new_tab_opened(
+ {
+ gBrowser,
+ url: "view-source:http://example.com",
+ },
+ test_expect_view_source_disabled
+ );
+});
diff --git a/browser/base/content/test/general/browser_visibleFindSelection.js b/browser/base/content/test/general/browser_visibleFindSelection.js
new file mode 100644
index 0000000000..56099521e2
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleFindSelection.js
@@ -0,0 +1,62 @@
+add_task(async function () {
+ const childContent =
+ "<div style='position: absolute; left: 2200px; background: green; width: 200px; height: 200px;'>" +
+ "div</div><div style='position: absolute; left: 0px; background: red; width: 200px; height: 200px;'>" +
+ "<span id='s'>div</span></div>";
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ await promiseTabLoadEvent(
+ tab,
+ "data:text/html;charset=utf-8," + escape(childContent)
+ );
+ await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
+
+ let remote = gBrowser.selectedBrowser.isRemoteBrowser;
+
+ let findBarOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "findbaropen"
+ );
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ await findBarOpenPromise;
+
+ ok(gFindBarInitialized, "find bar is now initialized");
+
+ // Finds the div in the green box.
+ let scrollPromise = remote
+ ? BrowserTestUtils.waitForContentEvent(gBrowser.selectedBrowser, "scroll")
+ : BrowserTestUtils.waitForEvent(gBrowser, "scroll");
+ EventUtils.sendString("div");
+ await scrollPromise;
+
+ // Wait for one paint to ensure we've processed the previous key events and scrolling.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ return new Promise(resolve => {
+ content.requestAnimationFrame(() => {
+ content.setTimeout(resolve, 0);
+ });
+ });
+ });
+
+ // Finds the div in the red box.
+ scrollPromise = remote
+ ? BrowserTestUtils.waitForContentEvent(gBrowser.selectedBrowser, "scroll")
+ : BrowserTestUtils.waitForEvent(gBrowser, "scroll");
+ EventUtils.synthesizeKey("g", { accelKey: true });
+ await scrollPromise;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ Assert.ok(
+ content.document.getElementById("s").getBoundingClientRect().left >= 0,
+ "scroll should include find result"
+ );
+ });
+
+ // clear the find bar
+ EventUtils.synthesizeKey("a", { accelKey: true });
+ EventUtils.synthesizeKey("KEY_Delete");
+
+ gFindBar.close();
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_visibleTabs.js b/browser/base/content/test/general/browser_visibleTabs.js
new file mode 100644
index 0000000000..7bf7dc1387
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs.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/. */
+
+"use strict";
+
+add_task(async function () {
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+
+ // Add a tab that will get pinned
+ let pinned = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.pinTab(pinned);
+
+ let testTab = BrowserTestUtils.addTab(gBrowser);
+
+ let firefoxViewTab = BrowserTestUtils.addTab(gBrowser, "about:firefoxview");
+ gBrowser.hideTab(firefoxViewTab);
+
+ let visible = gBrowser.visibleTabs;
+ is(visible.length, 3, "3 tabs should be visible");
+ is(visible[0], pinned, "the pinned tab is first");
+ is(visible[1], origTab, "original tab is next");
+ is(visible[2], testTab, "last created tab is next to last");
+
+ // Only show the test tab (but also get pinned and selected)
+ is(
+ gBrowser.selectedTab,
+ origTab,
+ "sanity check that we're on the original tab"
+ );
+ gBrowser.showOnlyTheseTabs([testTab]);
+ is(gBrowser.visibleTabs.length, 3, "all 3 tabs are still visible");
+
+ // Select the test tab and only show that (and pinned)
+ gBrowser.selectedTab = testTab;
+ gBrowser.showOnlyTheseTabs([testTab]);
+
+ visible = gBrowser.visibleTabs;
+ is(visible.length, 2, "2 tabs should be visible including the pinned");
+ is(visible[0], pinned, "first is pinned");
+ is(visible[1], testTab, "next is the test tab");
+ is(gBrowser.tabs.length, 4, "4 tabs should still be open");
+
+ gBrowser.selectTabAtIndex(1);
+ is(gBrowser.selectedTab, testTab, "second tab is the test tab");
+ gBrowser.selectTabAtIndex(0);
+ is(gBrowser.selectedTab, pinned, "first tab is pinned");
+ gBrowser.selectTabAtIndex(2);
+ is(gBrowser.selectedTab, testTab, "no third tab, so no change");
+ gBrowser.selectTabAtIndex(0);
+ is(gBrowser.selectedTab, pinned, "switch back to the pinned");
+ gBrowser.selectTabAtIndex(2);
+ is(gBrowser.selectedTab, testTab, "no third tab, so select last tab");
+ gBrowser.selectTabAtIndex(-2);
+ is(
+ gBrowser.selectedTab,
+ pinned,
+ "pinned tab is second from left (when orig tab is hidden)"
+ );
+ gBrowser.selectTabAtIndex(-1);
+ is(gBrowser.selectedTab, testTab, "last tab is the test tab");
+
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, pinned, "wrapped around the end to pinned");
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, testTab, "next to test tab");
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, pinned, "next to pinned again");
+
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, testTab, "going backwards to last tab");
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, pinned, "next to pinned");
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, testTab, "next to test tab again");
+
+ // select a hidden tab thats selectable
+ gBrowser.selectedTab = firefoxViewTab;
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, pinned, "next to first visible tab, the pinned tab");
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, testTab, "next to second visible tab, the test tab");
+
+ // again select a hidden tab thats selectable
+ gBrowser.selectedTab = firefoxViewTab;
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, testTab, "next to last visible tab, the test tab");
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, pinned, "next to first visible tab, the pinned tab");
+
+ // Try showing all tabs except for the Firefox View tab
+ gBrowser.showOnlyTheseTabs(Array.from(gBrowser.tabs.slice(0, 3)));
+ is(gBrowser.visibleTabs.length, 3, "all 3 tabs are visible again");
+
+ // Select the pinned tab and show the testTab to make sure selection updates
+ gBrowser.selectedTab = pinned;
+ gBrowser.showOnlyTheseTabs([testTab]);
+ is(gBrowser.tabs[1], origTab, "make sure origTab is in the middle");
+ is(origTab.hidden, true, "make sure it's hidden");
+ gBrowser.removeTab(pinned);
+ is(gBrowser.selectedTab, testTab, "making sure origTab was skipped");
+ is(gBrowser.visibleTabs.length, 1, "only testTab is there");
+
+ // Only show one of the non-pinned tabs (but testTab is selected)
+ gBrowser.showOnlyTheseTabs([origTab]);
+ is(gBrowser.visibleTabs.length, 2, "got 2 tabs");
+
+ // Now really only show one of the tabs
+ gBrowser.showOnlyTheseTabs([testTab]);
+ visible = gBrowser.visibleTabs;
+ is(visible.length, 1, "only the original tab is visible");
+ is(visible[0], testTab, "it's the original tab");
+ is(gBrowser.tabs.length, 3, "still have 3 open tabs");
+
+ // Close the selectable hidden tab
+ gBrowser.removeTab(firefoxViewTab);
+
+ // Close the last visible tab and make sure we still get a visible tab
+ gBrowser.removeTab(testTab);
+ is(gBrowser.visibleTabs.length, 1, "only orig is left and visible");
+ is(gBrowser.tabs.length, 1, "sanity check that it matches");
+ is(gBrowser.selectedTab, origTab, "got the orig tab");
+ is(origTab.hidden, false, "and it's not hidden -- visible!");
+});
diff --git a/browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js b/browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js
new file mode 100644
index 0000000000..2c0002fc44
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ let tabOne = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let tabTwo = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/");
+ gBrowser.selectedTab = tabTwo;
+
+ let browser = gBrowser.getBrowserForTab(tabTwo);
+ BrowserTestUtils.browserLoaded(browser).then(() => {
+ gBrowser.showOnlyTheseTabs([tabTwo]);
+
+ is(gBrowser.visibleTabs.length, 1, "Only one tab is visible");
+
+ let uris = PlacesCommandHook.uniqueCurrentPages;
+ is(uris.length, 1, "Only one uri is returned");
+
+ is(
+ uris[0].uri.spec,
+ tabTwo.linkedBrowser.currentURI.spec,
+ "It's the correct URI"
+ );
+
+ gBrowser.removeTab(tabOne);
+ gBrowser.removeTab(tabTwo);
+ for (let tab of gBrowser.tabs) {
+ gBrowser.showTab(tab);
+ }
+
+ finish();
+ });
+}
diff --git a/browser/base/content/test/general/browser_visibleTabs_tabPreview.js b/browser/base/content/test/general/browser_visibleTabs_tabPreview.js
new file mode 100644
index 0000000000..ecc7228d65
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs_tabPreview.js
@@ -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/. */
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.ctrlTab.sortByRecentlyUsed", true]],
+ });
+
+ let [origTab] = gBrowser.visibleTabs;
+ let tabOne = BrowserTestUtils.addTab(gBrowser);
+ let tabTwo = BrowserTestUtils.addTab(gBrowser);
+
+ // test the ctrlTab.tabList
+ pressCtrlTab();
+ ok(ctrlTab.isOpen, "With 3 tab open, Ctrl+Tab opens the preview panel");
+ is(ctrlTab.tabList.length, 3, "Ctrl+Tab panel displays all visible tabs");
+ releaseCtrl();
+
+ gBrowser.showOnlyTheseTabs([origTab]);
+ pressCtrlTab();
+ ok(
+ !ctrlTab.isOpen,
+ "With 1 tab open, Ctrl+Tab doesn't open the preview panel"
+ );
+ releaseCtrl();
+
+ gBrowser.showOnlyTheseTabs([origTab, tabOne, tabTwo]);
+ pressCtrlTab();
+ ok(
+ ctrlTab.isOpen,
+ "Ctrl+Tab opens the preview panel after re-showing hidden tabs"
+ );
+ is(
+ ctrlTab.tabList.length,
+ 3,
+ "Ctrl+Tab panel displays all visible tabs after re-showing hidden ones"
+ );
+ releaseCtrl();
+
+ // cleanup
+ gBrowser.removeTab(tabOne);
+ gBrowser.removeTab(tabTwo);
+});
+
+function pressCtrlTab(aShiftKey) {
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: !!aShiftKey });
+}
+
+function releaseCtrl() {
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+}
diff --git a/browser/base/content/test/general/browser_windowactivation.js b/browser/base/content/test/general/browser_windowactivation.js
new file mode 100644
index 0000000000..f5d30d7ac9
--- /dev/null
+++ b/browser/base/content/test/general/browser_windowactivation.js
@@ -0,0 +1,112 @@
+/*
+ * This test checks that window activation state is set properly with multiple tabs.
+ */
+
+const testPageChrome =
+ getRootDirectory(gTestPath) + "file_window_activation.html";
+const testPageHttp = testPageChrome.replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const testPageWindow =
+ getRootDirectory(gTestPath) + "file_window_activation2.html";
+
+add_task(async function reallyRunTests() {
+ let chromeTab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ testPageChrome
+ );
+ let chromeBrowser1 = chromeTab1.linkedBrowser;
+
+ // This can't use openNewForegroundTab because if we focus chromeTab2 now, we
+ // won't send a focus event during test 6, further down in this file.
+ let chromeTab2 = BrowserTestUtils.addTab(gBrowser, testPageChrome);
+ let chromeBrowser2 = chromeTab2.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(chromeBrowser2);
+
+ let httpTab = BrowserTestUtils.addTab(gBrowser, testPageHttp);
+ let httpBrowser = httpTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(httpBrowser);
+
+ function failTest() {
+ ok(false, "Test received unexpected activate/deactivate event");
+ }
+
+ // chrome:// url tabs should not receive "activate" or "deactivate" events
+ // as they should be sent to the top-level window in the parent process.
+ for (let b of [chromeBrowser1, chromeBrowser2]) {
+ BrowserTestUtils.waitForContentEvent(b, "activate", true).then(failTest);
+ BrowserTestUtils.waitForContentEvent(b, "deactivate", true).then(failTest);
+ }
+
+ gURLBar.focus();
+
+ gBrowser.selectedTab = chromeTab1;
+
+ // The test performs four checks, using -moz-window-inactive on three child
+ // tabs (2 loading chrome:// urls and one loading an http:// url).
+ // First, the initial state should be transparent. The second check is done
+ // while another window is focused. The third check is done after that window
+ // is closed and the main window focused again. The fourth check is done after
+ // switching to the second tab.
+
+ // Step 1 - check the initial state
+ let colorChromeBrowser1 = await getBackgroundColor(chromeBrowser1, true);
+ let colorChromeBrowser2 = await getBackgroundColor(chromeBrowser2, true);
+ let colorHttpBrowser = await getBackgroundColor(httpBrowser, true);
+ is(colorChromeBrowser1, "rgba(0, 0, 0, 0)", "first tab initial");
+ is(colorChromeBrowser2, "rgba(0, 0, 0, 0)", "second tab initial");
+ is(colorHttpBrowser, "rgba(0, 0, 0, 0)", "third tab initial");
+
+ // Step 2 - open and focus another window
+ let otherWindow = window.open(testPageWindow, "", "chrome");
+ await SimpleTest.promiseFocus(otherWindow);
+ colorChromeBrowser1 = await getBackgroundColor(chromeBrowser1, false);
+ colorChromeBrowser2 = await getBackgroundColor(chromeBrowser2, false);
+ colorHttpBrowser = await getBackgroundColor(httpBrowser, false);
+ is(colorChromeBrowser1, "rgb(255, 0, 0)", "first tab lowered");
+ is(colorChromeBrowser2, "rgb(255, 0, 0)", "second tab lowered");
+ is(colorHttpBrowser, "rgb(255, 0, 0)", "third tab lowered");
+
+ // Step 3 - close the other window again
+ otherWindow.close();
+ colorChromeBrowser1 = await getBackgroundColor(chromeBrowser1, true);
+ colorChromeBrowser2 = await getBackgroundColor(chromeBrowser2, true);
+ colorHttpBrowser = await getBackgroundColor(httpBrowser, true);
+ is(colorChromeBrowser1, "rgba(0, 0, 0, 0)", "first tab raised");
+ is(colorChromeBrowser2, "rgba(0, 0, 0, 0)", "second tab raised");
+ is(colorHttpBrowser, "rgba(0, 0, 0, 0)", "third tab raised");
+
+ // Step 4 - switch to the second tab
+ gBrowser.selectedTab = chromeTab2;
+ colorChromeBrowser1 = await getBackgroundColor(chromeBrowser1, true);
+ colorChromeBrowser2 = await getBackgroundColor(chromeBrowser2, true);
+ colorHttpBrowser = await getBackgroundColor(httpBrowser, true);
+ is(colorChromeBrowser1, "rgba(0, 0, 0, 0)", "first tab after tab switch");
+ is(colorChromeBrowser2, "rgba(0, 0, 0, 0)", "second tab after tab switch");
+ is(colorHttpBrowser, "rgba(0, 0, 0, 0)", "third tab after tab switch");
+
+ BrowserTestUtils.removeTab(chromeTab1);
+ BrowserTestUtils.removeTab(chromeTab2);
+ BrowserTestUtils.removeTab(httpTab);
+ otherWindow = null;
+});
+
+function getBackgroundColor(browser, expectedActive) {
+ return SpecialPowers.spawn(
+ browser,
+ [!expectedActive],
+ async hasPseudoClass => {
+ let area = content.document.getElementById("area");
+ await ContentTaskUtils.waitForCondition(() => {
+ return area;
+ }, "Page has loaded");
+ await ContentTaskUtils.waitForCondition(() => {
+ return area.matches(":-moz-window-inactive") == hasPseudoClass;
+ }, `Window is considered ${hasPseudoClass ? "inactive" : "active"}`);
+
+ return content.getComputedStyle(area).backgroundColor;
+ }
+ );
+}
diff --git a/browser/base/content/test/general/browser_zbug569342.js b/browser/base/content/test/general/browser_zbug569342.js
new file mode 100644
index 0000000000..4aa6bfbb9c
--- /dev/null
+++ b/browser/base/content/test/general/browser_zbug569342.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function findBarDisabledOnSomePages() {
+ ok(!gFindBar || gFindBar.hidden, "Find bar should not be visible by default");
+
+ let findbarOpenedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabFindInitialized"
+ );
+ document.documentElement.focus();
+ // Open the Find bar before we navigate to pages that shouldn't have it.
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ await findbarOpenedPromise;
+ ok(!gFindBar.hidden, "Find bar should be visible");
+
+ let urls = ["about:preferences", "about:addons"];
+
+ for (let url of urls) {
+ await testFindDisabled(url);
+ }
+
+ // Make sure the find bar is re-enabled after disabled page is closed.
+ await testFindEnabled("about:about");
+ gFindBar.close();
+ ok(gFindBar.hidden, "Find bar should now be hidden");
+});
+
+function testFindDisabled(url) {
+ return BrowserTestUtils.withNewTab(url, async function (browser) {
+ let waitForFindBar = async () => {
+ await new Promise(r => requestAnimationFrame(r));
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+ };
+ ok(
+ !gFindBar || gFindBar.hidden,
+ "Find bar should not be visible at the start"
+ );
+ await BrowserTestUtils.synthesizeKey("/", {}, browser);
+ await waitForFindBar();
+ ok(
+ !gFindBar || gFindBar.hidden,
+ "Find bar should not be visible after fast find"
+ );
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ await waitForFindBar();
+ ok(
+ !gFindBar || gFindBar.hidden,
+ "Find bar should not be visible after find command"
+ );
+ ok(
+ document.getElementById("cmd_find").getAttribute("disabled"),
+ "Find command should be disabled"
+ );
+ });
+}
+
+async function testFindEnabled(url) {
+ return BrowserTestUtils.withNewTab(url, async function (browser) {
+ ok(
+ !document.getElementById("cmd_find").getAttribute("disabled"),
+ "Find command should not be disabled"
+ );
+
+ // Open Find bar and then close it.
+ let findbarOpenedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabFindInitialized"
+ );
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ await findbarOpenedPromise;
+ ok(!gFindBar.hidden, "Find bar should be visible again");
+ EventUtils.synthesizeKey("KEY_Escape");
+ ok(gFindBar.hidden, "Find bar should now be hidden");
+ });
+}
diff --git a/browser/base/content/test/general/bug792517-2.html b/browser/base/content/test/general/bug792517-2.html
new file mode 100644
index 0000000000..bfc24d817f
--- /dev/null
+++ b/browser/base/content/test/general/bug792517-2.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<a href="bug792517.sjs" id="fff">this is a link</a>
+</body>
+</html>
diff --git a/browser/base/content/test/general/bug792517.html b/browser/base/content/test/general/bug792517.html
new file mode 100644
index 0000000000..e7c040bf1f
--- /dev/null
+++ b/browser/base/content/test/general/bug792517.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<img src="moz.png" id="img">
+</body>
+</html>
diff --git a/browser/base/content/test/general/bug792517.sjs b/browser/base/content/test/general/bug792517.sjs
new file mode 100644
index 0000000000..c1f2b282fb
--- /dev/null
+++ b/browser/base/content/test/general/bug792517.sjs
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ if (aRequest.hasHeader("Cookie")) {
+ aResponse.write("cookie-present");
+ } else {
+ aResponse.setHeader("Set-Cookie", "foopy=1");
+ aResponse.write("cookie-not-present");
+ }
+}
diff --git a/browser/base/content/test/general/clipboard_pastefile.html b/browser/base/content/test/general/clipboard_pastefile.html
new file mode 100644
index 0000000000..cffafcdb49
--- /dev/null
+++ b/browser/base/content/test/general/clipboard_pastefile.html
@@ -0,0 +1,52 @@
+<html><body>
+<script>
+async function checkPaste(event) {
+ let result = null;
+ try {
+ result = await checkPasteHelper(event);
+ } catch (e) {
+ result = e.toString();
+ }
+
+ document.dispatchEvent(new CustomEvent('testresult', {
+ detail: { result }
+ }));
+}
+
+function is(a, b, msg) {
+ if (!Object.is(a, b)) {
+ throw new Error(`FAIL: expected ${b} got ${a} - ${msg}`);
+ }
+}
+
+async function checkPasteHelper(event) {
+ let dt = event.clipboardData;
+
+ is(dt.types.length, 2, "Correct number of types");
+
+ // TODO: Remove application/x-moz-file from content.
+ is(dt.types[0], "application/x-moz-file", "First type")
+ is(dt.types[1], "Files", "Last type must be Files");
+
+ is(dt.getData("text/plain"), "", "text/plain found with getData");
+ is(dt.getData("application/x-moz-file"), "", "application/x-moz-file found with getData");
+
+ is(dt.files.length, 1, "Correct number of files");
+ is(dt.files[0].name, "test-file.txt", "Correct file name");
+ is(dt.files[0].type, "text/plain", "Correct file type");
+
+ is(dt.items.length, 1, "Correct number of items");
+ is(dt.items[0].kind, "file", "Correct item kind");
+ is(dt.items[0].type, "text/plain", "Correct item type");
+
+ let file = dt.files[0];
+ is(await file.text(), "Hello World!", "Pasted file contains right text");
+
+ return file.name;
+}
+</script>
+
+<input id="input" onpaste="checkPaste(event)">
+
+
+</body></html>
diff --git a/browser/base/content/test/general/close_beforeunload.html b/browser/base/content/test/general/close_beforeunload.html
new file mode 100644
index 0000000000..4b62002cc4
--- /dev/null
+++ b/browser/base/content/test/general/close_beforeunload.html
@@ -0,0 +1,8 @@
+<body>
+ <p>I will close myself if you close me.</p>
+ <script>
+ window.onbeforeunload = function() {
+ window.close();
+ };
+ </script>
+</body>
diff --git a/browser/base/content/test/general/close_beforeunload_opens_second_tab.html b/browser/base/content/test/general/close_beforeunload_opens_second_tab.html
new file mode 100644
index 0000000000..b17df8ee27
--- /dev/null
+++ b/browser/base/content/test/general/close_beforeunload_opens_second_tab.html
@@ -0,0 +1,3 @@
+<body>
+ <a href="#" onclick="window.open('close_beforeunload.html', '_blank')">Open second tab</a>
+</body>
diff --git a/browser/base/content/test/general/download_page.html b/browser/base/content/test/general/download_page.html
new file mode 100644
index 0000000000..300bacdb72
--- /dev/null
+++ b/browser/base/content/test/general/download_page.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=676619
+-->
+ <head>
+ <title>Test for the download attribute</title>
+
+ </head>
+ <body>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=676619">Bug 676619</a>
+ <br/>
+ <ul>
+ <li><a href="download_page_1.txt"
+ download="test.txt" id="link1">Download "test.txt"</a></li>
+ <li><a href="video.ogg"
+ download id="link2">Download "video.ogg"</a></li>
+ <li><a href="video.ogg"
+ download="just some video.ogg" id="link3">Download "just some video.ogg"</a></li>
+ <li><a href="download_page_2.txt"
+ download="with-target.txt" id="link4">Download "with-target.txt"</a></li>
+ <li><a href="javascript:(1+2)+''"
+ download="javascript.html" id="link5">Download "javascript.html"</a></li>
+ <li><a href="#" download="test.blob" id=link6>Download "test.blob"</a></li>
+ <li><a href="#" download="test.file" id=link7>Download "test.file"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?inline=download_page_3.txt"
+ download="not_used.txt" id="link8">Download "download_page_3.txt"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?attachment=download_page_3.txt"
+ download="not_used.txt" id="link9">Download "download_page_3.txt"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?inline=none"
+ download="download_page_4.txt" id="link10">Download "download_page_4.txt"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?attachment=none"
+ download="download_page_4.txt" id="link11">Download "download_page_4.txt"</a></li>
+ <li><a href="http://example.com/"
+ download="example.com" id="link12" target="_blank">Download "example.com"</a></li>
+ <li><a href="video.ogg"
+ download="no file extension" id="link13">Download "force extension"</a></li>
+ <li><a href="dummy.ics"
+ download="dummy.not-ics" id="link14">Download "dummy.not-ics"</a></li>
+ <li><a href="redirect_download.sjs?inline=download_page_3.txt"
+ download="not_used.txt" id="link15">Download "download_page_3.txt"</a></li>
+ <li><a href="redirect_download.sjs?attachment=download_page_3.txt"
+ download="not_used.txt" id="link16">Download "download_page_3.txt"</a></li>
+ <li><a href="redirect_download.sjs?inline=none"
+ download="download_page_4.txt" id="link17">Download "download_page_4.txt"</a></li>
+ <li><a href="redirect_download.sjs?attachment=none"
+ download="download_page_4.txt" id="link18">Download "download_page_4.txt"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?inline;attachment=none"
+ download="download_page_4.txt" id="link19">Download "download_page_4.txt"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?invalid=none"
+ download="download_page_4.txt" id="link20">Download "download_page_4.txt"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?inline;attachment=download_page_4.txt"
+ download="download_page_4.txt" id="link21">Download "download_page_4.txt"</a></li>
+ <li><a href="download_with_content_disposition_header.sjs?invalid=download_page_4.txt"
+ download="download_page_4.txt" id="link22">Download "download_page_4.txt"</a></li>
+ </ul>
+ <div id="unload-flag">Okay</div>
+
+ <script>
+ let blobURL = window.URL.createObjectURL(new Blob(["just text"], {type: "application/x-blob"}));
+ document.getElementById("link6").href = blobURL;
+
+ let fileURL = window.URL.createObjectURL(new File(["just text"],
+ "wrong-file-name", {type: "application/x-some-file"}));
+ document.getElementById("link7").href = fileURL;
+
+ window.addEventListener("beforeunload", function(evt) {
+ document.getElementById("unload-flag").textContent = "Fail";
+ });
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/download_page_1.txt b/browser/base/content/test/general/download_page_1.txt
new file mode 100644
index 0000000000..404b2da2ad
--- /dev/null
+++ b/browser/base/content/test/general/download_page_1.txt
@@ -0,0 +1 @@
+Hey What are you looking for?
diff --git a/browser/base/content/test/general/download_page_2.txt b/browser/base/content/test/general/download_page_2.txt
new file mode 100644
index 0000000000..9daeafb986
--- /dev/null
+++ b/browser/base/content/test/general/download_page_2.txt
@@ -0,0 +1 @@
+test
diff --git a/browser/base/content/test/general/download_with_content_disposition_header.sjs b/browser/base/content/test/general/download_with_content_disposition_header.sjs
new file mode 100644
index 0000000000..26be6c44b7
--- /dev/null
+++ b/browser/base/content/test/general/download_with_content_disposition_header.sjs
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 handleRequest(request, response) {
+ let page = "download";
+ response.setStatusLine(request.httpVersion, "200", "OK");
+
+ let [first, second] = request.queryString.split("=");
+ let headerStr = first;
+ if (second !== "none") {
+ headerStr += "; filename=" + second;
+ }
+
+ response.setHeader("Content-Disposition", headerStr);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+}
diff --git a/browser/base/content/test/general/dummy.ics b/browser/base/content/test/general/dummy.ics
new file mode 100644
index 0000000000..6100d46fb7
--- /dev/null
+++ b/browser/base/content/test/general/dummy.ics
@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//hacksw/handcal//NONSGML v1.0//EN
+BEGIN:VEVENT
+UID:uid1@example.com
+DTSTAMP:19970714T170000Z
+ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
+DTSTART:19970714T170000Z
+DTEND:19970715T035959Z
+SUMMARY:Bastille Day Party
+GEO:48.85299;2.36885
+END:VEVENT
+END:VCALENDAR \ No newline at end of file
diff --git a/browser/base/content/test/general/dummy.ics^headers^ b/browser/base/content/test/general/dummy.ics^headers^
new file mode 100644
index 0000000000..93e1fca48d
--- /dev/null
+++ b/browser/base/content/test/general/dummy.ics^headers^
@@ -0,0 +1 @@
+Content-Type: text/calendar
diff --git a/browser/base/content/test/general/dummy_page.html b/browser/base/content/test/general/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/base/content/test/general/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/base/content/test/general/file_documentnavigation_frameset.html b/browser/base/content/test/general/file_documentnavigation_frameset.html
new file mode 100644
index 0000000000..beb01addfc
--- /dev/null
+++ b/browser/base/content/test/general/file_documentnavigation_frameset.html
@@ -0,0 +1,12 @@
+<html id="outer">
+
+<frameset rows="30%, 70%">
+ <frame src="data:text/html,&lt;html id='htmlframe1' &gt;&lt;body id='framebody1'&gt;&lt;input id='i1'&gt;&lt;body&gt;&lt;/html&gt;">
+ <frameset cols="30%, 33%, 34%">
+ <frame src="data:text/html,&lt;html id='htmlframe2'&gt;&lt;body id='framebody2'&gt;&lt;input id='i2'&gt;&lt;body&gt;&lt;/html&gt;">
+ <frame src="data:text/html,&lt;html id='htmlframe3'&gt;&lt;body id='framebody3'&gt;&lt;input id='i3'&gt;&lt;body&gt;&lt;/html&gt;">
+ <frame src="data:text/html,&lt;html id='htmlframe4'&gt;&lt;body id='framebody4'&gt;&lt;input id='i4'&gt;&lt;body&gt;&lt;/html&gt;">
+ </frameset>
+</frameset>
+
+</html>
diff --git a/browser/base/content/test/general/file_double_close_tab.html b/browser/base/content/test/general/file_double_close_tab.html
new file mode 100644
index 0000000000..0bead5efc6
--- /dev/null
+++ b/browser/base/content/test/general/file_double_close_tab.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test page that blocks beforeunload. Used in tests for bug 1050638 and bug 305085</title>
+ </head>
+ <body>
+ This page will block beforeunload. It should still be user-closable at all times.
+ <script>
+ window.onbeforeunload = function() {
+ return "stop";
+ };
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/file_fullscreen-window-open.html b/browser/base/content/test/general/file_fullscreen-window-open.html
new file mode 100644
index 0000000000..44ac3196a0
--- /dev/null
+++ b/browser/base/content/test/general/file_fullscreen-window-open.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for window.open() when browser is in fullscreen</title>
+ </head>
+ <body>
+ <script>
+ window.addEventListener("load", function() {
+ document.getElementById("test").addEventListener("click", onClick, true);
+ }, {capture: true, once: true});
+
+ function onClick(aEvent) {
+ aEvent.preventDefault();
+
+ var dataStr = aEvent.target.getAttribute("data-test-param");
+ var data = JSON.parse(dataStr);
+ window.open(data.uri, data.title, data.option);
+ }
+ </script>
+ <a id="test" href="" data-test-param="">Test</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/file_window_activation.html b/browser/base/content/test/general/file_window_activation.html
new file mode 100644
index 0000000000..dda62986d1
--- /dev/null
+++ b/browser/base/content/test/general/file_window_activation.html
@@ -0,0 +1,4 @@
+<body>
+<style>:-moz-window-inactive { background-color: red; }</style>
+<div id='area'></div>
+</body>
diff --git a/browser/base/content/test/general/file_window_activation2.html b/browser/base/content/test/general/file_window_activation2.html
new file mode 100644
index 0000000000..e1b7ecf12f
--- /dev/null
+++ b/browser/base/content/test/general/file_window_activation2.html
@@ -0,0 +1 @@
+<body>Hi</body>
diff --git a/browser/base/content/test/general/file_with_link_to_http.html b/browser/base/content/test/general/file_with_link_to_http.html
new file mode 100644
index 0000000000..4c1a766a3a
--- /dev/null
+++ b/browser/base/content/test/general/file_with_link_to_http.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test page for Bug 1338375</title>
+</head>
+<body>
+ <a id="linkToExample" href="http://example.org" target="_blank">example.org</a>
+</body>
+</html>
diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js
new file mode 100644
index 0000000000..fc3d2be19f
--- /dev/null
+++ b/browser/base/content/test/general/head.js
@@ -0,0 +1,347 @@
+ChromeUtils.defineModuleGetter(
+ this,
+ "AboutNewTab",
+ "resource:///modules/AboutNewTab.jsm"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "TabCrashHandler",
+ "resource:///modules/ContentCrashHandlers.jsm"
+);
+
+/**
+ * Wait for a <notification> to be closed then call the specified callback.
+ */
+function waitForNotificationClose(notification, cb) {
+ let observer = new MutationObserver(function onMutatations(mutations) {
+ for (let mutation of mutations) {
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
+ let node = mutation.removedNodes.item(i);
+ if (node != notification) {
+ continue;
+ }
+ observer.disconnect();
+ cb();
+ }
+ }
+ });
+ observer.observe(notification.control.stack, { childList: true });
+}
+
+function closeAllNotifications() {
+ if (!gNotificationBox.currentNotification) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ for (let notification of gNotificationBox.allNotifications) {
+ waitForNotificationClose(notification, function () {
+ if (gNotificationBox.allNotifications.length === 0) {
+ resolve();
+ }
+ });
+ notification.close();
+ }
+ });
+}
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ }
+ }, "browser-delayed-startup-finished");
+}
+
+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();
+}
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+ retryTimes = typeof retryTimes !== "undefined" ? retryTimes : 30;
+ var tries = 0;
+ var interval = setInterval(function () {
+ if (tries >= retryTimes) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function () {
+ clearInterval(interval);
+ nextTest();
+ };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+ return new Promise(resolve => {
+ waitForCondition(aConditionFn, resolve, "Condition didn't pass.");
+ });
+}
+
+function promiseWaitForEvent(
+ object,
+ eventName,
+ capturing = false,
+ chrome = false
+) {
+ return new Promise(resolve => {
+ function listener(event) {
+ info("Saw " + eventName);
+ object.removeEventListener(eventName, listener, capturing, chrome);
+ resolve(event);
+ }
+
+ info("Waiting for " + eventName);
+ object.addEventListener(eventName, listener, capturing, chrome);
+ });
+}
+
+/**
+ * Allows setting focus on a window, and waiting for that window to achieve
+ * focus.
+ *
+ * @param aWindow
+ * The window to focus and wait for.
+ *
+ * @return {Promise}
+ * @resolves When the window is focused.
+ * @rejects Never.
+ */
+function promiseWaitForFocus(aWindow) {
+ return new Promise(resolve => {
+ waitForFocus(resolve, aWindow);
+ });
+}
+
+function pushPrefs(...aPrefs) {
+ return SpecialPowers.pushPrefEnv({ set: aPrefs });
+}
+
+function popPrefs() {
+ return SpecialPowers.popPrefEnv();
+}
+
+function promiseWindowClosed(win) {
+ let promise = BrowserTestUtils.domWindowClosed(win);
+ win.close();
+ return promise;
+}
+
+function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup = false) {
+ return new Promise(resolve => {
+ let win = OpenBrowserWindow(aOptions);
+ if (aWaitForDelayedStartup) {
+ Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
+ if (aSubject != win) {
+ return;
+ }
+ Services.obs.removeObserver(onDS, "browser-delayed-startup-finished");
+ resolve(win);
+ }, "browser-delayed-startup-finished");
+ } else {
+ win.addEventListener(
+ "load",
+ function () {
+ resolve(win);
+ },
+ { once: true }
+ );
+ }
+ });
+}
+
+async function whenNewTabLoaded(aWindow, aCallback) {
+ aWindow.BrowserOpenTab();
+
+ let expectedURL = AboutNewTab.newTabURL;
+ let browser = aWindow.gBrowser.selectedBrowser;
+ let loadPromise = BrowserTestUtils.browserLoaded(browser, false, expectedURL);
+ let alreadyLoaded = await SpecialPowers.spawn(browser, [expectedURL], url => {
+ let doc = content.document;
+ return doc && doc.readyState === "complete" && doc.location.href == url;
+ });
+ if (!alreadyLoaded) {
+ await loadPromise;
+ }
+ aCallback();
+}
+
+function whenTabLoaded(aTab, aCallback) {
+ promiseTabLoadEvent(aTab).then(aCallback);
+}
+
+function promiseTabLoaded(aTab) {
+ return new Promise(resolve => {
+ whenTabLoaded(aTab, resolve);
+ });
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url) {
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+/**
+ * Returns a Promise that resolves once a new tab has been opened in
+ * a xul:tabbrowser.
+ *
+ * @param aTabBrowser
+ * The xul:tabbrowser to monitor for a new tab.
+ * @return {Promise}
+ * Resolved when the new tab has been opened.
+ * @resolves to the TabOpen event that was fired.
+ * @rejects Never.
+ */
+function waitForNewTabEvent(aTabBrowser) {
+ return BrowserTestUtils.waitForEvent(aTabBrowser.tabContainer, "TabOpen");
+}
+
+function is_hidden(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none") {
+ return true;
+ }
+ if (style.visibility != "visible") {
+ return true;
+ }
+ if (XULPopupElement.isInstance(element)) {
+ return ["hiding", "closed"].includes(element.state);
+ }
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument) {
+ return is_hidden(element.parentNode);
+ }
+
+ return false;
+}
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(BrowserTestUtils.is_visible(element), msg || "Element should be visible");
+}
+
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(element), msg || "Element should be hidden");
+}
+
+function promisePopupShown(popup) {
+ return BrowserTestUtils.waitForPopupEvent(popup, "shown");
+}
+
+function promisePopupHidden(popup) {
+ return BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = promisePopupShown(win.PopupNotifications.panel);
+ notification.reshow();
+ return panelPromise;
+}
+
+/**
+ * Resolves when a bookmark with the given uri is added.
+ */
+function promiseOnBookmarkItemAdded(aExpectedURI) {
+ return new Promise((resolve, reject) => {
+ let listener = events => {
+ is(events.length, 1, "Should only receive one event.");
+ info("Added a bookmark to " + events[0].url);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ if (events[0].url == aExpectedURI.spec) {
+ resolve();
+ } else {
+ reject(new Error("Added an unexpected bookmark"));
+ }
+ };
+ info("Waiting for a bookmark to be added");
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ });
+}
+
+async function loadBadCertPage(url) {
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await loaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.getElementById("exceptionDialogButton").click();
+ });
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+}
diff --git a/browser/base/content/test/general/moz.png b/browser/base/content/test/general/moz.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/base/content/test/general/moz.png
Binary files differ
diff --git a/browser/base/content/test/general/navigating_window_with_download.html b/browser/base/content/test/general/navigating_window_with_download.html
new file mode 100644
index 0000000000..6b0918941f
--- /dev/null
+++ b/browser/base/content/test/general/navigating_window_with_download.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <head><title>This window will navigate while you're downloading something</title></head>
+ <body>
+ <iframe src="http://mochi.test:8888/browser/browser/base/content/test/general/unknownContentType_file.pif"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/print_postdata.sjs b/browser/base/content/test/general/print_postdata.sjs
new file mode 100644
index 0000000000..0e3ef38419
--- /dev/null
+++ b/browser/base/content/test/general/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 {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/browser/base/content/test/general/redirect_download.sjs b/browser/base/content/test/general/redirect_download.sjs
new file mode 100644
index 0000000000..c2857b9338
--- /dev/null
+++ b/browser/base/content/test/general/redirect_download.sjs
@@ -0,0 +1,11 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ let queryStr = request.queryString;
+
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader(
+ "Location",
+ `download_with_content_disposition_header.sjs?${queryStr}`,
+ false
+ );
+}
diff --git a/browser/base/content/test/general/refresh_header.sjs b/browser/base/content/test/general/refresh_header.sjs
new file mode 100644
index 0000000000..7d66e0a429
--- /dev/null
+++ b/browser/base/content/test/general/refresh_header.sjs
@@ -0,0 +1,24 @@
+/**
+ * Will cause an auto-refresh to the URL provided in the query string
+ * after some delay using the refresh HTTP header.
+ *
+ * Expects the query string to be in the format:
+ *
+ * ?p=[URL of the page to redirect to]&d=[delay]
+ *
+ * Example:
+ *
+ * ?p=http%3A%2F%2Fexample.org%2Fbrowser%2Fbrowser%2Fbase%2Fcontent%2Ftest%2Fgeneral%2Frefresh_meta.sjs&d=200
+ */
+function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let page = query.get("p");
+ let delay = query.get("d");
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "200", "Found");
+ response.setHeader("refresh", `${delay}; url=${page}`);
+ response.write("OK");
+}
diff --git a/browser/base/content/test/general/refresh_meta.sjs b/browser/base/content/test/general/refresh_meta.sjs
new file mode 100644
index 0000000000..0f91507c18
--- /dev/null
+++ b/browser/base/content/test/general/refresh_meta.sjs
@@ -0,0 +1,36 @@
+/**
+ * Will cause an auto-refresh to the URL provided in the query string
+ * after some delay using a <meta> tag.
+ *
+ * Expects the query string to be in the format:
+ *
+ * ?p=[URL of the page to redirect to]&d=[delay]
+ *
+ * Example:
+ *
+ * ?p=http%3A%2F%2Fexample.org%2Fbrowser%2Fbrowser%2Fbase%2Fcontent%2Ftest%2Fgeneral%2Frefresh_meta.sjs&d=200
+ */
+function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let page = query.get("p");
+ let delay = query.get("d");
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <META http-equiv='refresh' content='${delay}; url=${page}'>
+ <title>Gonna refresh you, folks.</title>
+ </head>
+ <body>
+ <h1>Wait for it...</h1>
+ </body>
+ </html>`;
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "200", "Found");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/general/test_bug462673.html b/browser/base/content/test/general/test_bug462673.html
new file mode 100644
index 0000000000..d864990e4f
--- /dev/null
+++ b/browser/base/content/test/general/test_bug462673.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+<script>
+var w;
+function openIt() {
+ w = window.open("", "window2");
+}
+function closeIt() {
+ if (w) {
+ w.close();
+ w = null;
+ }
+}
+</script>
+</head>
+<body onload="openIt();" onunload="closeIt();">
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_bug628179.html b/browser/base/content/test/general/test_bug628179.html
new file mode 100644
index 0000000000..1136048d36
--- /dev/null
+++ b/browser/base/content/test/general/test_bug628179.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for closing the Find bar in subdocuments</title>
+ </head>
+ <body>
+ <iframe id=iframe src="http://example.com/" width=320 height=240></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/test_remoteTroubleshoot.html b/browser/base/content/test/general/test_remoteTroubleshoot.html
new file mode 100644
index 0000000000..c0c3f5e604
--- /dev/null
+++ b/browser/base/content/test/general/test_remoteTroubleshoot.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+// This test is run multiple times, once with only strings allowed through the
+// WebChannel, and once with objects allowed. This function allows us to handle
+// both cases without too much pain.
+function makeDetails(object) {
+ if (window.location.search.includes("object")) {
+ return object;
+ }
+ return JSON.stringify(object);
+}
+// Add a listener for responses to our remote requests.
+window.addEventListener("WebChannelMessageToContent", function(event) {
+ if (event.detail.id == "remote-troubleshooting") {
+ // Send what we got back to the test.
+ var backEvent = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: makeDetails({
+ id: "test-remote-troubleshooting-backchannel",
+ message: {
+ message: event.detail.message,
+ },
+ }),
+ });
+ window.dispatchEvent(backEvent);
+ // and stick it in our DOM just for good measure/diagnostics.
+ document.getElementById("troubleshooting").textContent =
+ JSON.stringify(event.detail.message, null, 2);
+ }
+});
+
+// Make a request for the troubleshooting data as we load.
+window.onload = function() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: makeDetails({
+ id: "remote-troubleshooting",
+ message: {
+ command: "request",
+ },
+ }),
+ });
+ window.dispatchEvent(event);
+};
+</script>
+
+<body>
+ <pre id="troubleshooting"/>
+</body>
+
+</html>
diff --git a/browser/base/content/test/general/title_test.svg b/browser/base/content/test/general/title_test.svg
new file mode 100644
index 0000000000..80390a3cca
--- /dev/null
+++ b/browser/base/content/test/general/title_test.svg
@@ -0,0 +1,59 @@
+<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>This is a root SVG element's title</title>
+ <foreignObject>
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+ <svg xmlns="http://www.w3.org/2000/svg" id="svg1">
+ <title>This is a non-root SVG element title</title>
+ </svg>
+ </body>
+ </html>
+ </foreignObject>
+ <text id="text1" x="10px" y="32px" font-size="24px">
+ This contains only &lt;title&gt;
+ <title>
+
+
+ This is a title
+
+ </title>
+ </text>
+ <text id="text2" x="10px" y="96px" font-size="24px">
+ This contains only &lt;desc&gt;
+ <desc>This is a desc</desc>
+ </text>
+ <text id="text3" x="10px" y="128px" font-size="24px" title="ignored for SVG">
+ This contains nothing.
+ </text>
+ <a id="link1" href="#">
+ This link contains &lt;title&gt;
+ <title>
+ This is a title
+ </title>
+ <text id="text4" x="10px" y="192px" font-size="24px">
+ </text>
+ </a>
+ <a id="link2" href="#">
+ <text x="10px" y="192px" font-size="24px">
+ This text contains &lt;title&gt;
+ <title>
+ This is a title
+ </title>
+ </text>
+ </a>
+ <a id="link3" href="#" xlink:title="This is an xlink:title attribute">
+ <text x="10px" y="224px" font-size="24px">
+ This link contains &lt;title&gt; &amp; xlink:title attr.
+ <title>This is a title</title>
+ </text>
+ </a>
+ <a id="link4" href="#" xlink:title="This is an xlink:title attribute">
+ <text x="10px" y="256px" font-size="24px">
+ This link contains xlink:title attr.
+ </text>
+ </a>
+ <text id="text5" x="10px" y="160px" font-size="24px"
+ xlink:title="This is an xlink:title attribute but it isn't on a link" >
+ This contains nothing.
+ </text>
+</svg>
diff --git a/browser/base/content/test/general/unknownContentType_file.pif b/browser/base/content/test/general/unknownContentType_file.pif
new file mode 100644
index 0000000000..9353d13126
--- /dev/null
+++ b/browser/base/content/test/general/unknownContentType_file.pif
@@ -0,0 +1 @@
+Dummy content for unknownContentType_dialog_layout_data.pif
diff --git a/browser/base/content/test/general/unknownContentType_file.pif^headers^ b/browser/base/content/test/general/unknownContentType_file.pif^headers^
new file mode 100644
index 0000000000..09b22facc0
--- /dev/null
+++ b/browser/base/content/test/general/unknownContentType_file.pif^headers^
@@ -0,0 +1 @@
+Content-Type: application/octet-stream
diff --git a/browser/base/content/test/general/video.ogg b/browser/base/content/test/general/video.ogg
new file mode 100644
index 0000000000..ac7ece3519
--- /dev/null
+++ b/browser/base/content/test/general/video.ogg
Binary files differ
diff --git a/browser/base/content/test/general/web_video.html b/browser/base/content/test/general/web_video.html
new file mode 100644
index 0000000000..467fb0ce1c
--- /dev/null
+++ b/browser/base/content/test/general/web_video.html
@@ -0,0 +1,10 @@
+<html>
+ <head>
+ <title>Document with Web Video</title>
+ </head>
+ <body>
+ This document has some web video in it.
+ <br>
+ <video src="web_video1.ogv" id="video1"> </video>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/web_video1.ogv b/browser/base/content/test/general/web_video1.ogv
new file mode 100644
index 0000000000..093158432a
--- /dev/null
+++ b/browser/base/content/test/general/web_video1.ogv
Binary files differ
diff --git a/browser/base/content/test/general/web_video1.ogv^headers^ b/browser/base/content/test/general/web_video1.ogv^headers^
new file mode 100644
index 0000000000..4511e92552
--- /dev/null
+++ b/browser/base/content/test/general/web_video1.ogv^headers^
@@ -0,0 +1,3 @@
+Content-Disposition: filename="web-video1-expectedName.ogv"
+Content-Type: video/ogg
+
diff --git a/browser/base/content/test/gesture/browser.ini b/browser/base/content/test/gesture/browser.ini
new file mode 100644
index 0000000000..1ae3ad6df5
--- /dev/null
+++ b/browser/base/content/test/gesture/browser.ini
@@ -0,0 +1 @@
+[browser_gesture_navigation.js]
diff --git a/browser/base/content/test/gesture/browser_gesture_navigation.js b/browser/base/content/test/gesture/browser_gesture_navigation.js
new file mode 100644
index 0000000000..667a1f07b6
--- /dev/null
+++ b/browser/base/content/test/gesture/browser_gesture_navigation.js
@@ -0,0 +1,233 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_setup(async () => {
+ // Disable window occlusion. See bug 1733955 / bug 1779559.
+ if (navigator.platform.indexOf("Win") == 0) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["widget.windows.window_occlusion_tracking.enabled", false]],
+ });
+ }
+});
+
+add_task(async () => {
+ // Open a new browser window to make sure there is no navigation history.
+ const newBrowser = await BrowserTestUtils.openNewBrowserWindow({});
+
+ let event = {
+ direction: SimpleGestureEvent.DIRECTION_LEFT,
+ };
+ ok(!newBrowser.gGestureSupport._shouldDoSwipeGesture(event));
+
+ event = {
+ direction: SimpleGestureEvent.DIRECTION_RIGHT,
+ };
+ ok(!newBrowser.gGestureSupport._shouldDoSwipeGesture(event));
+
+ await BrowserTestUtils.closeWindow(newBrowser);
+});
+
+function createSimpleGestureEvent(type, direction) {
+ let event = document.createEvent("SimpleGestureEvent");
+ event.initSimpleGestureEvent(
+ type,
+ false /* canBubble */,
+ false /* cancelableArg */,
+ window,
+ 0 /* detail */,
+ 0 /* screenX */,
+ 0 /* screenY */,
+ 0 /* clientX */,
+ 0 /* clientY */,
+ false /* ctrlKey */,
+ false /* altKey */,
+ false /* shiftKey */,
+ false /* metaKey */,
+ 0 /* button */,
+ null /* relatedTarget */,
+ 0 /* allowedDirections */,
+ direction,
+ 1 /* delta */
+ );
+ return event;
+}
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.swipeAnimationEnabled", false]],
+ });
+
+ // Open a new browser window and load two pages so that the browser can go
+ // back but can't go forward.
+ const newWindow = await BrowserTestUtils.openNewBrowserWindow({});
+
+ // gHistroySwipeAnimation gets initialized in a requestIdleCallback so we need
+ // to wait for the initialization.
+ await TestUtils.waitForCondition(() => {
+ return (
+ // There's no explicit notification for the initialization, so we wait
+ // until `isLTR` matches the browser locale state.
+ newWindow.gHistorySwipeAnimation.isLTR != Services.locale.isAppLocaleRTL
+ );
+ });
+
+ BrowserTestUtils.loadURIString(
+ newWindow.gBrowser.selectedBrowser,
+ "about:mozilla"
+ );
+ await BrowserTestUtils.browserLoaded(
+ newWindow.gBrowser.selectedBrowser,
+ false,
+ "about:mozilla"
+ );
+ BrowserTestUtils.loadURIString(
+ newWindow.gBrowser.selectedBrowser,
+ "about:about"
+ );
+ await BrowserTestUtils.browserLoaded(
+ newWindow.gBrowser.selectedBrowser,
+ false,
+ "about:about"
+ );
+
+ let event = createSimpleGestureEvent(
+ "MozSwipeGestureMayStart",
+ SimpleGestureEvent.DIRECTION_LEFT
+ );
+ newWindow.gGestureSupport._shouldDoSwipeGesture(event);
+
+ // Assuming we are on LTR environment.
+ is(
+ event.allowedDirections,
+ SimpleGestureEvent.DIRECTION_LEFT,
+ "Allows only swiping to left, i.e. backward"
+ );
+
+ event = createSimpleGestureEvent(
+ "MozSwipeGestureMayStart",
+ SimpleGestureEvent.DIRECTION_RIGHT
+ );
+ newWindow.gGestureSupport._shouldDoSwipeGesture(event);
+ is(
+ event.allowedDirections,
+ SimpleGestureEvent.DIRECTION_LEFT,
+ "Allows only swiping to left, i.e. backward"
+ );
+
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.swipeAnimationEnabled", true]],
+ });
+
+ // Open a new browser window and load two pages so that the browser can go
+ // back but can't go forward.
+ const newWindow = await BrowserTestUtils.openNewBrowserWindow({});
+
+ if (!newWindow.gHistorySwipeAnimation._isSupported()) {
+ await BrowserTestUtils.closeWindow(newWindow);
+ return;
+ }
+
+ function sendSwipeSequence(sendEnd) {
+ let event = createSimpleGestureEvent(
+ "MozSwipeGestureMayStart",
+ SimpleGestureEvent.DIRECTION_LEFT
+ );
+ newWindow.gGestureSupport.handleEvent(event);
+
+ event = createSimpleGestureEvent(
+ "MozSwipeGestureStart",
+ SimpleGestureEvent.DIRECTION_LEFT
+ );
+ newWindow.gGestureSupport.handleEvent(event);
+
+ event = createSimpleGestureEvent(
+ "MozSwipeGestureUpdate",
+ SimpleGestureEvent.DIRECTION_LEFT
+ );
+ newWindow.gGestureSupport.handleEvent(event);
+
+ event = createSimpleGestureEvent(
+ "MozSwipeGestureUpdate",
+ SimpleGestureEvent.DIRECTION_LEFT
+ );
+ newWindow.gGestureSupport.handleEvent(event);
+
+ if (sendEnd) {
+ sendSwipeEnd();
+ }
+ }
+ function sendSwipeEnd() {
+ let event = createSimpleGestureEvent(
+ "MozSwipeGestureEnd",
+ SimpleGestureEvent.DIRECTION_LEFT
+ );
+ newWindow.gGestureSupport.handleEvent(event);
+ }
+
+ // gHistroySwipeAnimation gets initialized in a requestIdleCallback so we need
+ // to wait for the initialization.
+ await TestUtils.waitForCondition(() => {
+ return (
+ // There's no explicit notification for the initialization, so we wait
+ // until `isLTR` matches the browser locale state.
+ newWindow.gHistorySwipeAnimation.isLTR != Services.locale.isAppLocaleRTL
+ );
+ });
+
+ BrowserTestUtils.loadURIString(
+ newWindow.gBrowser.selectedBrowser,
+ "about:mozilla"
+ );
+ await BrowserTestUtils.browserLoaded(
+ newWindow.gBrowser.selectedBrowser,
+ false,
+ "about:mozilla"
+ );
+ BrowserTestUtils.loadURIString(
+ newWindow.gBrowser.selectedBrowser,
+ "about:about"
+ );
+ await BrowserTestUtils.browserLoaded(
+ newWindow.gBrowser.selectedBrowser,
+ false,
+ "about:about"
+ );
+
+ // Start a swipe that's not enough to navigate
+ sendSwipeSequence(/* sendEnd = */ true);
+
+ // Wait two frames
+ await new Promise(r =>
+ window.requestAnimationFrame(() => window.requestAnimationFrame(r))
+ );
+
+ // The transition to fully stopped shouldn't have had enough time yet to
+ // become fully stopped.
+ ok(
+ newWindow.gHistorySwipeAnimation._isStoppingAnimation,
+ "should be stopping anim"
+ );
+
+ // Start another swipe.
+ sendSwipeSequence(/* sendEnd = */ false);
+
+ // Wait two frames
+ await new Promise(r =>
+ window.requestAnimationFrame(() => window.requestAnimationFrame(r))
+ );
+
+ // We should have started a new swipe, ie we shouldn't be stopping.
+ ok(
+ !newWindow.gHistorySwipeAnimation._isStoppingAnimation,
+ "should not be stopping anim"
+ );
+
+ sendSwipeEnd();
+
+ await BrowserTestUtils.closeWindow(newWindow);
+});
diff --git a/browser/base/content/test/historySwipeAnimation/browser.ini b/browser/base/content/test/historySwipeAnimation/browser.ini
new file mode 100644
index 0000000000..c9fb8cd246
--- /dev/null
+++ b/browser/base/content/test/historySwipeAnimation/browser.ini
@@ -0,0 +1 @@
+[browser_historySwipeAnimation.js]
diff --git a/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js b/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js
new file mode 100644
index 0000000000..a5910964e7
--- /dev/null
+++ b/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ BrowserOpenTab();
+ let tab = gBrowser.selectedTab;
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ });
+
+ ok(gHistorySwipeAnimation, "gHistorySwipeAnimation exists.");
+
+ if (!gHistorySwipeAnimation._isSupported()) {
+ is(
+ gHistorySwipeAnimation.active,
+ false,
+ "History swipe animation is not " +
+ "active when not supported by the platform."
+ );
+ finish();
+ return;
+ }
+
+ gHistorySwipeAnimation.init();
+
+ is(
+ gHistorySwipeAnimation.active,
+ true,
+ "History swipe animation support " +
+ "was successfully initialized when supported."
+ );
+
+ test0();
+
+ function test0() {
+ // Test uninit of gHistorySwipeAnimation.
+ // This test MUST be the last one to execute.
+ gHistorySwipeAnimation.uninit();
+ is(
+ gHistorySwipeAnimation.active,
+ false,
+ "History swipe animation support was successfully uninitialized"
+ );
+ finish();
+ }
+}
diff --git a/browser/base/content/test/keyboard/browser.ini b/browser/base/content/test/keyboard/browser.ini
new file mode 100644
index 0000000000..a92b0838ef
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_bookmarks_shortcut.js]
+https_first_disabled = true
+[browser_cancel_caret_browsing_in_content.js]
+support-files = file_empty.html
+[browser_popup_keyNav.js]
+https_first_disabled = true
+support-files = focusableContent.html
+[browser_toolbarButtonKeyPress.js]
+skip-if =
+ os == "linux" #Bug 1532501
+ os == "win" && asan # Bug 1775712
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_toolbarKeyNav.js]
+support-files = !/browser/base/content/test/permissions/permissions.html
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
diff --git a/browser/base/content/test/keyboard/browser_bookmarks_shortcut.js b/browser/base/content/test/keyboard/browser_bookmarks_shortcut.js
new file mode 100644
index 0000000000..02aedfaf79
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_bookmarks_shortcut.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the behavior of keypress shortcuts for the bookmarks toolbar.
+ */
+
+// Test that the bookmarks toolbar's visibility is toggled using the bookmarks-shortcut.
+add_task(async function testBookmarksToolbarShortcut() {
+ let blankTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "example.com",
+ waitForLoad: false,
+ });
+
+ info("Toggle toolbar visibility on");
+ let toolbar = document.getElementById("PersonalToolbar");
+ is(
+ toolbar.getAttribute("collapsed"),
+ "true",
+ "Toolbar bar should already be collapsed"
+ );
+
+ EventUtils.synthesizeKey("b", { shiftKey: true, accelKey: true });
+ toolbar = document.getElementById("PersonalToolbar");
+ await BrowserTestUtils.waitForAttribute("collapsed", toolbar, "false");
+ ok(true, "bookmarks toolbar is visible");
+
+ await testIsBookmarksMenuItemStateChecked("always");
+
+ info("Toggle toolbar visibility off");
+ EventUtils.synthesizeKey("b", { shiftKey: true, accelKey: true });
+ toolbar = document.getElementById("PersonalToolbar");
+ await BrowserTestUtils.waitForAttribute("collapsed", toolbar, "true");
+ ok(true, "bookmarks toolbar is not visible");
+
+ await testIsBookmarksMenuItemStateChecked("never");
+
+ await BrowserTestUtils.removeTab(blankTab);
+});
+
+// Test that the bookmarks library windows opens with the new keyboard shortcut.
+add_task(async function testNewBookmarksLibraryShortcut() {
+ let blankTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "example.com",
+ waitForLoad: false,
+ });
+
+ info("Check that the bookmarks library windows opens.");
+ let bookmarksLibraryOpened = promiseOpenBookmarksLibrary();
+
+ await EventUtils.synthesizeKey("o", { shiftKey: true, accelKey: true });
+
+ let win = await bookmarksLibraryOpened;
+
+ ok(true, "Bookmarks library successfully opened.");
+
+ win.close();
+
+ await BrowserTestUtils.removeTab(blankTab);
+});
+
+/**
+ * Tests whether or not the bookmarks' menuitem state is checked.
+ */
+async function testIsBookmarksMenuItemStateChecked(expected) {
+ info("Test that the toolbar menuitem state is correct.");
+ let contextMenu = document.getElementById("toolbar-context-menu");
+ let target = document.getElementById("PanelUI-menu-button");
+
+ await openContextMenu(contextMenu, target);
+
+ let menuitems = ["always", "never", "newtab"].map(e =>
+ document.querySelector(`menuitem[data-visibility-enum="${e}"]`)
+ );
+
+ let checkedItem = menuitems.filter(m => m.getAttribute("checked") == "true");
+ is(checkedItem.length, 1, "should have only one menuitem checked");
+ is(
+ checkedItem[0].dataset.visibilityEnum,
+ expected,
+ `checked menuitem should be ${expected}`
+ );
+
+ for (let menuitem of menuitems) {
+ if (menuitem.dataset.visibilityEnum == expected) {
+ ok(!menuitem.hasAttribute("key"), "dont show shortcut on current state");
+ } else {
+ is(
+ menuitem.hasAttribute("key"),
+ menuitem.dataset.visibilityEnum != "newtab",
+ "shortcut is on the menuitem opposite of the current state excluding newtab"
+ );
+ }
+ }
+
+ await closeContextMenu(contextMenu);
+}
+
+/**
+ * Returns a promise for opening the bookmarks library.
+ */
+async function promiseOpenBookmarksLibrary() {
+ return BrowserTestUtils.domWindowOpened(null, async win => {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ await TestUtils.waitForCondition(
+ () =>
+ win.document.documentURI ===
+ "chrome://browser/content/places/places.xhtml"
+ );
+ return true;
+ });
+}
+
+/**
+ * Helper for opening the context menu.
+ */
+async function openContextMenu(contextMenu, target) {
+ info("Opening context menu.");
+ EventUtils.synthesizeMouseAtCenter(target, {
+ type: "contextmenu",
+ });
+ await BrowserTestUtils.waitForPopupEvent(contextMenu, "shown");
+ let bookmarksToolbarMenu = document.querySelector("#toggle_PersonalToolbar");
+ let subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ bookmarksToolbarMenu.openMenu(true);
+ await BrowserTestUtils.waitForPopupEvent(subMenu, "shown");
+}
+
+/**
+ * Helper for closing the context menu.
+ */
+async function closeContextMenu(contextMenu) {
+ info("Closing context menu.");
+ contextMenu.hidePopup();
+ await BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden");
+}
diff --git a/browser/base/content/test/keyboard/browser_cancel_caret_browsing_in_content.js b/browser/base/content/test/keyboard/browser_cancel_caret_browsing_in_content.js
new file mode 100644
index 0000000000..719b92eed6
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_cancel_caret_browsing_in_content.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const kPrefName_CaretBrowsingOn = "accessibility.browsewithcaret";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["accessibility.browsewithcaret_shortcut.enabled", true],
+ ["accessibility.warn_on_browsewithcaret", false],
+ ["test.events.async.enabled", true],
+ [kPrefName_CaretBrowsingOn, false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/browser/base/content/test/keyboard/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ function promiseFirstAndReplyKeyEvents(aExpectedConsume) {
+ return new Promise(resolve => {
+ const eventType = aExpectedConsume ? "keydown" : "keypress";
+ let eventCount = 0;
+ let listener = () => {
+ if (++eventCount === 2) {
+ window.removeEventListener(eventType, listener, {
+ capture: true,
+ mozSystemGroup: true,
+ });
+ resolve();
+ }
+ };
+ window.addEventListener(eventType, listener, {
+ capture: true,
+ mozSystemGroup: true,
+ });
+ registerCleanupFunction(() => {
+ window.removeEventListener(eventType, listener, {
+ capture: true,
+ mozSystemGroup: true,
+ });
+ });
+ });
+ }
+ let promiseReplyF7KeyEvents = promiseFirstAndReplyKeyEvents(false);
+ EventUtils.synthesizeKey("KEY_F7");
+ info("Waiting reply F7 keypress event...");
+ await promiseReplyF7KeyEvents;
+ await TestUtils.waitForTick();
+ is(
+ Services.prefs.getBoolPref(kPrefName_CaretBrowsingOn),
+ true,
+ "F7 key should enable caret browsing mode"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[kPrefName_CaretBrowsingOn, false]],
+ });
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.documentElement.scrollTop; // Flush layout.
+ content.window.addEventListener(
+ "keydown",
+ event => event.preventDefault(),
+ { capture: true }
+ );
+ });
+ promiseReplyF7KeyEvents = promiseFirstAndReplyKeyEvents(true);
+ EventUtils.synthesizeKey("KEY_F7");
+ info("Waiting for reply F7 keydown event...");
+ await promiseReplyF7KeyEvents;
+ try {
+ info(`Checking reply keypress event is not fired...`);
+ await TestUtils.waitForCondition(
+ () => Services.prefs.getBoolPref(kPrefName_CaretBrowsingOn),
+ "",
+ 100, // interval
+ 5 // maxTries
+ );
+ } catch (e) {}
+ is(
+ Services.prefs.getBoolPref(kPrefName_CaretBrowsingOn),
+ false,
+ "F7 key shouldn't enable caret browsing mode because F7 keydown event is consumed by web content"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/keyboard/browser_popup_keyNav.js b/browser/base/content/test/keyboard/browser_popup_keyNav.js
new file mode 100644
index 0000000000..bf3c1ae44a
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_popup_keyNav.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+/**
+ * Keyboard navigation has some edgecases in popups because
+ * there is no tabstrip or menubar. Check that tabbing forward
+ * and backward to/from the content document works:
+ */
+add_task(async function test_popup_keynav() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.toolbars.keyboard_navigation", true],
+ ["accessibility.tabfocus", 7],
+ ],
+ });
+
+ const kURL = TEST_PATH + "focusableContent.html";
+ await BrowserTestUtils.withNewTab(kURL, async browser => {
+ let windowPromise = BrowserTestUtils.waitForNewWindow({
+ url: kURL,
+ });
+ SpecialPowers.spawn(browser, [], () => {
+ content.window.open(
+ content.location.href,
+ "_blank",
+ "height=500,width=500,menubar=no,toolbar=no,status=1,resizable=1"
+ );
+ });
+ let win = await windowPromise;
+ let hamburgerButton = win.document.getElementById("PanelUI-menu-button");
+ forceFocus(hamburgerButton);
+ await expectFocusAfterKey("Tab", win.gBrowser.selectedBrowser, false, win);
+ // Focus the button inside the webpage.
+ EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ // Focus the first item in the URL bar
+ let firstButton = win.document
+ .getElementById("urlbar-container")
+ .querySelector("toolbarbutton,[role=button]");
+ await expectFocusAfterKey("Tab", firstButton, false, win);
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
diff --git a/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js b/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js
new file mode 100644
index 0000000000..01c829c581
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js
@@ -0,0 +1,336 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kDevPanelID = "PanelUI-developer-tools";
+
+/**
+ * Test the behavior of key presses on various toolbar buttons.
+ */
+
+function waitForLocationChange() {
+ let promise = new Promise(resolve => {
+ let wpl = {
+ onLocationChange(aWebProgress, aRequest, aLocation) {
+ gBrowser.removeProgressListener(wpl);
+ resolve();
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+ });
+ return promise;
+}
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.keyboard_navigation", true]],
+ });
+});
+
+// Test activation of the app menu button from the keyboard.
+// The app menu should appear and focus should move inside it.
+add_task(async function testAppMenuButtonPress() {
+ let button = document.getElementById("PanelUI-menu-button");
+ forceFocus(button);
+ let focused = BrowserTestUtils.waitForEvent(
+ window.PanelUI.mainView,
+ "focus",
+ true
+ );
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(true, "Focus inside app menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(
+ window.PanelUI.panel,
+ "popuphidden"
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ await hidden;
+});
+
+// Test that the app menu doesn't open when a key other than space or enter is
+// pressed .
+add_task(async function testAppMenuButtonWrongKey() {
+ let button = document.getElementById("PanelUI-menu-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey("KEY_Tab");
+ await TestUtils.waitForTick();
+ is(window.PanelUI.panel.state, "closed", "App menu is closed after tab");
+});
+
+// Test activation of the Library button from the keyboard.
+// The Library menu should appear and focus should move inside it.
+add_task(async function testLibraryButtonPress() {
+ CustomizableUI.addWidgetToArea("library-button", "nav-bar");
+ let button = document.getElementById("library-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById("appMenu-libraryView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focused;
+ ok(true, "Focus inside Library menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ CustomizableUI.removeWidgetFromArea("library-button");
+});
+
+// Test activation of the Developer button from the keyboard.
+// This is a customizable widget of type "view".
+// The Developer menu should appear and focus should move inside it.
+add_task(async function testDeveloperButtonPress() {
+ CustomizableUI.addWidgetToArea(
+ "developer-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ let button = document.getElementById("developer-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById(kDevPanelID);
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focused;
+ ok(true, "Focus inside Developer menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ CustomizableUI.reset();
+});
+
+// Test that the Developer menu doesn't open when a key other than space or
+// enter is pressed .
+add_task(async function testDeveloperButtonWrongKey() {
+ CustomizableUI.addWidgetToArea(
+ "developer-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ let button = document.getElementById("developer-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey("KEY_Tab");
+ await TestUtils.waitForTick();
+ let panel = document.getElementById(kDevPanelID).closest("panel");
+ ok(!panel || panel.state == "closed", "Developer menu not open after tab");
+ CustomizableUI.reset();
+});
+
+// Test activation of the Page actions button from the keyboard.
+// The Page Actions menu should appear and focus should move inside it.
+add_task(async function testPageActionsButtonPress() {
+ // The page actions button is not normally visible, so we must
+ // unhide it.
+ BrowserPageActions.mainButtonNode.style.visibility = "visible";
+ registerCleanupFunction(() => {
+ BrowserPageActions.mainButtonNode.style.removeProperty("visibility");
+ });
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ let button = document.getElementById("pageActionButton");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById("pageActionPanelMainView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focused;
+ ok(true, "Focus inside Page Actions menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ });
+});
+
+// Test activation of the Back and Forward buttons from the keyboard.
+add_task(async function testBackForwardButtonPress() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/1",
+ async function (aBrowser) {
+ BrowserTestUtils.loadURIString(aBrowser, "https://example.com/2");
+
+ await BrowserTestUtils.browserLoaded(aBrowser);
+ let backButton = document.getElementById("back-button");
+ forceFocus(backButton);
+ let onLocationChange = waitForLocationChange();
+ EventUtils.synthesizeKey(" ");
+ await onLocationChange;
+ ok(true, "Location changed after back button pressed");
+
+ let forwardButton = document.getElementById("forward-button");
+ forceFocus(forwardButton);
+ onLocationChange = waitForLocationChange();
+ EventUtils.synthesizeKey(" ");
+ await onLocationChange;
+ ok(true, "Location changed after forward button pressed");
+ }
+ );
+});
+
+// Test activation of the Reload button from the keyboard.
+// This is a toolbarbutton with a click handler and no command handler, but
+// the toolbar keyboard navigation code should handle keyboard activation.
+add_task(async function testReloadButtonPress() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/1",
+ async function (aBrowser) {
+ let button = document.getElementById("reload-button");
+ info("Waiting for button to be enabled.");
+ await TestUtils.waitForCondition(() => !button.disabled);
+ let loaded = BrowserTestUtils.browserLoaded(aBrowser);
+ info("Focusing button");
+ forceFocus(button);
+ info("Pressing space on the button");
+ EventUtils.synthesizeKey(" ");
+ info("Waiting for load.");
+ await loaded;
+ ok(true, "Page loaded after Reload button pressed");
+ }
+ );
+});
+
+// Test activation of the Sidebars button from the keyboard.
+// This is a toolbarbutton with a command handler.
+add_task(async function testSidebarsButtonPress() {
+ CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
+ let button = document.getElementById("sidebar-button");
+ ok(!button.checked, "Sidebars button not checked at start of test");
+ let sidebarBox = document.getElementById("sidebar-box");
+ ok(sidebarBox.hidden, "Sidebar hidden at start of test");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ await TestUtils.waitForCondition(() => button.checked);
+ ok(true, "Sidebars button checked after press");
+ ok(!sidebarBox.hidden, "Sidebar visible after press");
+ // Make sure the sidebar is fully loaded before we hide it.
+ // Otherwise, the unload event might call JS which isn't loaded yet.
+ // We can't use BrowserTestUtils.browserLoaded because it fails on non-tab
+ // docs. Instead, wait for something in the JS script.
+ let sidebarWin = document.getElementById("sidebar").contentWindow;
+ await TestUtils.waitForCondition(() => sidebarWin.PlacesUIUtils);
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ await TestUtils.waitForCondition(() => !button.checked);
+ ok(true, "Sidebars button not checked after press");
+ ok(sidebarBox.hidden, "Sidebar hidden after press");
+ CustomizableUI.removeWidgetFromArea("sidebar-button");
+});
+
+// Test activation of the Bookmark this page button from the keyboard.
+// This is an image with a click handler on its parent and no command handler,
+// but the toolbar keyboard navigation code should handle keyboard activation.
+add_task(async function testBookmarkButtonPress() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (aBrowser) {
+ let button = document.getElementById("star-button-box");
+ forceFocus(button);
+ StarUI._createPanelIfNeeded();
+ let panel = document.getElementById("editBookmarkPanel");
+ let focused = BrowserTestUtils.waitForEvent(panel, "focus", true);
+ // The button ignores activation while the bookmarked status is being
+ // updated. So, wait for it to finish updating.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING
+ );
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(
+ true,
+ "Focus inside edit bookmark panel after Bookmark button pressed"
+ );
+ let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await hidden;
+ }
+ );
+});
+
+// Test activation of the Bookmarks Menu button from the keyboard.
+// This is a button with type="menu".
+// The Bookmarks Menu should appear.
+add_task(async function testBookmarksmenuButtonPress() {
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ let button = document.getElementById("bookmarks-menu-button");
+ forceFocus(button);
+ let menu = document.getElementById("BMB_bookmarksPopup");
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeKey(" ");
+ await shown;
+ ok(true, "Bookmarks Menu shown after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hidden;
+ CustomizableUI.reset();
+});
+
+// Test activation of the overflow button from the keyboard.
+// The overflow menu should appear and focus should move inside it.
+add_task(async function testOverflowButtonPress() {
+ // Move something to the overflow menu to make the button appear.
+ CustomizableUI.addWidgetToArea(
+ "developer-button",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+ let button = document.getElementById("nav-bar-overflow-button");
+ forceFocus(button);
+ let view = document.getElementById("widget-overflow-mainView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(true, "Focus inside overflow menu after toolbar button pressed");
+ let panel = document.getElementById("widget-overflow");
+ let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await hidden;
+ CustomizableUI.reset();
+});
+
+// Test activation of the Downloads button from the keyboard.
+// The Downloads panel should appear and focus should move inside it.
+add_task(async function testDownloadsButtonPress() {
+ DownloadsButton.unhide();
+ let button = document.getElementById("downloads-button");
+ forceFocus(button);
+ let panel = document.getElementById("downloadsPanel");
+ let focused = BrowserTestUtils.waitForEvent(panel, "focus", true);
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(true, "Focus inside Downloads panel after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await hidden;
+ DownloadsButton.hide();
+});
+
+// Test activation of the Save to Pocket button from the keyboard.
+// This is a customizable widget button which shows an popup panel
+// with a browser element to embed the pocket UI into it.
+// The Pocket panel should appear and focus should move inside it.
+add_task(async function testPocketButtonPress() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (aBrowser) {
+ let button = document.getElementById("save-to-pocket-button");
+ forceFocus(button);
+ // The panel is created on the fly, so we can't simply wait for focus
+ // inside it.
+ let showing = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshowing",
+ true
+ );
+ EventUtils.synthesizeKey(" ");
+ let event = await showing;
+ let panel = event.target;
+ is(panel.id, "customizationui-widget-panel");
+ let focused = BrowserTestUtils.waitForEvent(panel, "focus", true);
+ await focused;
+ is(
+ document.activeElement.tagName,
+ "browser",
+ "Focus inside Pocket panel after Bookmark button pressed"
+ );
+ let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await hidden;
+ }
+ );
+});
diff --git a/browser/base/content/test/keyboard/browser_toolbarKeyNav.js b/browser/base/content/test/keyboard/browser_toolbarKeyNav.js
new file mode 100644
index 0000000000..7fb67a6a91
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_toolbarKeyNav.js
@@ -0,0 +1,641 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test browser toolbar keyboard navigation.
+ * These tests assume the default browser configuration for toolbars unless
+ * otherwise specified.
+ */
+
+const PERMISSIONS_PAGE =
+ "https://example.com/browser/browser/base/content/test/permissions/permissions.html";
+const afterUrlBarButton = "save-to-pocket-button";
+
+// The DevEdition has the DevTools button in the toolbar by default. Remove it
+// to prevent branch-specific rules what button should be focused.
+function resetToolbarWithoutDevEditionButtons() {
+ CustomizableUI.reset();
+ CustomizableUI.removeWidgetFromArea("developer-button");
+}
+
+function AddHomeBesideReload() {
+ CustomizableUI.addWidgetToArea(
+ "home-button",
+ "nav-bar",
+ CustomizableUI.getPlacementOfWidget("stop-reload-button").position + 1
+ );
+}
+
+function RemoveHomeButton() {
+ CustomizableUI.removeWidgetFromArea("home-button");
+}
+
+function AddOldMenuSideButtons() {
+ // Make the FxA button visible even though we're signed out.
+ // We'll use oldfxastatus to restore the old state.
+ document.documentElement.setAttribute(
+ "oldfxastatus",
+ document.documentElement.getAttribute("fxastatus")
+ );
+ document.documentElement.setAttribute("fxastatus", "signed_in");
+ // The FxA button is supposed to be last, add these buttons before it.
+ CustomizableUI.addWidgetToArea(
+ "library-button",
+ "nav-bar",
+ CustomizableUI.getWidgetIdsInArea("nav-bar").length - 2
+ );
+ CustomizableUI.addWidgetToArea(
+ "sidebar-button",
+ "nav-bar",
+ CustomizableUI.getWidgetIdsInArea("nav-bar").length - 2
+ );
+ CustomizableUI.addWidgetToArea(
+ "unified-extensions-button",
+ "nav-bar",
+ CustomizableUI.getWidgetIdsInArea("nav-bar").length - 2
+ );
+}
+
+function RemoveOldMenuSideButtons() {
+ CustomizableUI.removeWidgetFromArea("library-button");
+ CustomizableUI.removeWidgetFromArea("sidebar-button");
+ document.documentElement.setAttribute(
+ "fxastatus",
+ document.documentElement.getAttribute("oldfxastatus")
+ );
+ document.documentElement.removeAttribute("oldfxastatus");
+}
+
+function startFromUrlBar(aWindow = window) {
+ aWindow.gURLBar.focus();
+ is(
+ aWindow.document.activeElement,
+ aWindow.gURLBar.inputField,
+ "URL bar focused for start of test"
+ );
+}
+
+// The Reload button is disabled for a short time even after the page finishes
+// loading. Wait for it to be enabled.
+async function waitUntilReloadEnabled() {
+ let button = document.getElementById("reload-button");
+ await TestUtils.waitForCondition(() => !button.disabled);
+}
+
+// Opens a new, blank tab, executes a task and closes the tab.
+function withNewBlankTab(taskFn) {
+ return BrowserTestUtils.withNewTab("about:blank", async function () {
+ // For a blank tab, the Reload button should be disabled. However, when we
+ // open about:blank with BrowserTestUtils.withNewTab, this is unreliable.
+ // Therefore, explicitly disable the reload command.
+ // We disable the command (rather than disabling the button directly) so the
+ // button will be updated correctly for future page loads.
+ document.getElementById("Browser:Reload").setAttribute("disabled", "true");
+ await taskFn();
+ });
+}
+
+function removeFirefoxViewButton() {
+ CustomizableUI.removeWidgetFromArea("firefox-view-button");
+}
+
+const BOOKMARKS_COUNT = 100;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.toolbars.keyboard_navigation", true],
+ ["accessibility.tabfocus", 7],
+ ],
+ });
+ resetToolbarWithoutDevEditionButtons();
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ // Add bookmarks.
+ let bookmarks = new Array(BOOKMARKS_COUNT);
+ for (let i = 0; i < BOOKMARKS_COUNT; ++i) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ bookmarks[i] = { url: `http://test.places.${i}/` };
+ }
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarks,
+ });
+
+ // The page actions button is not normally visible, so we must
+ // unhide it.
+ BrowserPageActions.mainButtonNode.style.visibility = "visible";
+ registerCleanupFunction(() => {
+ BrowserPageActions.mainButtonNode.style.removeProperty("visibility");
+ });
+});
+
+// Test tab stops with no page loaded.
+add_task(async function testTabStopsNoPageWithHomeButton() {
+ AddHomeBesideReload();
+ await withNewBlankTab(async function () {
+ startFromUrlBar();
+ await expectFocusAfterKey("Shift+Tab", "home-button");
+ await expectFocusAfterKey("Shift+Tab", "tabs-newtab-button");
+ await expectFocusAfterKey("Shift+Tab", gBrowser.selectedTab);
+ await expectFocusAfterKey("Tab", "tabs-newtab-button");
+ await expectFocusAfterKey("Tab", "home-button");
+ await expectFocusAfterKey("Tab", gURLBar.inputField);
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ await expectFocusAfterKey("Tab", gBrowser.selectedBrowser);
+ });
+ RemoveHomeButton();
+});
+
+async function doTestTabStopsPageLoaded(aPageActionsVisible) {
+ info(`doTestTabStopsPageLoaded(${aPageActionsVisible})`);
+
+ BrowserPageActions.mainButtonNode.style.visibility = aPageActionsVisible
+ ? "visible"
+ : "";
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ await waitUntilReloadEnabled();
+ startFromUrlBar();
+ await expectFocusAfterKey(
+ "Shift+Tab",
+ "tracking-protection-icon-container"
+ );
+ await expectFocusAfterKey("Shift+Tab", "reload-button");
+ await expectFocusAfterKey("Shift+Tab", "tabs-newtab-button");
+ await expectFocusAfterKey("Shift+Tab", gBrowser.selectedTab);
+ await expectFocusAfterKey("Tab", "tabs-newtab-button");
+ await expectFocusAfterKey("Tab", "reload-button");
+ await expectFocusAfterKey("Tab", "tracking-protection-icon-container");
+ await expectFocusAfterKey("Tab", gURLBar.inputField);
+ await expectFocusAfterKey(
+ "Tab",
+ aPageActionsVisible ? "pageActionButton" : "star-button-box"
+ );
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ await expectFocusAfterKey("Tab", gBrowser.selectedBrowser);
+ });
+}
+
+// Test tab stops with a page loaded.
+add_task(async function testTabStopsPageLoaded() {
+ is(
+ BrowserPageActions.mainButtonNode.style.visibility,
+ "visible",
+ "explicitly shown at the beginning of test"
+ );
+ await doTestTabStopsPageLoaded(false);
+ await doTestTabStopsPageLoaded(true);
+});
+
+// Test tab stops with a notification anchor visible.
+// The notification anchor should not get its own tab stop.
+add_task(async function testTabStopsWithNotification() {
+ await BrowserTestUtils.withNewTab(
+ PERMISSIONS_PAGE,
+ async function (aBrowser) {
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ // Request a permission.
+ BrowserTestUtils.synthesizeMouseAtCenter("#geo", {}, aBrowser);
+ await popupShown;
+ startFromUrlBar();
+ // If the notification anchor were in the tab order, the next shift+tab
+ // would focus it instead of #tracking-protection-icon-container.
+ await expectFocusAfterKey(
+ "Shift+Tab",
+ "tracking-protection-icon-container"
+ );
+ }
+ );
+});
+
+// Test tab stops with the Bookmarks toolbar visible.
+add_task(async function testTabStopsWithBookmarksToolbar() {
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ CustomizableUI.setToolbarVisibility("PersonalToolbar", true);
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ await expectFocusAfterKey("Tab", "PersonalToolbar", true);
+ await expectFocusAfterKey("Tab", gBrowser.selectedBrowser);
+
+ // Make sure the Bookmarks toolbar is no longer tabbable once hidden.
+ CustomizableUI.setToolbarVisibility("PersonalToolbar", false);
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ await expectFocusAfterKey("Tab", gBrowser.selectedBrowser);
+ });
+});
+
+// Test a focusable toolbartabstop which has no navigable buttons.
+add_task(async function testTabStopNoButtons() {
+ await withNewBlankTab(async function () {
+ // The Back, Forward and Reload buttons are all currently disabled.
+ // The Home button is the only other button at that tab stop.
+ CustomizableUI.removeWidgetFromArea("home-button");
+ startFromUrlBar();
+ await expectFocusAfterKey("Shift+Tab", "tabs-newtab-button");
+ await expectFocusAfterKey("Tab", gURLBar.inputField);
+ resetToolbarWithoutDevEditionButtons();
+ AddHomeBesideReload();
+ // Make sure the button is reachable now that it has been re-added.
+ await expectFocusAfterKey("Shift+Tab", "home-button", true);
+ RemoveHomeButton();
+ });
+});
+
+// Test that right/left arrows move through toolbarbuttons.
+// This also verifies that:
+// 1. Right/left arrows do nothing when at the edges; and
+// 2. The overflow menu button can't be reached by right arrow when it isn't
+// visible.
+add_task(async function testArrowsToolbarbuttons() {
+ AddOldMenuSideButtons();
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ document.activeElement.id,
+ afterUrlBarButton,
+ "ArrowLeft at end of button group does nothing"
+ );
+ await expectFocusAfterKey("ArrowRight", "library-button");
+ await expectFocusAfterKey("ArrowRight", "sidebar-button");
+ await expectFocusAfterKey("ArrowRight", "unified-extensions-button");
+ await expectFocusAfterKey("ArrowRight", "fxa-toolbar-menu-button");
+ // This next check also confirms that the overflow menu button is skipped,
+ // since it is currently invisible.
+ await expectFocusAfterKey("ArrowRight", "PanelUI-menu-button");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ is(
+ document.activeElement.id,
+ "PanelUI-menu-button",
+ "ArrowRight at end of button group does nothing"
+ );
+ await expectFocusAfterKey("ArrowLeft", "fxa-toolbar-menu-button");
+ await expectFocusAfterKey("ArrowLeft", "unified-extensions-button");
+ await expectFocusAfterKey("ArrowLeft", "sidebar-button");
+ await expectFocusAfterKey("ArrowLeft", "library-button");
+ });
+ RemoveOldMenuSideButtons();
+});
+
+// Test that right/left arrows move through buttons which aren't toolbarbuttons
+// but have role="button".
+add_task(async function testArrowsRoleButton() {
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", "pageActionButton");
+ await expectFocusAfterKey("ArrowRight", "star-button-box");
+ await expectFocusAfterKey("ArrowLeft", "pageActionButton");
+ });
+});
+
+// Test that right/left arrows do not land on disabled buttons.
+add_task(async function testArrowsDisabledButtons() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (aBrowser) {
+ await waitUntilReloadEnabled();
+ startFromUrlBar();
+ await expectFocusAfterKey(
+ "Shift+Tab",
+ "tracking-protection-icon-container"
+ );
+ // Back and Forward buttons are disabled.
+ await expectFocusAfterKey("Shift+Tab", "reload-button");
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ document.activeElement.id,
+ "reload-button",
+ "ArrowLeft on Reload button when prior buttons disabled does nothing"
+ );
+
+ BrowserTestUtils.loadURIString(aBrowser, "https://example.com/2");
+ await BrowserTestUtils.browserLoaded(aBrowser);
+ await waitUntilReloadEnabled();
+ startFromUrlBar();
+ await expectFocusAfterKey(
+ "Shift+Tab",
+ "tracking-protection-icon-container"
+ );
+ await expectFocusAfterKey("Shift+Tab", "back-button");
+ // Forward button is still disabled.
+ await expectFocusAfterKey("ArrowRight", "reload-button");
+ }
+ );
+});
+
+// Test that right arrow reaches the overflow menu button when it is visible.
+add_task(async function testArrowsOverflowButton() {
+ AddOldMenuSideButtons();
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ // Move something to the overflow menu to make the button appear.
+ CustomizableUI.addWidgetToArea(
+ "home-button",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ await expectFocusAfterKey("ArrowRight", "library-button");
+ await expectFocusAfterKey("ArrowRight", "sidebar-button");
+ await expectFocusAfterKey("ArrowRight", "unified-extensions-button");
+ await expectFocusAfterKey("ArrowRight", "fxa-toolbar-menu-button");
+ await expectFocusAfterKey("ArrowRight", "nav-bar-overflow-button");
+ // Make sure the button is not reachable once it is invisible again.
+ await expectFocusAfterKey("ArrowRight", "PanelUI-menu-button");
+ resetToolbarWithoutDevEditionButtons();
+ // Flush layout so its invisibility can be detected.
+ document.getElementById("nav-bar-overflow-button").clientWidth;
+ // We reset the toolbar above so the unified extensions button is now the
+ // "last" button.
+ await expectFocusAfterKey("ArrowLeft", "unified-extensions-button");
+ await expectFocusAfterKey("ArrowLeft", "fxa-toolbar-menu-button");
+ });
+ RemoveOldMenuSideButtons();
+});
+
+// Test that toolbar keyboard navigation doesn't interfere with PanelMultiView
+// keyboard navigation.
+// We do this by opening the Library menu and ensuring that pressing left arrow
+// does nothing.
+add_task(async function testArrowsInPanelMultiView() {
+ AddOldMenuSideButtons();
+ let button = document.getElementById("library-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById("appMenu-libraryView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ let focusEvt = await focused;
+ ok(true, "Focus inside Library menu after toolbar button pressed");
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ document.activeElement,
+ focusEvt.target,
+ "ArrowLeft inside panel does nothing"
+ );
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ RemoveOldMenuSideButtons();
+});
+
+// Test that right/left arrows move in the expected direction for RTL locales.
+add_task(async function testArrowsRtl() {
+ AddOldMenuSideButtons();
+ await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] });
+ // window.RTL_UI doesn't update in existing windows when this pref is changed,
+ // so we need to test in a new window.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ startFromUrlBar(win);
+ await expectFocusAfterKey("Tab", afterUrlBarButton, false, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ is(
+ win.document.activeElement.id,
+ afterUrlBarButton,
+ "ArrowRight at end of button group does nothing"
+ );
+ await expectFocusAfterKey("ArrowLeft", "library-button", false, win);
+ await expectFocusAfterKey("ArrowLeft", "sidebar-button", false, win);
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+ RemoveOldMenuSideButtons();
+});
+
+// Test that right arrow reaches the overflow menu button on the Bookmarks
+// toolbar when it is visible.
+add_task(async function testArrowsBookmarksOverflowButton() {
+ let toolbar = gNavToolbox.querySelector("#PersonalToolbar");
+ // Third parameter is 'persist' and true is the default.
+ // Fourth parameter is 'animated' and we want no animation.
+ setToolbarVisibility(toolbar, true, true, false);
+ Assert.ok(!toolbar.collapsed, "toolbar should be visible");
+
+ await BrowserTestUtils.waitForEvent(
+ toolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ let items = document.getElementById("PlacesToolbarItems").children;
+ let lastVisible;
+ for (let item of items) {
+ if (item.style.visibility == "hidden") {
+ break;
+ }
+ lastVisible = item;
+ }
+ forceFocus(lastVisible);
+ await expectFocusAfterKey("ArrowRight", "PlacesChevron");
+ setToolbarVisibility(toolbar, false, true, false);
+});
+
+registerCleanupFunction(async function () {
+ CustomizableUI.reset();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+// Test that when a toolbar button opens a panel, closing the panel restores
+// focus to the button which opened it.
+add_task(async function testPanelCloseRestoresFocus() {
+ AddOldMenuSideButtons();
+ await withNewBlankTab(async function () {
+ // We can't use forceFocus because that removes focusability immediately.
+ // Instead, we must let ToolbarKeyboardNavigator handle this properly.
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ await expectFocusAfterKey("ArrowRight", "library-button");
+ let view = document.getElementById("appMenu-libraryView");
+ let shown = BrowserTestUtils.waitForEvent(view, "ViewShown");
+ EventUtils.synthesizeKey(" ");
+ await shown;
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ is(
+ document.activeElement.id,
+ "library-button",
+ "Focus restored to Library button after panel closed"
+ );
+ });
+ RemoveOldMenuSideButtons();
+});
+
+// Test that the arrow key works in the group of the
+// 'tracking-protection-icon-container' and the 'identity-box'.
+add_task(async function testArrowKeyForTPIconContainerandIdentityBox() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ // Simulate geo sharing so the permission box shows
+ gBrowser.updateBrowserSharing(browser, { geo: true });
+ await waitUntilReloadEnabled();
+ startFromUrlBar();
+ await expectFocusAfterKey(
+ "Shift+Tab",
+ "tracking-protection-icon-container"
+ );
+ await expectFocusAfterKey("ArrowRight", "identity-icon-box");
+ await expectFocusAfterKey("ArrowRight", "identity-permission-box");
+ await expectFocusAfterKey("ArrowLeft", "identity-icon-box");
+ await expectFocusAfterKey(
+ "ArrowLeft",
+ "tracking-protection-icon-container"
+ );
+ gBrowser.updateBrowserSharing(browser, { geo: false });
+ }
+ );
+});
+
+// Test navigation by typed characters.
+add_task(async function testCharacterNavigation() {
+ AddHomeBesideReload();
+ AddOldMenuSideButtons();
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ await waitUntilReloadEnabled();
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", "pageActionButton");
+ await expectFocusAfterKey("h", "home-button");
+ // There's no button starting with "hs", so pressing s should do nothing.
+ EventUtils.synthesizeKey("s");
+ is(
+ document.activeElement.id,
+ "home-button",
+ "home-button still focused after s pressed"
+ );
+ // Escape should reset the search.
+ EventUtils.synthesizeKey("KEY_Escape");
+ // Now that the search is reset, pressing s should focus Save to Pocket.
+ await expectFocusAfterKey("s", "save-to-pocket-button");
+ // Pressing i makes the search "si", so it should focus Sidebars.
+ await expectFocusAfterKey("i", "sidebar-button");
+ // Reset the search.
+ EventUtils.synthesizeKey("KEY_Escape");
+ await expectFocusAfterKey("s", "save-to-pocket-button");
+ // Pressing s again should find the next button starting with s: Sidebars.
+ await expectFocusAfterKey("s", "sidebar-button");
+ });
+ RemoveHomeButton();
+ RemoveOldMenuSideButtons();
+});
+
+// Test that toolbar character navigation doesn't trigger in PanelMultiView for
+// a panel anchored to the toolbar.
+// We do this by opening the Library menu and ensuring that pressing s
+// does nothing.
+// This test should be removed if PanelMultiView implements character
+// navigation.
+add_task(async function testCharacterInPanelMultiView() {
+ AddOldMenuSideButtons();
+ let button = document.getElementById("library-button");
+ forceFocus(button);
+ let view = document.getElementById("appMenu-libraryView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ EventUtils.synthesizeKey(" ");
+ let focusEvt = await focused;
+ ok(true, "Focus inside Library menu after toolbar button pressed");
+ EventUtils.synthesizeKey("s");
+ is(document.activeElement, focusEvt.target, "s inside panel does nothing");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ RemoveOldMenuSideButtons();
+});
+
+// Test tab stops after the search bar is added.
+add_task(async function testTabStopsAfterSearchBarAdded() {
+ AddOldMenuSideButtons();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", 1]],
+ });
+ await withNewBlankTab(async function () {
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", "searchbar", true);
+ await expectFocusAfterKey("Tab", afterUrlBarButton);
+ await expectFocusAfterKey("ArrowRight", "library-button");
+ });
+ await SpecialPowers.popPrefEnv();
+ RemoveOldMenuSideButtons();
+});
+
+// Test tab navigation when the Firefox View button is present
+// and when the button is not present.
+add_task(async function testFirefoxViewButtonNavigation() {
+ // Add enough tabs so that the new-tab-button appears in the toolbar
+ // and the tabs-newtab-button is hidden.
+ await BrowserTestUtils.overflowTabs(registerCleanupFunction, window);
+
+ // Assert that Firefox View button receives focus when tab navigating
+ // forward from the end of web content.
+ // Additionally, ensure that focus is not trapped between the
+ // selected tab and the new-tab button.
+ // Finally, assert that focus is restored to web content when
+ // navigating backwards from the Firefox View button.
+ await BrowserTestUtils.withNewTab(
+ PERMISSIONS_PAGE,
+ async function (aBrowser) {
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ content.document.querySelector("#camera").focus();
+ });
+
+ await expectFocusAfterKey("Tab", "firefox-view-button");
+ let selectedTab = document.querySelector("tab[selected]");
+ await expectFocusAfterKey("Tab", selectedTab);
+ await expectFocusAfterKey("Tab", "new-tab-button");
+ await expectFocusAfterKey("Shift+Tab", selectedTab);
+ await expectFocusAfterKey("Shift+Tab", "firefox-view-button");
+
+ // Moving from toolbar back into content
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ let activeElement = content.document.activeElement;
+ let expectedElement = content.document.querySelector("#camera");
+ is(
+ activeElement,
+ expectedElement,
+ "Focus should be returned to the 'camera' button"
+ );
+ });
+ }
+ );
+
+ // Assert that the selected tab receives focus before the new-tab button
+ // if there is no Firefox View button.
+ // Additionally, assert that navigating backwards from the selected tab
+ // restores focus to the last element in the web content.
+ await BrowserTestUtils.withNewTab(
+ PERMISSIONS_PAGE,
+ async function (aBrowser) {
+ removeFirefoxViewButton();
+
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ content.document.querySelector("#camera").focus();
+ });
+
+ let selectedTab = document.querySelector("tab[selected]");
+ await expectFocusAfterKey("Tab", selectedTab);
+ await expectFocusAfterKey("Tab", "new-tab-button");
+ await expectFocusAfterKey("Shift+Tab", selectedTab);
+
+ // Moving from toolbar back into content
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ let activeElement = content.document.activeElement;
+ let expectedElement = content.document.querySelector("#camera");
+ is(
+ activeElement,
+ expectedElement,
+ "Focus should be returned to the 'camera' button"
+ );
+ });
+ }
+ );
+
+ // Clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[0]);
+ }
+ CustomizableUI.reset();
+});
diff --git a/browser/base/content/test/keyboard/file_empty.html b/browser/base/content/test/keyboard/file_empty.html
new file mode 100644
index 0000000000..d2b0361f09
--- /dev/null
+++ b/browser/base/content/test/keyboard/file_empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Page left intentionally blank...</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/keyboard/focusableContent.html b/browser/base/content/test/keyboard/focusableContent.html
new file mode 100644
index 0000000000..255512645c
--- /dev/null
+++ b/browser/base/content/test/keyboard/focusableContent.html
@@ -0,0 +1 @@
+<button>Just a button here to have something focusable.</button>
diff --git a/browser/base/content/test/keyboard/head.js b/browser/base/content/test/keyboard/head.js
new file mode 100644
index 0000000000..9d6f901f2c
--- /dev/null
+++ b/browser/base/content/test/keyboard/head.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Force focus to an element that isn't focusable.
+ * Toolbar buttons aren't focusable because if they were, clicking them would
+ * focus them, which is undesirable. Therefore, they're only made focusable
+ * when a user is navigating with the keyboard. This function forces focus as
+ * is done during toolbar keyboard navigation.
+ */
+function forceFocus(aElem) {
+ aElem.setAttribute("tabindex", "-1");
+ aElem.focus();
+ aElem.removeAttribute("tabindex");
+}
+
+async function expectFocusAfterKey(
+ aKey,
+ aFocus,
+ aAncestorOk = false,
+ aWindow = window
+) {
+ let res = aKey.match(/^(Shift\+)?(?:(.)|(.+))$/);
+ let shift = Boolean(res[1]);
+ let key;
+ if (res[2]) {
+ key = res[2]; // Character.
+ } else {
+ key = "KEY_" + res[3]; // Tab, ArrowRight, etc.
+ }
+ let expected;
+ let friendlyExpected;
+ if (typeof aFocus == "string") {
+ expected = aWindow.document.getElementById(aFocus);
+ friendlyExpected = aFocus;
+ } else {
+ expected = aFocus;
+ if (aFocus == aWindow.gURLBar.inputField) {
+ friendlyExpected = "URL bar input";
+ } else if (aFocus == aWindow.gBrowser.selectedBrowser) {
+ friendlyExpected = "Web document";
+ }
+ }
+ info("Listening on item " + (expected.id || expected.className));
+ let focused = BrowserTestUtils.waitForEvent(expected, "focus", aAncestorOk);
+ EventUtils.synthesizeKey(key, { shiftKey: shift }, aWindow);
+ let receivedEvent = await focused;
+ info(
+ "Got focus on item: " +
+ (receivedEvent.target.id || receivedEvent.target.className)
+ );
+ ok(true, friendlyExpected + " focused after " + aKey + " pressed");
+}
diff --git a/browser/base/content/test/menubar/browser.ini b/browser/base/content/test/menubar/browser.ini
new file mode 100644
index 0000000000..e32dc6dffc
--- /dev/null
+++ b/browser/base/content/test/menubar/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+
+[browser_file_close_tabs.js]
+[browser_file_menu_import_wizard.js]
+[browser_file_share.js]
+https_first_disabled = true
+run-if = os == "mac" # Mac only feature
+support-files =
+ file_shareurl.html
diff --git a/browser/base/content/test/menubar/browser_file_close_tabs.js b/browser/base/content/test/menubar/browser_file_close_tabs.js
new file mode 100644
index 0000000000..15abd92bba
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_file_close_tabs.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/**
+ * This test verifies behavior from bug 1732375:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1732375
+ *
+ * If there are multiple tabs selected, the 'Close' entry
+ * under the File menu should correctly reflect the number of
+ * selected tabs
+ */
+add_task(async function test_menu_close_tab_count() {
+ // Window should have one tab open already, so we
+ // just need to add one more to have a total of two
+ info("Adding new tabs");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+
+ info("Selecting all tabs");
+ await gBrowser.selectAllTabs();
+ is(gBrowser.multiSelectedTabsCount, 2, "Two (2) tabs are selected");
+
+ let fileMenu = document.getElementById("menu_FilePopup");
+ await simulateMenuOpen(fileMenu);
+
+ let closeMenuEntry = document.getElementById("menu_close");
+ let closeMenuL10nArgsObject = document.l10n.getAttributes(closeMenuEntry);
+
+ is(
+ closeMenuL10nArgsObject.args.tabCount,
+ 2,
+ "Menu bar reflects multi-tab selection number (Close 2 Tabs)"
+ );
+
+ let onClose = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await onClose;
+
+ info("Tabs closed");
+});
+
+async function simulateMenuOpen(menu) {
+ return new Promise(resolve => {
+ menu.addEventListener("popupshown", resolve, { once: true });
+ menu.dispatchEvent(new MouseEvent("popupshowing"));
+ menu.dispatchEvent(new MouseEvent("popupshown"));
+ });
+}
+
+async function simulateMenuClosed(menu) {
+ return new Promise(resolve => {
+ menu.addEventListener("popuphidden", resolve, { once: true });
+ menu.dispatchEvent(new MouseEvent("popuphiding"));
+ menu.dispatchEvent(new MouseEvent("popuphidden"));
+ });
+}
diff --git a/browser/base/content/test/menubar/browser_file_menu_import_wizard.js b/browser/base/content/test/menubar/browser_file_menu_import_wizard.js
new file mode 100644
index 0000000000..7783d59da7
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_file_menu_import_wizard.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async () => {
+ // Load the initial tab at example.com. This makes it so that if
+ // we're using the new migration wizard, we'll load the about:preferences
+ // page in a new tab rather than overtaking the initial one. This
+ // makes it easier to be consistent with closing and opening
+ // behaviours between the two kinds of migration wizards.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURIString(browser, "https://example.com");
+ await BrowserTestUtils.browserLoaded(browser);
+});
+
+add_task(async function file_menu_import_wizard() {
+ // We can't call this code directly or our JS execution will get blocked on Windows/Linux where
+ // the dialog is modal.
+ executeSoon(() =>
+ document.getElementById("menu_importFromAnotherBrowser").doCommand()
+ );
+
+ let wizard = await BrowserTestUtils.waitForMigrationWizard(window);
+ ok(wizard, "Migrator window opened");
+ await BrowserTestUtils.closeMigrationWizard(wizard);
+});
diff --git a/browser/base/content/test/menubar/browser_file_share.js b/browser/base/content/test/menubar/browser_file_share.js
new file mode 100644
index 0000000000..bd6a4c3f60
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_file_share.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const TEST_URL = BASE + "file_shareurl.html";
+
+let mockShareData = [
+ {
+ name: "Test",
+ menuItemTitle: "Sharing Service Test",
+ image:
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKE" +
+ "lEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==",
+ },
+];
+
+// Setup spies for observing function calls from MacSharingService
+let shareUrlSpy = sinon.spy();
+let openSharingPreferencesSpy = sinon.spy();
+let getSharingProvidersSpy = sinon.spy();
+
+let stub = sinon.stub(gBrowser, "MacSharingService").get(() => {
+ return {
+ getSharingProviders(url) {
+ getSharingProvidersSpy(url);
+ return mockShareData;
+ },
+ shareUrl(name, url, title) {
+ shareUrlSpy(name, url, title);
+ },
+ openSharingPreferences() {
+ openSharingPreferencesSpy();
+ },
+ };
+});
+
+registerCleanupFunction(async function () {
+ stub.restore();
+});
+
+/**
+ * Test the "Share" item menus in the tab contextmenu on MacOSX.
+ */
+add_task(async function test_file_menu_share() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async () => {
+ // We can't toggle menubar items on OSX, so mocking instead.
+ let menu = document.getElementById("menu_FilePopup");
+ await simulateMenuOpen(menu);
+
+ await BrowserTestUtils.waitForMutationCondition(
+ menu,
+ { childList: true },
+ () => menu.querySelector(".share-tab-url-item")
+ );
+ ok(true, "Got Share item");
+
+ let popup = menu.querySelector(".share-tab-url-item").menupopup;
+ await simulateMenuOpen(popup);
+ ok(getSharingProvidersSpy.calledOnce, "getSharingProviders called");
+
+ info(
+ "Check we have a service and one extra menu item for the More... button"
+ );
+ let items = popup.querySelectorAll("menuitem");
+ is(items.length, 2, "There should be 2 sharing services.");
+
+ info("Click on the sharing service");
+ let shareButton = items[0];
+ is(
+ shareButton.label,
+ mockShareData[0].menuItemTitle,
+ "Share button's label should match the service's menu item title. "
+ );
+ is(
+ shareButton.getAttribute("share-name"),
+ mockShareData[0].name,
+ "Share button's share-name value should match the service's name. "
+ );
+
+ shareButton.doCommand();
+
+ ok(shareUrlSpy.calledOnce, "shareUrl called");
+
+ info("Check the correct data was shared.");
+ let [name, url, title] = shareUrlSpy.getCall(0).args;
+ is(name, mockShareData[0].name, "Shared correct service name");
+ is(url, TEST_URL, "Shared correct URL");
+ is(title, "Sharing URL", "Shared the correct title.");
+ await simulateMenuClosed(popup);
+ await simulateMenuClosed(menu);
+
+ info("Test the More... button");
+
+ await simulateMenuOpen(menu);
+ popup = menu.querySelector(".share-tab-url-item").menupopup;
+ await simulateMenuOpen(popup);
+ // Since the menu was collapsed previously, the popup needs to get the
+ // providers again.
+ ok(getSharingProvidersSpy.calledTwice, "getSharingProviders called again");
+ items = popup.querySelectorAll("menuitem");
+ is(items.length, 2, "There should be 2 sharing services.");
+
+ info("Click on the More Button");
+ let moreButton = items[1];
+ moreButton.doCommand();
+ ok(openSharingPreferencesSpy.calledOnce, "openSharingPreferences called");
+ // Tidy up:
+ await simulateMenuClosed(popup);
+ await simulateMenuClosed(menu);
+ });
+});
+
+async function simulateMenuOpen(menu) {
+ return new Promise(resolve => {
+ menu.addEventListener("popupshown", resolve, { once: true });
+ menu.dispatchEvent(new MouseEvent("popupshowing"));
+ menu.dispatchEvent(new MouseEvent("popupshown"));
+ });
+}
+
+async function simulateMenuClosed(menu) {
+ return new Promise(resolve => {
+ menu.addEventListener("popuphidden", resolve, { once: true });
+ menu.dispatchEvent(new MouseEvent("popuphiding"));
+ menu.dispatchEvent(new MouseEvent("popuphidden"));
+ });
+}
diff --git a/browser/base/content/test/menubar/file_shareurl.html b/browser/base/content/test/menubar/file_shareurl.html
new file mode 100644
index 0000000000..c7fb193972
--- /dev/null
+++ b/browser/base/content/test/menubar/file_shareurl.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Sharing URL</title>
diff --git a/browser/base/content/test/metaTags/bad_meta_tags.html b/browser/base/content/test/metaTags/bad_meta_tags.html
new file mode 100644
index 0000000000..ce687d7792
--- /dev/null
+++ b/browser/base/content/test/metaTags/bad_meta_tags.html
@@ -0,0 +1,14 @@
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>BadMetaTags</title>
+ <meta property="twitter:image" content="http://test.com/twitter-image.jpg" />
+ <meta property="og:image:url" content="ftp://test.com/og-image-url" />
+ <meta property="og:image" content="file:///Users/invalid/img.jpg" />
+ <meta property="twitter:description" />
+ <meta property="og:description" content="" />
+ <meta name="description" content="description" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/metaTags/browser.ini b/browser/base/content/test/metaTags/browser.ini
new file mode 100644
index 0000000000..4468d331f0
--- /dev/null
+++ b/browser/base/content/test/metaTags/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_bad_meta_tags.js]
+support-files = bad_meta_tags.html
+[browser_meta_tags.js]
+skip-if = tsan # Bug 1403403
+support-files = meta_tags.html
diff --git a/browser/base/content/test/metaTags/browser_bad_meta_tags.js b/browser/base/content/test/metaTags/browser_bad_meta_tags.js
new file mode 100644
index 0000000000..00cc128ec0
--- /dev/null
+++ b/browser/base/content/test/metaTags/browser_bad_meta_tags.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_PATH =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "bad_meta_tags.html";
+
+/**
+ * This tests that with the page bad_meta_tags.html, ContentMetaHandler.jsm parses
+ * out the meta tags available and does not store content provided by a malformed
+ * meta tag. In this case the best defined meta tags are malformed, so here we
+ * test that we store the next best ones - "description" and "twitter:image". The
+ * list of meta tags and order of preference is found in ContentMetaHandler.jsm.
+ */
+add_task(async function test_bad_meta_tags() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH);
+
+ // Wait until places has stored the page info
+ const pageInfo = await waitForPageInfo(TEST_PATH);
+ is(
+ pageInfo.description,
+ "description",
+ "did not collect a og:description because meta tag was malformed"
+ );
+ is(
+ pageInfo.previewImageURL.href,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test.com/twitter-image.jpg",
+ "did not collect og:image because of invalid loading principal"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/base/content/test/metaTags/browser_meta_tags.js b/browser/base/content/test/metaTags/browser_meta_tags.js
new file mode 100644
index 0000000000..380a71214c
--- /dev/null
+++ b/browser/base/content/test/metaTags/browser_meta_tags.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_PATH =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "meta_tags.html";
+/**
+ * This tests that with the page meta_tags.html, ContentMetaHandler.jsm parses
+ * out the meta tags avilable and only stores the best one for description and
+ * one for preview image url. In the case of this test, the best defined meta
+ * tags are "og:description" and "og:image:secure_url". The list of meta tags
+ * and order of preference is found in ContentMetaHandler.jsm.
+ */
+add_task(async function test_metadata() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH);
+
+ // Wait until places has stored the page info
+ const pageInfo = await waitForPageInfo(TEST_PATH);
+ is(pageInfo.description, "og:description", "got the correct description");
+ is(
+ pageInfo.previewImageURL.href,
+ "https://test.com/og-image-secure-url.jpg",
+ "got the correct preview image"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+});
+
+/**
+ * This test is almost like the previous one except it opens a second tab to
+ * make sure the extra tab does not cause the debounce logic to be skipped. If
+ * incorrectly skipped, the updated metadata would not include the delayed meta.
+ */
+add_task(async function multiple_tabs() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH);
+
+ // Add a background tab to cause another page to load *without* putting the
+ // desired URL in a background tab, which results in its timers being throttled.
+ BrowserTestUtils.addTab(gBrowser);
+
+ // Wait until places has stored the page info
+ const pageInfo = await waitForPageInfo(TEST_PATH);
+ is(pageInfo.description, "og:description", "got the correct description");
+ is(
+ pageInfo.previewImageURL.href,
+ "https://test.com/og-image-secure-url.jpg",
+ "got the correct preview image"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/base/content/test/metaTags/head.js b/browser/base/content/test/metaTags/head.js
new file mode 100644
index 0000000000..1f292c4c03
--- /dev/null
+++ b/browser/base/content/test/metaTags/head.js
@@ -0,0 +1,19 @@
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+/**
+ * Wait for url's page info (non-null description and preview url) to be set.
+ * Because there is debounce logic in ContentLinkHandler.jsm to only make one
+ * single SQL update, we have to wait for some time before checking that the page
+ * info was stored.
+ */
+async function waitForPageInfo(url) {
+ let pageInfo;
+ await BrowserTestUtils.waitForCondition(async () => {
+ pageInfo = await PlacesUtils.history.fetch(url, { includeMeta: true });
+ return pageInfo && pageInfo.description && pageInfo.previewImageURL;
+ });
+ return pageInfo;
+}
diff --git a/browser/base/content/test/metaTags/meta_tags.html b/browser/base/content/test/metaTags/meta_tags.html
new file mode 100644
index 0000000000..ad162da1f5
--- /dev/null
+++ b/browser/base/content/test/metaTags/meta_tags.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>MetaTags</title>
+ <meta property="twitter:description" content="twitter:description" />
+ <meta property="og:description" content="og:description" />
+ <meta name="description" content="description" />
+ <meta name="unknown:tag" content="unknown:tag" />
+ <meta property="og:image" content="https://test.com/og-image.jpg" />
+ <meta property="twitter:image" content="https://test.com/twitter-image.jpg" />
+ <meta property="og:image:url" content="https://test.com/og-image-url" />
+ <meta name="thumbnail" content="https://test.com/thumbnail.jpg" />
+ </head>
+ <body>
+ <script>
+ function addMeta(tag) {
+ const meta = document.createElement("meta");
+ meta.content = "https://test.com/og-image-secure-url.jpg";
+ meta.setAttribute("property", tag);
+ document.head.appendChild(meta);
+ }
+
+ // Delay adding this "best" image tag to test that later tags are used.
+ // Use a delay that is long enough for tests to check for wrong metadata.
+ setTimeout(() => addMeta("og:image:secure_url"), 100);
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/notificationbox/browser.ini b/browser/base/content/test/notificationbox/browser.ini
new file mode 100644
index 0000000000..f5d296ca10
--- /dev/null
+++ b/browser/base/content/test/notificationbox/browser.ini
@@ -0,0 +1,3 @@
+[browser_notification_stacking.js]
+[browser_notificationbar_telemetry.js]
+[browser_tabnotificationbox_switch_tabs.js]
diff --git a/browser/base/content/test/notificationbox/browser_notification_stacking.js b/browser/base/content/test/notificationbox/browser_notification_stacking.js
new file mode 100644
index 0000000000..bd8817ea4b
--- /dev/null
+++ b/browser/base/content/test/notificationbox/browser_notification_stacking.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function addNotification(box, label, value, priorityName) {
+ let added = BrowserTestUtils.waitForNotificationInNotificationBox(box, value);
+ let priority = gNotificationBox[`PRIORITY_${priorityName}_MEDIUM`];
+ let notification = box.appendNotification(value, { label, priority });
+ await added;
+ return notification;
+}
+
+add_task(async function testStackingOrder() {
+ const tabNotificationBox = gBrowser.getNotificationBox();
+ ok(
+ gNotificationBox.stack.hasAttribute("prepend-notifications"),
+ "Browser stack will prepend"
+ );
+ ok(
+ !tabNotificationBox.stack.hasAttribute("prepend-notifications"),
+ "Tab stack will append"
+ );
+
+ let browserOne = await addNotification(
+ gNotificationBox,
+ "My first browser notification",
+ "browser-one",
+ "INFO"
+ );
+
+ let tabOne = await addNotification(
+ tabNotificationBox,
+ "My first tab notification",
+ "tab-one",
+ "CRITICAL"
+ );
+
+ let browserTwo = await addNotification(
+ gNotificationBox,
+ "My second browser notification",
+ "browser-two",
+ "CRITICAL"
+ );
+ let browserThree = await addNotification(
+ gNotificationBox,
+ "My third browser notification",
+ "browser-three",
+ "WARNING"
+ );
+
+ let tabTwo = await addNotification(
+ tabNotificationBox,
+ "My second tab notification",
+ "tab-two",
+ "INFO"
+ );
+ let tabThree = await addNotification(
+ tabNotificationBox,
+ "My third tab notification",
+ "tab-three",
+ "WARNING"
+ );
+
+ Assert.deepEqual(
+ [browserThree, browserTwo, browserOne],
+ [...gNotificationBox.stack.children],
+ "Browser notifications prepended"
+ );
+ Assert.deepEqual(
+ [tabOne, tabTwo, tabThree],
+ [...tabNotificationBox.stack.children],
+ "Tab notifications appended"
+ );
+
+ gNotificationBox.removeAllNotifications(true);
+ tabNotificationBox.removeAllNotifications(true);
+});
diff --git a/browser/base/content/test/notificationbox/browser_notificationbar_telemetry.js b/browser/base/content/test/notificationbox/browser_notificationbar_telemetry.js
new file mode 100644
index 0000000000..7810d4022d
--- /dev/null
+++ b/browser/base/content/test/notificationbox/browser_notificationbar_telemetry.js
@@ -0,0 +1,219 @@
+const TELEMETRY_BASE = "notificationbar.";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+add_task(async function showNotification() {
+ Services.telemetry.clearScalars();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ ok(!gBrowser.readNotificationBox(), "no notificationbox created yet");
+
+ let box1 = gBrowser.getNotificationBox();
+
+ ok(gBrowser.readNotificationBox(), "notificationbox was created");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org/"
+ );
+
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<body>Hello</body>"
+ );
+ let box3 = gBrowser.getNotificationBox();
+
+ verifyTelemetry("initial", 0, 0, 0, 0, 0, 0);
+
+ let notif3 = box3.appendNotification("infobar-testtwo-value", {
+ label: "Message for tab 3",
+ priority: box3.PRIORITY_INFO_HIGH,
+ telemetry: TELEMETRY_BASE + "testtwo",
+ });
+
+ verifyTelemetry("first notification", 0, 0, 0, 0, 0, 1);
+
+ let notif1 = box1.appendNotification(
+ "infobar-testone-value",
+ {
+ label: "Message for tab 1",
+ priority: box1.PRIORITY_INFO_HIGH,
+ telemetry: TELEMETRY_BASE + "testone",
+ },
+ [
+ {
+ label: "Button1",
+ telemetry: "button1-pressed",
+ },
+ {
+ label: "Button2",
+ telemetry: "button2-pressed",
+ },
+ {
+ label: "Button3",
+ },
+ ]
+ );
+ verifyTelemetry("second notification", 0, 0, 0, 0, 0, 1);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ verifyTelemetry("switch to first tab", 1, 0, 0, 0, 0, 1);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ verifyTelemetry("switch to second tab", 1, 0, 0, 0, 0, 1);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab3);
+ verifyTelemetry("switch to third tab", 1, 0, 0, 0, 0, 1);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ verifyTelemetry("switch to first tab again", 1, 0, 0, 0, 0, 1);
+
+ notif1.buttonContainer.lastElementChild.click();
+ verifyTelemetry("press third button", 1, 1, 0, 0, 0, 1);
+
+ notif1.buttonContainer.lastElementChild.previousElementSibling.click();
+ verifyTelemetry("press second button", 1, 1, 0, 1, 0, 1);
+
+ notif1.buttonContainer.lastElementChild.previousElementSibling.previousElementSibling.click();
+ verifyTelemetry("press first button", 1, 1, 1, 1, 0, 1);
+
+ notif1.dismiss();
+ verifyTelemetry("dismiss notification for box 1", 1, 1, 1, 1, 1, 1);
+
+ notif3.dismiss();
+ verifyTelemetry("dismiss notification for box 3", 1, 1, 1, 1, 1, 1, 1);
+
+ let notif4 = box1.appendNotification(
+ "infobar-testtwo-value",
+ {
+ label: "Additional message for tab 1",
+ priority: box1.PRIORITY_INFO_HIGH,
+ telemetry: TELEMETRY_BASE + "testone",
+ telemetryFilter: ["shown"],
+ },
+ [
+ {
+ label: "Button1",
+ },
+ ]
+ );
+ verifyTelemetry("show first filtered notification", 2, 1, 1, 1, 1, 1, 1);
+
+ notif4.buttonContainer.lastElementChild.click();
+ notif4.dismiss();
+ verifyTelemetry("dismiss first filtered notification", 2, 1, 1, 1, 1, 1, 1);
+
+ let notif5 = box1.appendNotification(
+ "infobar-testtwo-value",
+ {
+ label: "Dimissed additional message for tab 1",
+ priority: box1.PRIORITY_INFO_HIGH,
+ telemetry: TELEMETRY_BASE + "testone",
+ telemetryFilter: ["dismissed"],
+ },
+ [
+ {
+ label: "Button1",
+ },
+ ]
+ );
+ verifyTelemetry("show second filtered notification", 2, 1, 1, 1, 1, 1, 1);
+
+ notif5.buttonContainer.lastElementChild.click();
+ notif5.dismiss();
+ verifyTelemetry("dismiss second filtered notification", 2, 1, 1, 1, 2, 1, 1);
+
+ let notif6 = box1.appendNotification(
+ "infobar-testtwo-value",
+ {
+ label: "Dimissed additional message for tab 1",
+ priority: box1.PRIORITY_INFO_HIGH,
+ telemetry: TELEMETRY_BASE + "testone",
+ telemetryFilter: ["button1-pressed", "dismissed"],
+ },
+ [
+ {
+ label: "Button1",
+ telemetry: "button1-pressed",
+ },
+ ]
+ );
+ verifyTelemetry("show third filtered notification", 2, 1, 1, 1, 2, 1, 1);
+
+ notif6.buttonContainer.lastElementChild.click();
+ verifyTelemetry(
+ "click button in third filtered notification",
+ 2,
+ 1,
+ 2,
+ 1,
+ 2,
+ 1,
+ 1
+ );
+ notif6.dismiss();
+ verifyTelemetry("dismiss third filtered notification", 2, 1, 2, 1, 3, 1, 1);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
+
+function verify(scalars, scalar, key, expected, exists) {
+ scalar = TELEMETRY_BASE + scalar;
+
+ if (expected > 0) {
+ TelemetryTestUtils.assertKeyedScalar(scalars, scalar, key, expected);
+ return;
+ }
+
+ Assert.equal(
+ scalar in scalars,
+ exists,
+ `expected ${scalar} to be ${exists ? "present" : "unset"}`
+ );
+
+ if (exists) {
+ Assert.ok(
+ !(key in scalars[scalar]),
+ "expected key " + key + " to be unset"
+ );
+ }
+}
+
+function verifyTelemetry(
+ desc,
+ box1shown,
+ box1action,
+ box1button1,
+ box1button2,
+ box1dismissed,
+ box3shown,
+ box3dismissed = 0
+) {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+
+ info(desc);
+ let n1exists =
+ box1shown || box1action || box1button1 || box1button2 || box1dismissed;
+
+ verify(scalars, "testone", "shown", box1shown, n1exists);
+ verify(scalars, "testone", "action", box1action, n1exists);
+ verify(scalars, "testone", "button1-pressed", box1button1, n1exists);
+ verify(scalars, "testone", "button2-pressed", box1button2, n1exists);
+ verify(scalars, "testone", "dismissed", box1dismissed, n1exists);
+ verify(scalars, "testtwo", "shown", box3shown, box3shown || box3dismissed);
+ verify(
+ scalars,
+ "testtwo",
+ "dismissed",
+ box3dismissed,
+ box3shown || box3dismissed
+ );
+}
diff --git a/browser/base/content/test/notificationbox/browser_tabnotificationbox_switch_tabs.js b/browser/base/content/test/notificationbox/browser_tabnotificationbox_switch_tabs.js
new file mode 100644
index 0000000000..f00916c773
--- /dev/null
+++ b/browser/base/content/test/notificationbox/browser_tabnotificationbox_switch_tabs.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function assertNotificationBoxHidden(reason, browser) {
+ let notificationBox = gBrowser.readNotificationBox(browser);
+
+ if (!notificationBox) {
+ ok(!notificationBox, `Notification box has not been created ${reason}`);
+ return;
+ }
+
+ let name = notificationBox._stack.getAttribute("name");
+ ok(name, `Notification box has a name ${reason}`);
+
+ let { selectedViewName } = notificationBox._stack.parentElement;
+ ok(
+ selectedViewName != name,
+ `Box is not shown ${reason} ${selectedViewName} != ${name}`
+ );
+}
+
+function assertNotificationBoxShown(reason, browser) {
+ let notificationBox = gBrowser.readNotificationBox(browser);
+ ok(notificationBox, `Notification box has been created ${reason}`);
+
+ let name = notificationBox._stack.getAttribute("name");
+ ok(name, `Notification box has a name ${reason}`);
+
+ let { selectedViewName } = notificationBox._stack.parentElement;
+ is(selectedViewName, name, `Box is shown ${reason}`);
+}
+
+function createNotification({ browser, label, value, priority }) {
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.appendNotification(value, {
+ label,
+ priority: notificationBox[priority],
+ });
+ return notification;
+}
+
+add_task(async function testNotificationInBackgroundTab() {
+ let firstTab = gBrowser.selectedTab;
+
+ // Navigating to a page should not create the notification box
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ let notificationBox = gBrowser.readNotificationBox(browser);
+ ok(!notificationBox, "The notification box has not been created");
+
+ gBrowser.selectedTab = firstTab;
+ assertNotificationBoxHidden("initial first tab");
+
+ createNotification({
+ browser,
+ label: "My notification body",
+ value: "test-notification",
+ priority: "PRIORITY_INFO_LOW",
+ });
+
+ gBrowser.selectedTab = gBrowser.getTabForBrowser(browser);
+ assertNotificationBoxShown("notification created");
+ });
+});
+
+add_task(async function testNotificationInActiveTab() {
+ // Open about:blank so the notification box isn't created on tab open.
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ ok(!gBrowser.readNotificationBox(browser), "No notifications for new tab");
+
+ createNotification({
+ browser,
+ label: "Notification!",
+ value: "test-notification",
+ priority: "PRIORITY_INFO_LOW",
+ });
+ assertNotificationBoxShown("after appendNotification");
+ });
+});
+
+add_task(async function testNotificationMultipleTabs() {
+ let tabOne = gBrowser.selectedTab;
+ let tabTwo = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:blank",
+ });
+ let tabThree = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "https://example.com",
+ });
+ let browserOne = tabOne.linkedBrowser;
+ let browserTwo = tabTwo.linkedBrowser;
+ let browserThree = tabThree.linkedBrowser;
+
+ is(gBrowser.selectedBrowser, browserThree, "example.com selected");
+
+ let notificationBoxOne = gBrowser.readNotificationBox(browserOne);
+ let notificationBoxTwo = gBrowser.readNotificationBox(browserTwo);
+ let notificationBoxThree = gBrowser.readNotificationBox(browserThree);
+
+ ok(!notificationBoxOne, "no initial tab box");
+ ok(!notificationBoxTwo, "no about:blank box");
+ ok(!notificationBoxThree, "no example.com box");
+
+ // Verify the correct box is shown after creating tabs.
+ assertNotificationBoxHidden("after open", browserOne);
+ assertNotificationBoxHidden("after open", browserTwo);
+ assertNotificationBoxHidden("after open", browserThree);
+
+ createNotification({
+ browser: browserTwo,
+ label: "Test blank",
+ value: "blank",
+ priority: "PRIORITY_INFO_LOW",
+ });
+ notificationBoxTwo = gBrowser.readNotificationBox(browserTwo);
+ ok(notificationBoxTwo, "Notification box was created");
+
+ // Verify the selected browser's notification box is still hidden.
+ assertNotificationBoxHidden("hidden create", browserTwo);
+ assertNotificationBoxHidden("other create", browserThree);
+
+ createNotification({
+ browser: browserThree,
+ label: "Test active tab",
+ value: "active",
+ priority: "PRIORITY_CRITICAL_LOW",
+ });
+ // Verify the selected browser's notification box is still shown.
+ assertNotificationBoxHidden("active create", browserTwo);
+ assertNotificationBoxShown("active create", browserThree);
+
+ gBrowser.selectedTab = tabTwo;
+
+ // Verify the notification box for the tab that has one gets shown.
+ assertNotificationBoxShown("tab switch", browserTwo);
+ assertNotificationBoxHidden("tab switch", browserThree);
+
+ BrowserTestUtils.removeTab(tabTwo);
+ BrowserTestUtils.removeTab(tabThree);
+});
diff --git a/browser/base/content/test/outOfProcess/browser.ini b/browser/base/content/test/outOfProcess/browser.ini
new file mode 100644
index 0000000000..de8ad0cb8b
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files =
+ file_base.html
+ file_frame1.html
+ file_frame2.html
+ file_innerframe.html
+ head.js
+
+[browser_basic_outofprocess.js]
+[browser_controller.js]
+skip-if =
+ os == "linux" && bits == 64 # Bug 1663506
+ os == "mac" && debug # Bug 1663506
+ os == "win" && bits == 64 # Bug 1663506
+[browser_promisefocus.js]
diff --git a/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js b/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js
new file mode 100644
index 0000000000..50914a286c
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js
@@ -0,0 +1,149 @@
+/**
+ * Verify that the colors were set properly. This has the effect of
+ * verifying that the processes are assigned for child frames correctly.
+ */
+async function verifyBaseFrameStructure(
+ browsingContexts,
+ testname,
+ expectedHTML
+) {
+ function checkColorAndText(bc, desc, expectedColor, expectedText) {
+ return SpecialPowers.spawn(
+ bc,
+ [expectedColor, expectedText, desc],
+ (expectedColorChild, expectedTextChild, descChild) => {
+ Assert.equal(
+ content.document.documentElement.style.backgroundColor,
+ expectedColorChild,
+ descChild + " color"
+ );
+ Assert.equal(
+ content.document.getElementById("insertPoint").innerHTML,
+ expectedTextChild,
+ descChild + " text"
+ );
+ }
+ );
+ }
+
+ let useOOPFrames = gFissionBrowser;
+
+ is(
+ browsingContexts.length,
+ TOTAL_FRAME_COUNT,
+ "correct number of browsing contexts"
+ );
+ await checkColorAndText(
+ browsingContexts[0],
+ testname + " base",
+ "white",
+ expectedHTML.next().value
+ );
+ await checkColorAndText(
+ browsingContexts[1],
+ testname + " frame 1",
+ useOOPFrames ? "seashell" : "white",
+ expectedHTML.next().value
+ );
+ await checkColorAndText(
+ browsingContexts[2],
+ testname + " frame 1-1",
+ useOOPFrames ? "seashell" : "white",
+ expectedHTML.next().value
+ );
+ await checkColorAndText(
+ browsingContexts[3],
+ testname + " frame 2",
+ useOOPFrames ? "lightcyan" : "white",
+ expectedHTML.next().value
+ );
+ await checkColorAndText(
+ browsingContexts[4],
+ testname + " frame 2-1",
+ useOOPFrames ? "seashell" : "white",
+ expectedHTML.next().value
+ );
+ await checkColorAndText(
+ browsingContexts[5],
+ testname + " frame 2-2",
+ useOOPFrames ? "lightcyan" : "white",
+ expectedHTML.next().value
+ );
+ await checkColorAndText(
+ browsingContexts[6],
+ testname + " frame 2-3",
+ useOOPFrames ? "palegreen" : "white",
+ expectedHTML.next().value
+ );
+ await checkColorAndText(
+ browsingContexts[7],
+ testname + " frame 2-4",
+ "white",
+ expectedHTML.next().value
+ );
+}
+
+/**
+ * Test setting up all of the frames where a string of markup is passed
+ * to initChildFrames.
+ */
+add_task(async function test_subframes_string() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ OOP_BASE_PAGE_URI
+ );
+
+ const markup = "<p>Text</p>";
+
+ let browser = tab.linkedBrowser;
+ let browsingContexts = await initChildFrames(browser, markup);
+
+ function* getExpectedHTML() {
+ for (let c = 1; c <= TOTAL_FRAME_COUNT; c++) {
+ yield markup;
+ }
+ ok(false, "Frame count does not match actual number of frames");
+ }
+ await verifyBaseFrameStructure(browsingContexts, "string", getExpectedHTML());
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Test setting up all of the frames where a function that returns different markup
+ * is passed to initChildFrames.
+ */
+add_task(async function test_subframes_function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ OOP_BASE_PAGE_URI
+ );
+ let browser = tab.linkedBrowser;
+
+ let counter = 0;
+ let browsingContexts = await initChildFrames(
+ browser,
+ function (browsingContext) {
+ return "<p>Text " + ++counter + "</p>";
+ }
+ );
+
+ is(
+ counter,
+ TOTAL_FRAME_COUNT,
+ "insert HTML function called the correct number of times"
+ );
+
+ function* getExpectedHTML() {
+ for (let c = 1; c <= TOTAL_FRAME_COUNT; c++) {
+ yield "<p>Text " + c + "</p>";
+ }
+ }
+ await verifyBaseFrameStructure(
+ browsingContexts,
+ "function",
+ getExpectedHTML()
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/outOfProcess/browser_controller.js b/browser/base/content/test/outOfProcess/browser_controller.js
new file mode 100644
index 0000000000..f9d9ca8c93
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser_controller.js
@@ -0,0 +1,127 @@
+function checkCommandState(testid, undoEnabled, copyEnabled, deleteEnabled) {
+ is(
+ !document.getElementById("cmd_undo").hasAttribute("disabled"),
+ undoEnabled,
+ testid + " undo"
+ );
+ is(
+ !document.getElementById("cmd_copy").hasAttribute("disabled"),
+ copyEnabled,
+ testid + " copy"
+ );
+ is(
+ !document.getElementById("cmd_delete").hasAttribute("disabled"),
+ deleteEnabled,
+ testid + " delete"
+ );
+}
+
+function keyAndUpdate(key, eventDetails, updateEventsCount) {
+ let updatePromise = BrowserTestUtils.waitForEvent(
+ window,
+ "commandupdate",
+ false,
+ () => {
+ return --updateEventsCount == 0;
+ }
+ );
+ EventUtils.synthesizeKey(key, eventDetails);
+ return updatePromise;
+}
+
+add_task(async function test_controllers_subframes() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ OOP_BASE_PAGE_URI
+ );
+ let browser = tab.linkedBrowser;
+ let browsingContexts = await initChildFrames(
+ browser,
+ "<input id='input'><br><br>"
+ );
+
+ gURLBar.focus();
+
+ for (let stepNum = 0; stepNum < browsingContexts.length; stepNum++) {
+ await keyAndUpdate(stepNum > 0 ? "VK_TAB" : "VK_F6", {}, 6);
+
+ // Since focus may be switching into a separate process here,
+ // need to wait for the focus to have been updated.
+ await SpecialPowers.spawn(browsingContexts[stepNum], [], () => {
+ return ContentTaskUtils.waitForCondition(
+ () => content.browsingContext.isActive && content.document.hasFocus()
+ );
+ });
+
+ // Force the UI to update on platforms that don't
+ // normally do so until menus are opened.
+ if (AppConstants.platform != "macosx") {
+ goUpdateGlobalEditMenuItems(true);
+ }
+
+ await SpecialPowers.spawn(browsingContexts[stepNum], [], () => {
+ // Both the tab key and document navigation with F6 will focus
+ // the root of the document within the frame.
+ let document = content.document;
+ Assert.equal(
+ document.activeElement,
+ document.documentElement,
+ "root focused"
+ );
+ });
+ // XXX Currently, Copy is always enabled when the root (not an editor element)
+ // is focused. Possibly that should only be true if a listener is present?
+ checkCommandState("step " + stepNum + " root focused", false, true, false);
+
+ // Tab to the textbox.
+ await keyAndUpdate("VK_TAB", {}, 1);
+
+ if (AppConstants.platform != "macosx") {
+ goUpdateGlobalEditMenuItems(true);
+ }
+
+ await SpecialPowers.spawn(browsingContexts[stepNum], [], () => {
+ Assert.equal(
+ content.document.activeElement,
+ content.document.getElementById("input"),
+ "input focused"
+ );
+ });
+ checkCommandState(
+ "step " + stepNum + " input focused",
+ false,
+ false,
+ false
+ );
+
+ // Type into the textbox.
+ await keyAndUpdate("a", {}, 1);
+ checkCommandState("step " + stepNum + " typed", true, false, false);
+
+ await SpecialPowers.spawn(browsingContexts[stepNum], [], () => {
+ Assert.equal(
+ content.document.activeElement,
+ content.document.getElementById("input"),
+ "input focused"
+ );
+ });
+
+ // Select all text; this causes the Copy and Delete commands to be enabled.
+ await keyAndUpdate("a", { accelKey: true }, 1);
+ if (AppConstants.platform != "macosx") {
+ goUpdateGlobalEditMenuItems(true);
+ }
+
+ checkCommandState("step " + stepNum + " selected", true, true, true);
+
+ // Now make sure that the text is selected.
+ await SpecialPowers.spawn(browsingContexts[stepNum], [], () => {
+ let input = content.document.getElementById("input");
+ Assert.equal(input.value, "a", "text matches");
+ Assert.equal(input.selectionStart, 0, "selectionStart matches");
+ Assert.equal(input.selectionEnd, 1, "selectionEnd matches");
+ });
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/outOfProcess/browser_promisefocus.js b/browser/base/content/test/outOfProcess/browser_promisefocus.js
new file mode 100644
index 0000000000..9018c0f0ae
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser_promisefocus.js
@@ -0,0 +1,262 @@
+// Opens another window and switches focus between them.
+add_task(async function test_window_focus() {
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+ ok(!document.hasFocus(), "hasFocus after open second window");
+ ok(window2.document.hasFocus(), "hasFocus after open second window");
+ is(
+ Services.focus.activeWindow,
+ window2,
+ "activeWindow after open second window"
+ );
+ is(
+ Services.focus.focusedWindow,
+ window2,
+ "focusedWindow after open second window"
+ );
+
+ await SimpleTest.promiseFocus(window);
+ ok(document.hasFocus(), "hasFocus after promiseFocus on window");
+ ok(!window2.document.hasFocus(), "hasFocus after promiseFocus on window");
+ is(
+ Services.focus.activeWindow,
+ window,
+ "activeWindow after promiseFocus on window"
+ );
+ is(
+ Services.focus.focusedWindow,
+ window,
+ "focusedWindow after promiseFocus on window"
+ );
+
+ await SimpleTest.promiseFocus(window2);
+ ok(!document.hasFocus(), "hasFocus after promiseFocus on second window");
+ ok(
+ window2.document.hasFocus(),
+ "hasFocus after promiseFocus on second window"
+ );
+ is(
+ Services.focus.activeWindow,
+ window2,
+ "activeWindow after promiseFocus on second window"
+ );
+ is(
+ Services.focus.focusedWindow,
+ window2,
+ "focusedWindow after promiseFocus on second window"
+ );
+
+ await BrowserTestUtils.closeWindow(window2);
+
+ // If the window is already focused, this should just return.
+ await SimpleTest.promiseFocus(window);
+ await SimpleTest.promiseFocus(window);
+});
+
+// Opens two tabs and ensures that focus can be switched to the browser.
+add_task(async function test_tab_focus() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<input>"
+ );
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<input>"
+ );
+
+ gURLBar.focus();
+
+ await SimpleTest.promiseFocus(tab2.linkedBrowser);
+ is(
+ document.activeElement,
+ tab2.linkedBrowser,
+ "Browser is focused after promiseFocus"
+ );
+
+ await SpecialPowers.spawn(tab1.linkedBrowser, [], () => {
+ Assert.equal(
+ Services.focus.activeBrowsingContext,
+ null,
+ "activeBrowsingContext in child process in hidden tab"
+ );
+ Assert.equal(
+ Services.focus.focusedWindow,
+ null,
+ "focusedWindow in child process in hidden tab"
+ );
+ Assert.ok(
+ !content.document.hasFocus(),
+ "hasFocus in child process in hidden tab"
+ );
+ });
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], () => {
+ Assert.equal(
+ Services.focus.activeBrowsingContext,
+ content.browsingContext,
+ "activeBrowsingContext in child process in visible tab"
+ );
+ Assert.equal(
+ Services.focus.focusedWindow,
+ content.window,
+ "focusedWindow in child process in visible tab"
+ );
+ Assert.ok(
+ content.document.hasFocus(),
+ "hasFocus in child process in visible tab"
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Opens a document with a nested hierarchy of frames using initChildFrames and
+// focuses each child iframe in turn.
+add_task(async function test_subframes_focus() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ OOP_BASE_PAGE_URI
+ );
+
+ const markup = "<input>";
+
+ let browser = tab.linkedBrowser;
+ let browsingContexts = await initChildFrames(browser, markup);
+
+ for (let blurSubframe of [true, false]) {
+ for (let index = browsingContexts.length - 1; index >= 0; index--) {
+ let bc = browsingContexts[index];
+
+ // Focus each browsing context in turn. Do this twice, once when the window
+ // is not already focused, and once when it is already focused.
+ for (let step = 0; step < 2; step++) {
+ let desc =
+ "within child frame " +
+ index +
+ " step " +
+ step +
+ " blur subframe " +
+ blurSubframe +
+ " ";
+
+ info(desc + "start");
+ await SimpleTest.promiseFocus(bc, false, blurSubframe);
+
+ let expectedFocusedBC = bc;
+ // Becuase we are iterating backwards through the iframes, when we get to a frame
+ // that contains the iframe we just tested, focusing it will keep the child
+ // iframe focused as well, so we need to account for this when verifying which
+ // child iframe is focused. For the root frame (index 0), the iframe nested
+ // two items down will actually be focused.
+ // If blurSubframe is true however, the iframe focus in the parent will be cleared,
+ // so the focused window should be the parent instead.
+ if (!blurSubframe) {
+ if (index == 0) {
+ expectedFocusedBC = browsingContexts[index + 2];
+ } else if (index == 3 || index == 1) {
+ expectedFocusedBC = browsingContexts[index + 1];
+ }
+ }
+ is(
+ Services.focus.focusedContentBrowsingContext,
+ expectedFocusedBC,
+ desc +
+ " focusedContentBrowsingContext" +
+ ":: " +
+ Services.focus.focusedContentBrowsingContext?.id +
+ "," +
+ expectedFocusedBC?.id
+ );
+
+ // If the processes don't match, then the child iframe is an out-of-process iframe.
+ let oop =
+ expectedFocusedBC.currentWindowGlobal.osPid !=
+ bc.currentWindowGlobal.osPid;
+ await SpecialPowers.spawn(
+ bc,
+ [
+ index,
+ desc,
+ expectedFocusedBC != bc ? expectedFocusedBC : null,
+ oop,
+ ],
+ (num, descChild, childBC, isOop) => {
+ Assert.equal(
+ Services.focus.activeBrowsingContext,
+ content.browsingContext.top,
+ descChild + "activeBrowsingContext"
+ );
+ Assert.ok(
+ content.document.hasFocus(),
+ descChild + "hasFocus: " + content.browsingContext.id
+ );
+
+ // If a child browsing context is expected to be focused, the focusedWindow
+ // should be set to that instead and the active element should be an iframe.
+ // Otherwise, the focused window should be this window, and the active
+ // element should be the document's body element.
+ if (childBC) {
+ // The frame structure is:
+ // A1
+ // -> B
+ // -> A2
+ // where A and B are two processes. The frame A2 starts out focused. When B is
+ // focused, A1's focus is updated correctly.
+
+ // In Fission mode, childBC.window returns a non-null proxy even if OOP
+ if (isOop) {
+ Assert.equal(
+ Services.focus.focusedWindow,
+ null,
+ descChild + "focusedWindow"
+ );
+ Assert.ok(!childBC.docShell, descChild + "childBC.docShell");
+ } else {
+ Assert.equal(
+ Services.focus.focusedWindow,
+ childBC.window,
+ descChild + "focusedWindow"
+ );
+ }
+ Assert.equal(
+ content.document.activeElement.localName,
+ "iframe",
+ descChild + "activeElement"
+ );
+ } else {
+ Assert.equal(
+ Services.focus.focusedWindow,
+ content.window,
+ descChild + "focusedWindow"
+ );
+ Assert.equal(
+ content.document.activeElement,
+ content.document.body,
+ descChild + "activeElement"
+ );
+ }
+ }
+ );
+ }
+ }
+ }
+
+ // Focus the top window without blurring the browser.
+ await SimpleTest.promiseFocus(window, false, false);
+ is(
+ document.activeElement.localName,
+ "browser",
+ "focus after blurring browser blur subframe false"
+ );
+
+ // Now, focus the top window, blurring the browser.
+ await SimpleTest.promiseFocus(window, false, true);
+ is(
+ document.activeElement,
+ document.body,
+ "focus after blurring browser blur subframe true"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/outOfProcess/file_base.html b/browser/base/content/test/outOfProcess/file_base.html
new file mode 100644
index 0000000000..03f0731a8e
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/file_base.html
@@ -0,0 +1,5 @@
+<html><body>
+<div id="insertPoint"></div>
+<iframe src="https://www.mozilla.org:443/browser/browser/base/content/test/outOfProcess/file_frame1.html" width="320" height="700" style="border: 1px solid black;"></iframe></body>
+<iframe src="https://test1.example.org:443/browser/browser/base/content/test/outOfProcess/file_frame2.html" width="320" height="700" style="border: 1px solid black;"></iframe></body>
+</html>
diff --git a/browser/base/content/test/outOfProcess/file_frame1.html b/browser/base/content/test/outOfProcess/file_frame1.html
new file mode 100644
index 0000000000..d39e970c0f
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/file_frame1.html
@@ -0,0 +1,5 @@
+<html><body>
+<div id="insertPoint"></div>
+Same domain:<br>
+<iframe src="file_innerframe.html" width="300" height="100" style="border: 1px solid black;"></iframe></body>
+</html>
diff --git a/browser/base/content/test/outOfProcess/file_frame2.html b/browser/base/content/test/outOfProcess/file_frame2.html
new file mode 100644
index 0000000000..f0bc91ba20
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/file_frame2.html
@@ -0,0 +1,11 @@
+<html><body>
+<div id="insertPoint"></div>
+Same domain as to the left:<br>
+<iframe src="https://www.mozilla.org:443/browser/browser/base/content/test/outOfProcess/file_innerframe.html" width="300" height="100" style="border: 1px solid black;"></iframe></body>
+Same domain as parent:<br>
+<iframe src="https://test1.example.org:443/browser/browser/base/content/test/outOfProcess/file_innerframe.html" width="300" height="100" style="border: 1px solid black;"></iframe></body>
+Different domain:<br>
+<iframe src="https://w3c-test.org:443/browser/browser/base/content/test/outOfProcess/file_innerframe.html" width="300" height="100" style="border: 1px solid black;"></iframe></body>
+Same as top-level domain:<br>
+<iframe src="https://example.com/browser/browser/base/content/test/outOfProcess/file_innerframe.html" width="300" height="100" style="border: 1px solid black;"></iframe></body>
+</html>
diff --git a/browser/base/content/test/outOfProcess/file_innerframe.html b/browser/base/content/test/outOfProcess/file_innerframe.html
new file mode 100644
index 0000000000..23c516232c
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/file_innerframe.html
@@ -0,0 +1,3 @@
+<html><body>
+<div id="insertPoint"></div>
+</html>
diff --git a/browser/base/content/test/outOfProcess/head.js b/browser/base/content/test/outOfProcess/head.js
new file mode 100644
index 0000000000..230e2e2cbc
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/head.js
@@ -0,0 +1,85 @@
+const OOP_BASE_PAGE_URI =
+ "https://example.com/browser/browser/base/content/test/outOfProcess/file_base.html";
+
+// The number of frames and subframes that exist for the basic OOP test. If frames are
+// modified within file_base.html, update this value.
+const TOTAL_FRAME_COUNT = 8;
+
+// The frames are assigned different colors based on their process ids. If you add a
+// frame you might need to add more colors to this list.
+const FRAME_COLORS = ["white", "seashell", "lightcyan", "palegreen"];
+
+/**
+ * Set up a set of child frames for the given browser for testing
+ * out of process frames. 'OOP_BASE_PAGE_URI' is the base page and subframes
+ * contain pages from the same or other domains.
+ *
+ * @param browser browser containing frame hierarchy to set up
+ * @param insertHTML HTML or function that returns what to insert into each frame
+ * @returns array of all browsing contexts in depth-first order
+ *
+ * This function adds a browsing context and process id label to each
+ * child subframe. It also sets the background color of each frame to
+ * different colors based on the process id. The browser_basic_outofprocess.js
+ * test verifies these colors to ensure that the frame/process hierarchy
+ * has been set up as expected. Colors are used to help people visualize
+ * the process setup.
+ *
+ * The insertHTML argument may be either a fixed string of HTML to insert
+ * into each subframe, or a function that returns the string to insert. The
+ * function takes one argument, the browsing context being processed.
+ */
+async function initChildFrames(browser, insertHTML) {
+ let colors = FRAME_COLORS.slice();
+ let colorMap = new Map();
+
+ let browsingContexts = [];
+
+ async function processBC(bc) {
+ browsingContexts.push(bc);
+
+ let pid = bc.currentWindowGlobal.osPid;
+ let ident = "BrowsingContext: " + bc.id + "\nProcess: " + pid;
+
+ let color = colorMap.get(pid);
+ if (!color) {
+ if (!colors.length) {
+ ok(false, "ran out of available colors");
+ }
+
+ color = colors.shift();
+ colorMap.set(pid, color);
+ }
+
+ let insertHTMLString = insertHTML;
+ if (typeof insertHTML == "function") {
+ insertHTMLString = insertHTML(bc);
+ }
+
+ await SpecialPowers.spawn(
+ bc,
+ [ident, color, insertHTMLString],
+ (identChild, colorChild, insertHTMLChild) => {
+ let root = content.document.documentElement;
+ root.style = "background-color: " + colorChild;
+
+ let pre = content.document.createElement("pre");
+ pre.textContent = identChild;
+ root.insertBefore(pre, root.firstChild);
+
+ if (insertHTMLChild) {
+ // eslint-disable-next-line no-unsanitized/property
+ content.document.getElementById("insertPoint").innerHTML =
+ insertHTMLChild;
+ }
+ }
+ );
+
+ for (let childBC of bc.children) {
+ await processBC(childBC);
+ }
+ }
+ await processBC(browser.browsingContext);
+
+ return browsingContexts;
+}
diff --git a/browser/base/content/test/pageActions/browser.ini b/browser/base/content/test/pageActions/browser.ini
new file mode 100644
index 0000000000..b19464fc48
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_PageActions_bookmark.js]
+[browser_PageActions_overflow.js]
+[browser_PageActions_removeExtension.js]
diff --git a/browser/base/content/test/pageActions/browser_PageActions_bookmark.js b/browser/base/content/test/pageActions/browser_PageActions_bookmark.js
new file mode 100644
index 0000000000..a77095f2cd
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_PageActions_bookmark.js
@@ -0,0 +1,130 @@
+"use strict";
+
+add_task(async function starButtonCtrlClick() {
+ // Open a unique page.
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let url = "http://example.com/browser_page_action_star_button";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ StarUI._createPanelIfNeeded();
+ // The button ignores activation while the bookmarked status is being
+ // updated. So, wait for it to finish updating.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING
+ );
+
+ const popup = document.getElementById("editBookmarkPanel");
+ const starButtonBox = document.getElementById("star-button-box");
+
+ let shownPromise = promisePanelShown(popup);
+ EventUtils.synthesizeMouseAtCenter(starButtonBox, { ctrlKey: true });
+ await shownPromise;
+ ok(true, "Panel shown after button pressed");
+
+ let hiddenPromise = promisePanelHidden(popup);
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function bookmark() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const url = "http://example.com/browser_page_action_menu";
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url },
+ async () => {
+ // The bookmark button should not be starred.
+ const bookmarkButton =
+ win.BrowserPageActions.urlbarButtonNodeForActionID("bookmark");
+ Assert.ok(!bookmarkButton.hasAttribute("starred"));
+
+ info("Click the button.");
+ // The button ignores activation while the bookmarked status is being
+ // updated. So, wait for it to finish updating.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING
+ );
+ const starUIPanel = win.StarUI.panel;
+ let panelShown = BrowserTestUtils.waitForPopupEvent(starUIPanel, "shown");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {}, win);
+ await panelShown;
+ is(
+ await PlacesUtils.bookmarks.fetch({ url }),
+ null,
+ "Bookmark has not been created before save."
+ );
+
+ // The bookmark button should now be starred.
+ Assert.equal(bookmarkButton.firstChild.getAttribute("starred"), "true");
+
+ info("Save the bookmark.");
+ const onItemAddedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(event => event.url == url)
+ );
+ starUIPanel.hidePopup();
+ await onItemAddedPromise;
+
+ info("Click it again.");
+ // The button ignores activation while the bookmarked status is being
+ // updated. So, wait for it to finish updating.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING
+ );
+ panelShown = BrowserTestUtils.waitForPopupEvent(starUIPanel, "shown");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {}, win);
+ await panelShown;
+
+ info("Remove the bookmark.");
+ const onItemRemovedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => event.url == url)
+ );
+ win.StarUI._element("editBookmarkPanelRemoveButton").click();
+ await onItemRemovedPromise;
+ }
+ );
+});
+
+add_task(async function bookmarkNoEditDialog() {
+ const url =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser_page_action_menu_no_edit_dialog";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.bookmarks.editDialog.showForNewBookmarks", false]],
+ });
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url },
+ async () => {
+ info("Click the button.");
+ // The button ignores activation while the bookmarked status is being
+ // updated. So, wait for it to finish updating.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING
+ );
+ const bookmarkButton = win.document.getElementById(
+ BrowserPageActions.urlbarButtonNodeIDForActionID("bookmark")
+ );
+
+ // The bookmark should be saved immediately after clicking the star.
+ const onItemAddedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(event => event.url == url)
+ );
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {}, win);
+ await onItemAddedPromise;
+ }
+ );
+});
diff --git a/browser/base/content/test/pageActions/browser_PageActions_overflow.js b/browser/base/content/test/pageActions/browser_PageActions_overflow.js
new file mode 100644
index 0000000000..463dd336c4
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_PageActions_overflow.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test() {
+ // We use an extension that shows a page action. We must add this additional
+ // action because otherwise the meatball menu would not appear as an overflow
+ // for a single action.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(win);
+ Assert.greater(win.outerWidth, 700, "window is bigger than 700px");
+ BrowserTestUtils.loadURIString(
+ win.gBrowser,
+ "data:text/html,<h1>A Page</h1>"
+ );
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame(win);
+
+ info("Check page action buttons are visible, the meatball button is not");
+ let addonButton =
+ win.BrowserPageActions.urlbarButtonNodeForActionID(actionId);
+ Assert.ok(BrowserTestUtils.is_visible(addonButton));
+ let starButton =
+ win.BrowserPageActions.urlbarButtonNodeForActionID("bookmark");
+ Assert.ok(BrowserTestUtils.is_visible(starButton));
+ let meatballButton = win.document.getElementById("pageActionButton");
+ Assert.ok(!BrowserTestUtils.is_visible(meatballButton));
+
+ info(
+ "Shrink the window, check page action buttons are not visible, the meatball menu is visible"
+ );
+ let originalOuterWidth = win.outerWidth;
+ await promiseStableResize(500, win);
+ Assert.ok(!BrowserTestUtils.is_visible(addonButton));
+ Assert.ok(!BrowserTestUtils.is_visible(starButton));
+ Assert.ok(BrowserTestUtils.is_visible(meatballButton));
+
+ info(
+ "Remove the extension, check the only page action button is visible, the meatball menu is not visible"
+ );
+ let promiseUninstalled = promiseAddonUninstalled(extension.id);
+ await extension.unload();
+ await promiseUninstalled;
+ Assert.ok(BrowserTestUtils.is_visible(starButton));
+ Assert.ok(!BrowserTestUtils.is_visible(meatballButton));
+ Assert.deepEqual(
+ win.BrowserPageActions.urlbarButtonNodeForActionID(actionId),
+ null
+ );
+
+ await promiseStableResize(originalOuterWidth, win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function bookmark() {
+ // We use an extension that shows a page action. We must add this additional
+ // action because otherwise the meatball menu would not appear as an overflow
+ // for a single action.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ const url = "data:text/html,<h1>A Page</h1>";
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(win);
+ BrowserTestUtils.loadURIString(win.gBrowser, url);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame(win);
+
+ info("Shrink the window if necessary, check the meatball menu is visible");
+ let originalOuterWidth = win.outerWidth;
+ await promiseStableResize(500, win);
+
+ let meatballButton = win.document.getElementById("pageActionButton");
+ Assert.ok(BrowserTestUtils.is_visible(meatballButton));
+
+ // Open the panel.
+ await promisePageActionPanelOpen(win);
+
+ // The bookmark button should read "Bookmark Current Tab…" and not be starred.
+ let bookmarkButton = win.document.getElementById("pageAction-panel-bookmark");
+ await TestUtils.waitForCondition(
+ () => bookmarkButton.label === "Bookmark Current Tab…"
+ );
+ Assert.ok(!bookmarkButton.hasAttribute("starred"));
+
+ // Click the button.
+ let hiddenPromise = promisePageActionPanelHidden(win);
+ let showPromise = BrowserTestUtils.waitForPopupEvent(
+ win.StarUI.panel,
+ "shown"
+ );
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {}, win);
+ await hiddenPromise;
+ await showPromise;
+ win.StarUI.panel.hidePopup();
+
+ // Open the panel again.
+ await promisePageActionPanelOpen(win);
+
+ // The bookmark button should now read "Edit This Bookmark…" and be starred.
+ await TestUtils.waitForCondition(
+ () => bookmarkButton.label === "Edit This Bookmark…"
+ );
+ Assert.ok(bookmarkButton.hasAttribute("starred"));
+ Assert.equal(bookmarkButton.getAttribute("starred"), "true");
+
+ // Click it again.
+ hiddenPromise = promisePageActionPanelHidden(win);
+ showPromise = BrowserTestUtils.waitForPopupEvent(win.StarUI.panel, "shown");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {}, win);
+ await hiddenPromise;
+ await showPromise;
+
+ let onItemRemovedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => event.url == url)
+ );
+
+ // Click the remove-bookmark button in the panel.
+ win.StarUI._element("editBookmarkPanelRemoveButton").click();
+
+ // Wait for the bookmark to be removed before continuing.
+ await onItemRemovedPromise;
+
+ // Open the panel again.
+ await promisePageActionPanelOpen(win);
+
+ // The bookmark button should read "Bookmark Current Tab…" and not be starred.
+ await TestUtils.waitForCondition(
+ () => bookmarkButton.label === "Bookmark Current Tab…"
+ );
+ Assert.ok(!bookmarkButton.hasAttribute("starred"));
+
+ // Done.
+ hiddenPromise = promisePageActionPanelHidden();
+ win.BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ info("Remove the extension");
+ let promiseUninstalled = promiseAddonUninstalled(extension.id);
+ await extension.unload();
+ await promiseUninstalled;
+
+ await promiseStableResize(originalOuterWidth, win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_disabledPageAction_hidden_in_protonOverflowMenu() {
+ // Make sure the overflow menu urlbar button is visible (indipendently from
+ // the current size of the Firefox window).
+ BrowserPageActions.mainButtonNode.style.visibility = "visible";
+ registerCleanupFunction(() => {
+ BrowserPageActions.mainButtonNode.style.removeProperty("visibility");
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: { page_action: {} },
+ async background() {
+ const { browser } = this;
+ const [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ browser.test.assertTrue(tab, "Got an active tab as expected");
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "show-pageAction":
+ await browser.pageAction.show(tab.id);
+ break;
+ case "hide-pageAction":
+ await browser.pageAction.hide(tab.id);
+ break;
+ default:
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ }
+ browser.test.sendMessage(`${msg}:done`);
+ });
+ },
+ });
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ const win = browser.ownerGlobal;
+ const promisePageActionPanelClosed = async () => {
+ let popupHiddenPromise = promisePageActionPanelHidden(win);
+ win.BrowserPageActions.panelNode.hidePopup();
+ await popupHiddenPromise;
+ };
+
+ await extension.startup();
+ const widgetId = ExtensionCommon.makeWidgetId(extension.id);
+
+ info(
+ "Show pageAction and verify it is visible in the urlbar overflow menu"
+ );
+ extension.sendMessage("show-pageAction");
+ await extension.awaitMessage("show-pageAction:done");
+ await promisePageActionPanelOpen(win);
+ let pageActionNode =
+ win.BrowserPageActions.panelButtonNodeForActionID(widgetId);
+ ok(
+ pageActionNode && BrowserTestUtils.is_visible(pageActionNode),
+ "enabled pageAction should be visible in the urlbar overflow menu"
+ );
+
+ info("Hide pageAction and verify it is hidden in the urlbar overflow menu");
+ extension.sendMessage("hide-pageAction");
+ await extension.awaitMessage("hide-pageAction:done");
+
+ await BrowserTestUtils.waitForCondition(
+ () => !win.BrowserPageActions.panelButtonNodeForActionID(widgetId),
+ "Wait for the disabled pageAction to be removed from the urlbar overflow menu"
+ );
+
+ await promisePageActionPanelClosed();
+
+ info("Reopen the urlbar overflow menu");
+ await promisePageActionPanelOpen(win);
+ ok(
+ !win.BrowserPageActions.panelButtonNodeForActionID(widgetId),
+ "Disabled pageAction is still removed as expected"
+ );
+
+ await promisePageActionPanelClosed();
+ await extension.unload();
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/pageActions/browser_PageActions_removeExtension.js b/browser/base/content/test/pageActions/browser_PageActions_removeExtension.js
new file mode 100644
index 0000000000..329be2db17
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_PageActions_removeExtension.js
@@ -0,0 +1,338 @@
+"use strict";
+
+// Initialization. Must run first.
+add_setup(async function () {
+ // The page action urlbar button, and therefore the panel, is only shown when
+ // the current tab is actionable -- i.e., a normal web page. about:blank is
+ // not, so open a new tab first thing, and close it when this test is done.
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com/",
+ });
+
+ // The prompt service is mocked later, so set it up to be restored.
+ let { prompt } = Services;
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ Services.prompt = prompt;
+ });
+});
+
+add_task(async function contextMenu_removeExtension_panel() {
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ const url = "data:text/html,<h1>A Page</h1>";
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(win);
+ BrowserTestUtils.loadURIString(win.gBrowser, url);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+
+ info("Shrink the window if necessary, check the meatball menu is visible");
+ let originalOuterWidth = win.outerWidth;
+ await promiseStableResize(500, win);
+
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame(win);
+
+ let meatballButton = win.document.getElementById("pageActionButton");
+ Assert.ok(BrowserTestUtils.is_visible(meatballButton));
+
+ // Open the panel.
+ await promisePageActionPanelOpen(win);
+
+ info("Open the context menu");
+ let panelButton = win.BrowserPageActions.panelButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu", win);
+ EventUtils.synthesizeMouseAtCenter(
+ panelButton,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ let contextMenu = await contextMenuPromise;
+
+ let removeExtensionItem = getRemoveExtensionItem(win);
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(!removeExtensionItem.disabled, "'Remove' item is not disabled");
+
+ // Click the "remove extension" item, a prompt should be displayed and then
+ // the add-on should be uninstalled. We mock the prompt service to confirm
+ // the removal of the add-on.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu", win);
+ let addonUninstalledPromise = promiseAddonUninstalled(extension.id);
+ mockPromptService();
+ contextMenu.activateItem(removeExtensionItem);
+ await Promise.all([contextMenuPromise, addonUninstalledPromise]);
+
+ // Done, clean up.
+ await extension.unload();
+
+ await promiseStableResize(originalOuterWidth, win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function contextMenu_removeExtension_urlbar() {
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame();
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ // Open the context menu on the action's urlbar button.
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(urlbarButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ let contextMenu = await contextMenuPromise;
+
+ let menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 2, "Context menu has two children");
+ let removeExtensionItem = getRemoveExtensionItem();
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(!removeExtensionItem.disabled, "'Remove' item is not disabled");
+ let manageExtensionItem = getManageExtensionItem();
+ Assert.ok(manageExtensionItem, "'Manage' item exists");
+ Assert.ok(!manageExtensionItem.hidden, "'Manage' item is visible");
+ Assert.ok(!manageExtensionItem.disabled, "'Manage' item is not disabled");
+
+ // Click the "remove extension" item, a prompt should be displayed and then
+ // the add-on should be uninstalled. We mock the prompt service to cancel the
+ // removal of the add-on.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ let promptService = mockPromptService();
+ let promptCancelledPromise = new Promise(resolve => {
+ promptService.confirmEx = () => resolve();
+ });
+ contextMenu.activateItem(removeExtensionItem);
+ await Promise.all([contextMenuPromise, promptCancelledPromise]);
+
+ // Done, clean up.
+ await extension.unload();
+
+ // urlbar tests that run after this one can break if the mouse is left over
+ // the area where the urlbar popup appears, which seems to happen due to the
+ // above synthesized mouse events. Move it over the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+add_task(async function contextMenu_removeExtension_disabled_in_urlbar() {
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame();
+ // Add a policy to prevent the add-on from being uninstalled.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Extensions: {
+ Locked: [extension.id],
+ },
+ },
+ });
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ // Open the context menu on the action's urlbar button.
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(urlbarButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ let contextMenu = await contextMenuPromise;
+
+ let menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 2, "Context menu has two children");
+ let removeExtensionItem = getRemoveExtensionItem();
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(removeExtensionItem.disabled, "'Remove' item is disabled");
+ let manageExtensionItem = getManageExtensionItem();
+ Assert.ok(manageExtensionItem, "'Manage' item exists");
+ Assert.ok(!manageExtensionItem.hidden, "'Manage' item is visible");
+ Assert.ok(!manageExtensionItem.disabled, "'Manage' item is not disabled");
+
+ // Hide the context menu.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ contextMenu.hidePopup();
+ await contextMenuPromise;
+
+ // Done, clean up.
+ await extension.unload();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+
+ // urlbar tests that run after this one can break if the mouse is left over
+ // the area where the urlbar popup appears, which seems to happen due to the
+ // above synthesized mouse events. Move it over the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+add_task(async function contextMenu_removeExtension_disabled_in_panel() {
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ // Add a policy to prevent the add-on from being uninstalled.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Extensions: {
+ Locked: [extension.id],
+ },
+ },
+ });
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ const url = "data:text/html,<h1>A Page</h1>";
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(win);
+ BrowserTestUtils.loadURIString(win.gBrowser, url);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+
+ info("Shrink the window if necessary, check the meatball menu is visible");
+ let originalOuterWidth = win.outerWidth;
+ await promiseStableResize(500, win);
+
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame(win);
+
+ let meatballButton = win.document.getElementById("pageActionButton");
+ Assert.ok(BrowserTestUtils.is_visible(meatballButton));
+
+ // Open the panel.
+ await promisePageActionPanelOpen(win);
+
+ info("Open the context menu");
+ let panelButton = win.BrowserPageActions.panelButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu", win);
+ EventUtils.synthesizeMouseAtCenter(
+ panelButton,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ let contextMenu = await contextMenuPromise;
+
+ let removeExtensionItem = getRemoveExtensionItem(win);
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(removeExtensionItem.disabled, "'Remove' item is disabled");
+
+ // Hide the context menu.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu", win);
+ contextMenu.hidePopup();
+ await contextMenuPromise;
+
+ // Done, clean up.
+ await extension.unload();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+
+ await promiseStableResize(originalOuterWidth, win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+function promiseAddonUninstalled(addonId) {
+ return new Promise(resolve => {
+ let listener = {};
+ listener.onUninstalled = addon => {
+ if (addon.id == addonId) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+function mockPromptService() {
+ let promptService = {
+ // The prompt returns 1 for cancelled and 0 for accepted.
+ _response: 0,
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx: () => promptService._response,
+ };
+
+ Services.prompt = promptService;
+
+ return promptService;
+}
+
+function getRemoveExtensionItem(win = window) {
+ return win.document.querySelector(
+ "#pageActionContextMenu > menuitem[label='Remove Extension']"
+ );
+}
+
+function getManageExtensionItem(win = window) {
+ return win.document.querySelector(
+ "#pageActionContextMenu > menuitem[label='Manage Extension…']"
+ );
+}
+
+function collectContextMenuItems(win = window) {
+ let contextMenu = win.document.getElementById("pageActionContextMenu");
+ return Array.prototype.filter.call(contextMenu.children, node => {
+ return win.getComputedStyle(node).visibility == "visible";
+ });
+}
diff --git a/browser/base/content/test/pageActions/head.js b/browser/base/content/test/pageActions/head.js
new file mode 100644
index 0000000000..15801c490e
--- /dev/null
+++ b/browser/base/content/test/pageActions/head.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ EnterprisePolicyTesting:
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs",
+ ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+async function promisePageActionPanelOpen(win = window, eventDict = {}) {
+ await BrowserTestUtils.waitForCondition(() => {
+ // Wait for the main page action button to become visible. It's hidden for
+ // some URIs, so depending on when this is called, it may not yet be quite
+ // visible. It's up to the caller to make sure it will be visible.
+ info("Waiting for main page action button to have non-0 size");
+ let bounds = win.windowUtils.getBoundsWithoutFlushing(
+ win.BrowserPageActions.mainButtonNode
+ );
+ return bounds.width > 0 && bounds.height > 0;
+ });
+
+ // Wait for the panel to become open, by clicking the button if necessary.
+ info("Waiting for main page action panel to be open");
+ if (win.BrowserPageActions.panelNode.state == "open") {
+ return;
+ }
+ let shownPromise = promisePageActionPanelShown(win);
+ EventUtils.synthesizeMouseAtCenter(
+ win.BrowserPageActions.mainButtonNode,
+ eventDict,
+ win
+ );
+ await shownPromise;
+ info("Wait for items in the panel to become visible.");
+ await promisePageActionViewChildrenVisible(
+ win.BrowserPageActions.mainViewNode,
+ win
+ );
+}
+
+function promisePageActionPanelShown(win = window) {
+ return promisePanelShown(win.BrowserPageActions.panelNode, win);
+}
+
+function promisePageActionPanelHidden(win = window) {
+ return promisePanelHidden(win.BrowserPageActions.panelNode, win);
+}
+
+function promisePanelShown(panelIDOrNode, win = window) {
+ return promisePanelEvent(panelIDOrNode, "popupshown", win);
+}
+
+function promisePanelHidden(panelIDOrNode, win = window) {
+ return promisePanelEvent(panelIDOrNode, "popuphidden", win);
+}
+
+function promisePanelEvent(panelIDOrNode, eventType, win = window) {
+ return new Promise(resolve => {
+ let panel = panelIDOrNode;
+ if (typeof panel == "string") {
+ panel = win.document.getElementById(panelIDOrNode);
+ if (!panel) {
+ throw new Error(`Panel with ID "${panelIDOrNode}" does not exist.`);
+ }
+ }
+ if (
+ (eventType == "popupshown" && panel.state == "open") ||
+ (eventType == "popuphidden" && panel.state == "closed")
+ ) {
+ executeSoon(() => resolve(panel));
+ return;
+ }
+ panel.addEventListener(
+ eventType,
+ () => {
+ executeSoon(() => resolve(panel));
+ },
+ { once: true }
+ );
+ });
+}
+
+async function promisePageActionViewChildrenVisible(
+ panelViewNode,
+ win = window
+) {
+ info(
+ "promisePageActionViewChildrenVisible waiting for a child node to be visible"
+ );
+ await new Promise(win.requestAnimationFrame);
+ let dwu = win.windowUtils;
+ return TestUtils.waitForCondition(() => {
+ let bodyNode = panelViewNode.firstElementChild;
+ for (let childNode of bodyNode.children) {
+ let bounds = dwu.getBoundsWithoutFlushing(childNode);
+ if (bounds.width > 0 && bounds.height > 0) {
+ return true;
+ }
+ }
+ return false;
+ });
+}
+
+function promiseAddonUninstalled(addonId) {
+ return new Promise(resolve => {
+ let listener = {};
+ listener.onUninstalled = addon => {
+ if (addon.id == addonId) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+async function promiseAnimationFrame(win = window) {
+ await new Promise(resolve => win.requestAnimationFrame(resolve));
+ await win.promiseDocumentFlushed(() => {});
+}
+
+async function promisePopupNotShown(id, win = window) {
+ let deferred = PromiseUtils.defer();
+ function listener(e) {
+ deferred.reject("Unexpected popupshown");
+ }
+ let panel = win.document.getElementById(id);
+ panel.addEventListener("popupshown", listener);
+ try {
+ await Promise.race([
+ deferred.promise,
+ new Promise(resolve => {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ win.setTimeout(resolve, 300);
+ }),
+ ]);
+ } finally {
+ panel.removeEventListener("popupshown", listener);
+ }
+}
+
+// TODO (Bug 1700780): Why is this necessary? Without this trick the test
+// fails intermittently on Ubuntu.
+function promiseStableResize(expectedWidth, win = window) {
+ let deferred = PromiseUtils.defer();
+ let id;
+ function listener() {
+ win.clearTimeout(id);
+ info(`Got resize event: ${win.innerWidth} x ${win.innerHeight}`);
+ if (win.innerWidth <= expectedWidth) {
+ id = win.setTimeout(() => {
+ win.removeEventListener("resize", listener);
+ deferred.resolve();
+ }, 100);
+ }
+ }
+ win.addEventListener("resize", listener);
+ win.resizeTo(expectedWidth, win.outerHeight);
+ return deferred.promise;
+}
diff --git a/browser/base/content/test/pageStyle/browser.ini b/browser/base/content/test/pageStyle/browser.ini
new file mode 100644
index 0000000000..4123fd7ec2
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files =
+ head.js
+ page_style_sample.html
+ style.css
+
+[browser_disable_author_style_oop.js]
+https_first_disabled = true
+support-files =
+ page_style.html
+
+[browser_page_style_menu.js]
+support-files =
+ page_style_only_alternates.html
+[browser_page_style_menu_update.js]
+
diff --git a/browser/base/content/test/pageStyle/browser_disable_author_style_oop.js b/browser/base/content/test/pageStyle/browser_disable_author_style_oop.js
new file mode 100644
index 0000000000..f89e08a220
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser_disable_author_style_oop.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function getColor(aSpawnTarget) {
+ return SpecialPowers.spawn(aSpawnTarget, [], function () {
+ return content.document.defaultView.getComputedStyle(
+ content.document.querySelector("p")
+ ).color;
+ });
+}
+
+async function insertIFrame() {
+ let bc = gBrowser.selectedBrowser.browsingContext;
+ let len = bc.children.length;
+
+ const kURL =
+ WEB_ROOT.replace("example.com", "example.net") + "page_style.html";
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [kURL], function (url) {
+ return new Promise(function (resolve) {
+ let e = content.document.createElement("iframe");
+ e.src = url;
+ e.onload = () => resolve();
+ content.document.body.append(e);
+ });
+ });
+
+ // Wait for the new frame to get a pres shell and be styled.
+ await BrowserTestUtils.waitForCondition(async function () {
+ return (
+ bc.children.length == len + 1 && (await getColor(bc.children[len])) != ""
+ );
+ });
+}
+
+// Test that inserting an iframe with a URL that is loaded OOP with Fission
+// enabled correctly matches the tab's author style disabled state.
+add_task(async function test_disable_style() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ WEB_ROOT + "page_style.html",
+ /* waitForLoad = */ true
+ );
+
+ let bc = gBrowser.selectedBrowser.browsingContext;
+
+ await insertIFrame();
+
+ is(
+ await getColor(bc),
+ "rgb(0, 0, 255)",
+ "parent color before disabling style"
+ );
+ is(
+ await getColor(bc.children[0]),
+ "rgb(0, 0, 255)",
+ "first child color before disabling style"
+ );
+
+ gPageStyleMenu.disableStyle();
+
+ is(await getColor(bc), "rgb(0, 0, 0)", "parent color after disabling style");
+ is(
+ await getColor(bc.children[0]),
+ "rgb(0, 0, 0)",
+ "first child color after disabling style"
+ );
+
+ await insertIFrame();
+
+ is(
+ await getColor(bc.children[1]),
+ "rgb(0, 0, 0)",
+ "second child color after disabling style"
+ );
+
+ await BrowserTestUtils.reloadTab(tab, true);
+
+ // Check the menu:
+ let { menupopup } = document.getElementById("pageStyleMenu");
+ gPageStyleMenu.fillPopup(menupopup);
+ Assert.equal(
+ menupopup.querySelector("menuitem[checked='true']").dataset.l10nId,
+ "menu-view-page-style-no-style",
+ "No style menu should be checked."
+ );
+
+ // check the page content still has a disabled author style:
+ Assert.ok(
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.docShell.contentViewer.authorStyleDisabled
+ ),
+ "Author style should still be disabled."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/pageStyle/browser_page_style_menu.js b/browser/base/content/test/pageStyle/browser_page_style_menu.js
new file mode 100644
index 0000000000..2ae635f16b
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser_page_style_menu.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function fillPopupAndGetItems() {
+ let menupopup = document.getElementById("pageStyleMenu").menupopup;
+ gPageStyleMenu.fillPopup(menupopup);
+ return Array.from(menupopup.querySelectorAll("menuseparator ~ menuitem"));
+}
+
+function getRootColor() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.document.defaultView.getComputedStyle(
+ content.document.documentElement
+ ).color;
+ });
+}
+
+const RED = "rgb(255, 0, 0)";
+const LIME = "rgb(0, 255, 0)";
+const BLUE = "rgb(0, 0, 255)";
+
+const kStyleSheetsInPageStyleSample = 18;
+
+/*
+ * Test that the right stylesheets do (and others don't) show up
+ * in the page style menu.
+ */
+add_task(async function test_menu() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ let browser = tab.linkedBrowser;
+ BrowserTestUtils.loadURIString(browser, WEB_ROOT + "page_style_sample.html");
+ await promiseStylesheetsLoaded(browser, kStyleSheetsInPageStyleSample);
+
+ let menuitems = fillPopupAndGetItems();
+ let items = menuitems.map(el => ({
+ label: el.getAttribute("label"),
+ checked: el.getAttribute("checked") == "true",
+ }));
+
+ let validLinks = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [items],
+ function (contentItems) {
+ let contentValidLinks = 0;
+ for (let el of content.document.querySelectorAll("link, style")) {
+ var title = el.getAttribute("title");
+ var rel = el.getAttribute("rel");
+ var media = el.getAttribute("media");
+ var idstring =
+ el.nodeName +
+ " " +
+ (title ? title : "without title and") +
+ ' with rel="' +
+ rel +
+ '"' +
+ (media ? ' and media="' + media + '"' : "");
+
+ var item = contentItems.filter(aItem => aItem.label == title);
+ var found = item.length == 1;
+ var checked = found && item[0].checked;
+
+ switch (el.getAttribute("data-state")) {
+ case "0":
+ ok(!found, idstring + " should not show up in page style menu");
+ break;
+ case "1":
+ contentValidLinks++;
+ ok(found, idstring + " should show up in page style menu");
+ ok(!checked, idstring + " should not be selected");
+ break;
+ case "2":
+ contentValidLinks++;
+ ok(found, idstring + " should show up in page style menu");
+ ok(checked, idstring + " should be selected");
+ break;
+ default:
+ throw new Error(
+ "data-state attribute is missing or has invalid value"
+ );
+ }
+ }
+ return contentValidLinks;
+ }
+ );
+
+ ok(menuitems.length, "At least one item in the menu");
+ is(menuitems.length, validLinks, "all valid links found");
+
+ is(await getRootColor(), LIME, "Root should be lime (styles should apply)");
+
+ let disableStyles = document.getElementById("menu_pageStyleNoStyle");
+ let defaultStyles = document.getElementById("menu_pageStylePersistentOnly");
+ let otherStyles = menuitems[0].parentNode.querySelector("[label='28']");
+
+ // Assert that the menu works as expected.
+ disableStyles.click();
+
+ await TestUtils.waitForCondition(async function () {
+ let color = await getRootColor();
+ return color != LIME && color != BLUE;
+ }, "ensuring disabled styles work");
+
+ otherStyles.click();
+
+ await TestUtils.waitForCondition(async function () {
+ let color = await getRootColor();
+ return color == BLUE;
+ }, "ensuring alternate styles work. clicking on: " + otherStyles.label);
+
+ defaultStyles.click();
+
+ info("ensuring default styles work");
+ await TestUtils.waitForCondition(async function () {
+ let color = await getRootColor();
+ return color == LIME;
+ }, "ensuring default styles work");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_default_style_with_no_sheets() {
+ const PAGE = WEB_ROOT + "page_style_only_alternates.html";
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ waitForLoad: true,
+ },
+ async function (browser) {
+ await promiseStylesheetsLoaded(browser, 2);
+
+ let menuitems = fillPopupAndGetItems();
+ is(menuitems.length, 2, "Should've found two style sets");
+ is(
+ await getRootColor(),
+ BLUE,
+ "First found style should become the preferred one and apply"
+ );
+
+ // Reset the styles.
+ document.getElementById("menu_pageStylePersistentOnly").click();
+ await TestUtils.waitForCondition(async function () {
+ let color = await getRootColor();
+ return color != BLUE && color != RED;
+ });
+
+ ok(
+ true,
+ "Should reset the style properly even if there are no non-alternate stylesheets"
+ );
+ }
+ );
+});
+
+add_task(async function test_page_style_file() {
+ const FILE_PAGE = Services.io.newFileURI(
+ new FileUtils.File(getTestFilePath("page_style_sample.html"))
+ ).spec;
+ await BrowserTestUtils.withNewTab(FILE_PAGE, async function (browser) {
+ await promiseStylesheetsLoaded(browser, kStyleSheetsInPageStyleSample);
+ let menuitems = fillPopupAndGetItems();
+ is(
+ menuitems.length,
+ kStyleSheetsInPageStyleSample,
+ "Should have the right amount of items even for file: URI."
+ );
+ });
+});
diff --git a/browser/base/content/test/pageStyle/browser_page_style_menu_update.js b/browser/base/content/test/pageStyle/browser_page_style_menu_update.js
new file mode 100644
index 0000000000..1cd0aadb6e
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser_page_style_menu_update.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE = WEB_ROOT + "page_style_sample.html";
+
+/**
+ * Tests that the Page Style menu shows the currently
+ * selected Page Style after a new one has been selected.
+ */
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ let browser = tab.linkedBrowser;
+ BrowserTestUtils.loadURIString(browser, PAGE);
+ await promiseStylesheetsLoaded(browser, 18);
+
+ let menupopup = document.getElementById("pageStyleMenu").menupopup;
+ gPageStyleMenu.fillPopup(menupopup);
+
+ // page_style_sample.html should default us to selecting the stylesheet
+ // with the title "6" first.
+ let selected = menupopup.querySelector("menuitem[checked='true']");
+ is(
+ selected.getAttribute("label"),
+ "6",
+ "Should have '6' stylesheet selected by default"
+ );
+
+ // Now select stylesheet "1"
+ let target = menupopup.querySelector("menuitem[label='1']");
+ target.doCommand();
+
+ gPageStyleMenu.fillPopup(menupopup);
+ // gPageStyleMenu empties out the menu between opens, so we need
+ // to get a new reference to the selected menuitem
+ selected = menupopup.querySelector("menuitem[checked='true']");
+ is(
+ selected.getAttribute("label"),
+ "1",
+ "Should now have stylesheet 1 selected"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/pageStyle/head.js b/browser/base/content/test/pageStyle/head.js
new file mode 100644
index 0000000000..57d5947d50
--- /dev/null
+++ b/browser/base/content/test/pageStyle/head.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const WEB_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+/**
+ * Waits for the stylesheets to be loaded into the browser menu.
+ *
+ * @param browser
+ * The browser that contains the webpage we're testing.
+ * @param styleSheetCount
+ * How many stylesheets we expect to be loaded.
+ * @return Promise
+ */
+function promiseStylesheetsLoaded(browser, styleSheetCount) {
+ return TestUtils.waitForCondition(() => {
+ let actor =
+ browser.browsingContext?.currentWindowGlobal?.getActor("PageStyle");
+ if (!actor) {
+ info("No jswindowactor (yet?)");
+ return false;
+ }
+ let sheetCount = actor.getSheetInfo().filteredStyleSheets.length;
+ info(`waiting for sheets: ${sheetCount}`);
+ return sheetCount >= styleSheetCount;
+ }, "waiting for style sheets to load");
+}
diff --git a/browser/base/content/test/pageStyle/page_style.html b/browser/base/content/test/pageStyle/page_style.html
new file mode 100644
index 0000000000..c16a9ea4aa
--- /dev/null
+++ b/browser/base/content/test/pageStyle/page_style.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<style>
+p { color: blue; font-weight: bold; }
+</style>
+<p>Some text.</p>
+<script>
+let gFramesLoaded = 0;
+</script>
diff --git a/browser/base/content/test/pageStyle/page_style_only_alternates.html b/browser/base/content/test/pageStyle/page_style_only_alternates.html
new file mode 100644
index 0000000000..b5f4a8181c
--- /dev/null
+++ b/browser/base/content/test/pageStyle/page_style_only_alternates.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<title>Test for the page style menu</title>
+<!-- We only have alternates here intentionally. "Basic Page Style" should still work and remove the blue / red colors -->
+<style title="blue">:root { color: blue }</style>
+<style title="red">:root { color: red}</style>
diff --git a/browser/base/content/test/pageStyle/page_style_sample.html b/browser/base/content/test/pageStyle/page_style_sample.html
new file mode 100644
index 0000000000..ec89e99bc9
--- /dev/null
+++ b/browser/base/content/test/pageStyle/page_style_sample.html
@@ -0,0 +1,45 @@
+<html>
+ <head>
+ <title>Test for page style menu</title>
+ <!-- data-state values:
+ 0: should not appear in the page style menu
+ 1: should appear in the page style menu
+ 2: should appear in the page style menu as the selected stylesheet -->
+ <style data-state="0">
+ /* Some default styles to ensure that disabling styles works */
+ :root { color: lime }
+ </style>
+ <link data-state="1" href="style.css" title="1" rel="alternate stylesheet">
+ <link data-state="0" title="2" rel="alternate stylesheet">
+ <link data-state="0" href="style.css" rel="alternate stylesheet">
+ <link data-state="0" href="style.css" title="" rel="alternate stylesheet">
+ <link data-state="1" href="style.css" title="3" rel="stylesheet alternate">
+ <link data-state="1" href="style.css" title="4" rel=" alternate stylesheet ">
+ <link data-state="1" href="style.css" title="5" rel="alternate stylesheet">
+ <link data-state="2" href="style.css" title="6" rel="stylesheet">
+ <link data-state="1" href="style.css" title="7" rel="foo stylesheet">
+ <link data-state="0" href="style.css" title="8" rel="alternate">
+ <link data-state="1" href="style.css" title="9" rel="alternate STYLEsheet">
+ <link data-state="1" href="style.css" title="10" rel="alternate stylesheet" media="">
+ <link data-state="1" href="style.css" title="11" rel="alternate stylesheet" media="all">
+ <link data-state="1" href="style.css" title="12" rel="alternate stylesheet" media="ALL ">
+ <link data-state="1" href="style.css" title="13" rel="alternate stylesheet" media="screen">
+ <link data-state="1" href="style.css" title="14" rel="alternate stylesheet" media=" Screen">
+ <link data-state="0" href="style.css" title="15" rel="alternate stylesheet" media="screen foo">
+ <link data-state="0" href="style.css" title="16" rel="alternate stylesheet" media="all screen">
+ <link data-state="0" href="style.css" title="17" rel="alternate stylesheet" media="foo bar">
+ <link data-state="1" href="style.css" title="18" rel="alternate stylesheet" media="all,screen">
+ <link data-state="1" href="style.css" title="19" rel="alternate stylesheet" media="all, screen">
+ <link data-state="0" href="style.css" title="20" rel="alternate stylesheet" media="all screen">
+ <link data-state="0" href="style.css" title="21" rel="alternate stylesheet" media="foo">
+ <link data-state="0" href="style.css" title="22" rel="alternate stylesheet" media="allscreen">
+ <link data-state="0" href="style.css" title="23" rel="alternate stylesheet" media="_all">
+ <link data-state="0" href="style.css" title="24" rel="alternate stylesheet" media="not screen">
+ <link data-state="1" href="style.css" title="25" rel="alternate stylesheet" media="only screen">
+ <link data-state="1" href="style.css" title="26" rel="alternate stylesheet" media="screen and (min-device-width: 1px)">
+ <link data-state="0" href="style.css" title="27" rel="alternate stylesheet" media="screen and (max-device-width: 1px)">
+ <style data-state="1" title="28">:root { color: blue }</style>
+ <link data-state="1" href="style.css" title="29" rel="alternate stylesheet" disabled>
+ </head>
+ <body></body>
+</html>
diff --git a/browser/base/content/test/pageStyle/style.css b/browser/base/content/test/pageStyle/style.css
new file mode 100644
index 0000000000..c8337cea19
--- /dev/null
+++ b/browser/base/content/test/pageStyle/style.css
@@ -0,0 +1 @@
+.unused { /* This sheet is here for testing purposes. */ }
diff --git a/browser/base/content/test/pageinfo/all_images.html b/browser/base/content/test/pageinfo/all_images.html
new file mode 100644
index 0000000000..c246e25519
--- /dev/null
+++ b/browser/base/content/test/pageinfo/all_images.html
@@ -0,0 +1,15 @@
+<html>
+ <head>
+ <title>Test for media tab</title>
+ <link rel='shortcut icon' href='dummy_icon.ico'>
+ </head>
+ <body style='background-image:url(about:logo?a);'>
+ <img src='dummy_image.gif'>
+ <ul>
+ <li style='list-style:url(about:logo?b);'>List Item 1</li>
+ </ul>
+ <div style='-moz-border-image: url(about:logo?c) 20 20 20 20;'>test</div>
+ <a href='' style='cursor: url(about:logo?d),default;'>test link</a>
+ <object type='image/svg+xml' width=20 height=20 data='dummy_object.svg'></object>
+ </body>
+</html>");
diff --git a/browser/base/content/test/pageinfo/browser.ini b/browser/base/content/test/pageinfo/browser.ini
new file mode 100644
index 0000000000..c109c6bf66
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser.ini
@@ -0,0 +1,27 @@
+[DEFAULT]
+
+[browser_pageinfo_firstPartyIsolation.js]
+support-files =
+ image.html
+ ../general/audio.ogg
+ ../general/moz.png
+ ../general/video.ogg
+[browser_pageinfo_iframe_media.js]
+support-files =
+ iframes.html
+[browser_pageinfo_image_info.js]
+skip-if = (os == 'linux') # bug 1161699
+[browser_pageinfo_images.js]
+support-files =
+ all_images.html
+[browser_pageinfo_permissions.js]
+[browser_pageinfo_rtl.js]
+[browser_pageinfo_security.js]
+https_first_disabled = true
+support-files =
+ ../general/moz.png
+[browser_pageinfo_separate_private.js]
+[browser_pageinfo_svg_image.js]
+support-files =
+ svg_image.html
+ ../general/title_test.svg
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js b/browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js
new file mode 100644
index 0000000000..b280242b40
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js
@@ -0,0 +1,89 @@
+const Cm = Components.manager;
+
+async function testFirstPartyDomain(pageInfo) {
+ const EXPECTED_DOMAIN = "example.com";
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+ info("pageInfo initialized");
+ let tree = pageInfo.document.getElementById("imagetree");
+ Assert.ok(!!tree, "should have imagetree element");
+
+ // i=0: <img>
+ // i=1: <video>
+ // i=2: <audio>
+ for (let i = 0; i < 3; i++) {
+ info("imagetree select " + i);
+ tree.view.selection.select(i);
+ tree.ensureRowIsVisible(i);
+ tree.focus();
+
+ let preview = pageInfo.document.getElementById("thepreviewimage");
+ info("preview.src=" + preview.src);
+
+ // For <img>, we will query imgIRequest.imagePrincipal later, so we wait
+ // for load event. For <audio> and <video>, so far we only can get
+ // the triggeringprincipal attribute on the node, so we simply wait for
+ // loadstart.
+ if (i == 0) {
+ await BrowserTestUtils.waitForEvent(preview, "load");
+ } else {
+ await BrowserTestUtils.waitForEvent(preview, "loadstart");
+ }
+
+ info("preview load " + i);
+
+ // Originally thepreviewimage is loaded with SystemPrincipal, therefore
+ // it won't have origin attributes, now we've changed to loadingPrincipal
+ // to the content in bug 1376971, it should have firstPartyDomain set.
+ if (i == 0) {
+ let req = preview.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ Assert.equal(
+ req.imagePrincipal.originAttributes.firstPartyDomain,
+ EXPECTED_DOMAIN,
+ "imagePrincipal should have firstPartyDomain set to " + EXPECTED_DOMAIN
+ );
+ }
+
+ // Check the node has the attribute 'triggeringprincipal'.
+ let loadingPrincipalStr = preview.getAttribute("triggeringprincipal");
+ let loadingPrincipal = E10SUtils.deserializePrincipal(loadingPrincipalStr);
+ Assert.equal(
+ loadingPrincipal.originAttributes.firstPartyDomain,
+ EXPECTED_DOMAIN,
+ "loadingPrincipal should have firstPartyDomain set to " + EXPECTED_DOMAIN
+ );
+ }
+}
+
+async function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("privacy.firstparty.isolate", true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("privacy.firstparty.isolate");
+ });
+
+ let url =
+ "https://example.com/browser/browser/base/content/test/pageinfo/image.html";
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await loadPromise;
+
+ // Pass a dummy imageElement, if there isn't an imageElement, pageInfo.js
+ // will do a preview, however this sometimes will cause intermittent failures,
+ // see bug 1403365.
+ let pageInfo = BrowserPageInfo(url, "mediaTab", {});
+ info("waitForEvent pageInfo");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+
+ info("calling testFirstPartyDomain");
+ await testFirstPartyDomain(pageInfo);
+
+ pageInfo.close();
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js b/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js
new file mode 100644
index 0000000000..3040474f31
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js
@@ -0,0 +1,31 @@
+/* Check proper media data retrieval in case of iframe */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+add_task(async function test_all_images_mentioned() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "iframes.html",
+ async function () {
+ let pageInfo = BrowserPageInfo(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab"
+ );
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ let imageTree = pageInfo.document.getElementById("imagetree");
+ let imageRowsNum = imageTree.view.rowCount;
+
+ ok(imageTree, "Image tree is null (media tab is broken)");
+ ok(
+ imageRowsNum == 2,
+ "Number of media items listed: " + imageRowsNum + ", should be 2"
+ );
+
+ pageInfo.close();
+ }
+ );
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js b/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
new file mode 100644
index 0000000000..374cd5f032
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
@@ -0,0 +1,57 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ **/
+
+const URI =
+ "data:text/html," +
+ "<style type='text/css'>%23test-image,%23not-test-image {background-image: url('about:logo?c');}</style>" +
+ "<img src='about:logo?b' height=300 width=350 alt=2 id='not-test-image'>" +
+ "<img src='about:logo?b' height=300 width=350 alt=2>" +
+ "<img src='about:logo?a' height=200 width=250>" +
+ "<img src='about:logo?b' height=200 width=250 alt=1>" +
+ "<img src='about:logo?b' height=100 width=150 alt=2 id='test-image'>";
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI);
+ let browser = tab.linkedBrowser;
+
+ let imageInfo = await SpecialPowers.spawn(browser, [], async () => {
+ let testImg = content.document.getElementById("test-image");
+
+ return {
+ src: testImg.src,
+ currentSrc: testImg.currentSrc,
+ width: testImg.width,
+ height: testImg.height,
+ imageText: testImg.title || testImg.alt,
+ };
+ });
+
+ let pageInfo = BrowserPageInfo(
+ browser.currentURI.spec,
+ "mediaTab",
+ imageInfo
+ );
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ let pageInfoImg = pageInfo.document.getElementById("thepreviewimage");
+ await BrowserTestUtils.waitForEvent(pageInfoImg, "load");
+ Assert.equal(
+ pageInfoImg.src,
+ imageInfo.src,
+ "selected image has the correct source"
+ );
+ Assert.equal(
+ pageInfoImg.width,
+ imageInfo.width,
+ "selected image has the correct width"
+ );
+ Assert.equal(
+ pageInfoImg.height,
+ imageInfo.height,
+ "selected image has the correct height"
+ );
+ pageInfo.close();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_images.js b/browser/base/content/test/pageinfo/browser_pageinfo_images.js
new file mode 100644
index 0000000000..5cb4c79bf3
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_images.js
@@ -0,0 +1,93 @@
+/* Check proper image url retrieval from all kinds of elements/styles */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+add_task(async function test_all_images_mentioned() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "all_images.html",
+ async function () {
+ let pageInfo = BrowserPageInfo(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab"
+ );
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ let imageTree = pageInfo.document.getElementById("imagetree");
+ let imageRowsNum = imageTree.view.rowCount;
+
+ ok(imageTree, "Image tree is null (media tab is broken)");
+
+ ok(
+ imageRowsNum == 7,
+ "Number of images listed: " + imageRowsNum + ", should be 7"
+ );
+
+ // Check that select all works
+ imageTree.focus();
+ ok(
+ !pageInfo.document.getElementById("cmd_copy").hasAttribute("disabled"),
+ "copy is enabled"
+ );
+ ok(
+ !pageInfo.document
+ .getElementById("cmd_selectAll")
+ .hasAttribute("disabled"),
+ "select all is enabled"
+ );
+ pageInfo.goDoCommand("cmd_selectAll");
+ is(imageTree.view.selection.count, 7, "all rows selected");
+
+ pageInfo.close();
+ }
+ );
+});
+
+add_task(async function test_view_image_info() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.menu.showViewImageInfo", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "all_images.html",
+
+ async function (browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let viewImageInfo = document.getElementById("context-viewimageinfo");
+
+ let imageInfo = await SpecialPowers.spawn(browser, [], async () => {
+ let testImg = content.document.querySelector("img");
+ return {
+ src: testImg.src,
+ };
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "img",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+
+ await BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+
+ let promisePageInfoLoaded = BrowserTestUtils.domWindowOpened().then(win =>
+ BrowserTestUtils.waitForEvent(win, "page-info-init")
+ );
+
+ contextMenu.activateItem(viewImageInfo);
+
+ let pageInfo = (await promisePageInfoLoaded).target.ownerGlobal;
+ let pageInfoImg = pageInfo.document.getElementById("thepreviewimage");
+
+ Assert.equal(
+ pageInfoImg.src,
+ imageInfo.src,
+ "selected image is the correct"
+ );
+ await BrowserTestUtils.closeWindow(pageInfo);
+ }
+ );
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js b/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js
new file mode 100644
index 0000000000..7e3e83b60d
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js
@@ -0,0 +1,258 @@
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const TEST_ORIGIN = "https://example.com";
+const TEST_ORIGIN_CERT_ERROR = "https://expired.example.com";
+const LOW_TLS_VERSION = "https://tls1.example.com/";
+
+async function testPermissions(defaultPermission) {
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) {
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+
+ let defaultCheckbox = await TestUtils.waitForCondition(() =>
+ pageInfo.document.getElementById("geoDef")
+ );
+ let radioGroup = pageInfo.document.getElementById("geoRadioGroup");
+ let defaultRadioButton = pageInfo.document.getElementById(
+ "geo#" + defaultPermission
+ );
+ let blockRadioButton = pageInfo.document.getElementById("geo#2");
+
+ ok(defaultCheckbox.checked, "The default checkbox should be checked.");
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.DENY_ACTION
+ );
+
+ ok(!defaultCheckbox.checked, "The default checkbox should not be checked.");
+
+ defaultCheckbox.checked = true;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ ok(
+ !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
+ "Checking the default checkbox should reset the permission."
+ );
+
+ defaultCheckbox.checked = false;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ ok(
+ !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
+ "Unchecking the default checkbox should pick the default permission."
+ );
+ is(
+ radioGroup.selectedItem,
+ defaultRadioButton,
+ "The unknown radio button should be selected."
+ );
+
+ radioGroup.selectedItem = blockRadioButton;
+ blockRadioButton.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo")
+ .capability,
+ Services.perms.DENY_ACTION,
+ "Selecting a value in the radio group should set the corresponding permission"
+ );
+
+ radioGroup.selectedItem = defaultRadioButton;
+ defaultRadioButton.dispatchEvent(new Event("command"));
+
+ ok(
+ !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
+ "Selecting the default value should reset the permission."
+ );
+ ok(defaultCheckbox.checked, "The default checkbox should be checked.");
+
+ pageInfo.close();
+ PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+ });
+}
+
+// Test displaying website permissions on certificate error pages.
+add_task(async function test_CertificateError() {
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ORIGIN_CERT_ERROR
+ );
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ await pageLoaded;
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN_CERT_ERROR, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let permissionTab = pageInfo.document.getElementById("permTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(permissionTab),
+ "Permission tab should be visible."
+ );
+
+ let hostText = pageInfo.document.getElementById("hostText");
+ let permList = pageInfo.document.getElementById("permList");
+ let excludedPermissions = pageInfo.window.getExcludedPermissions();
+ let permissions = SitePermissions.listPermissions().filter(
+ p =>
+ SitePermissions.getPermissionLabel(p) != null &&
+ !excludedPermissions.includes(p)
+ );
+
+ await TestUtils.waitForCondition(
+ () => hostText.value === browser.currentURI.displayPrePath,
+ `Value of owner should be "${browser.currentURI.displayPrePath}" instead got "${hostText.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => permList.childElementCount === permissions.length,
+ `Value of verifier should be ${permissions.length}, instead got ${permList.childElementCount}.`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying website permissions on network error pages.
+add_task(async function test_NetworkError() {
+ // Setup for TLS error
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+ Services.prefs.setIntPref("security.tls.version.min", 3);
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, LOW_TLS_VERSION);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ await pageLoaded;
+
+ let pageInfo = BrowserPageInfo(LOW_TLS_VERSION, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let permissionTab = pageInfo.document.getElementById("permTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(permissionTab),
+ "Permission tab should be visible."
+ );
+
+ let hostText = pageInfo.document.getElementById("hostText");
+ let permList = pageInfo.document.getElementById("permList");
+ let excludedPermissions = pageInfo.window.getExcludedPermissions();
+ let permissions = SitePermissions.listPermissions().filter(
+ p =>
+ SitePermissions.getPermissionLabel(p) != null &&
+ !excludedPermissions.includes(p)
+ );
+
+ await TestUtils.waitForCondition(
+ () => hostText.value === browser.currentURI.displayPrePath,
+ `Value of host should be should be "${browser.currentURI.displayPrePath}" instead got "${hostText.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => permList.childElementCount === permissions.length,
+ `Value of permissions list should be ${permissions.length}, instead got ${permList.childElementCount}.`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test some standard operations in the permission tab.
+add_task(async function test_geo_permission() {
+ await testPermissions(Services.perms.UNKNOWN_ACTION);
+});
+
+// Test some standard operations in the permission tab, falling back to a custom
+// default permission instead of UNKNOWN.
+add_task(async function test_default_geo_permission() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["permissions.default.geo", SitePermissions.ALLOW]],
+ });
+ await testPermissions(Services.perms.ALLOW_ACTION);
+});
+
+// Test special behavior for cookie permissions.
+add_task(async function test_cookie_permission() {
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) {
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+
+ let defaultCheckbox = await TestUtils.waitForCondition(() =>
+ pageInfo.document.getElementById("cookieDef")
+ );
+ let radioGroup = pageInfo.document.getElementById("cookieRadioGroup");
+ let allowRadioButton = pageInfo.document.getElementById("cookie#1");
+ let blockRadioButton = pageInfo.document.getElementById("cookie#2");
+
+ ok(defaultCheckbox.checked, "The default checkbox should be checked.");
+
+ defaultCheckbox.checked = false;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.ALLOW,
+ "Unchecking the default checkbox should pick the default permission."
+ );
+ is(
+ radioGroup.selectedItem,
+ allowRadioButton,
+ "The unknown radio button should be selected."
+ );
+
+ radioGroup.selectedItem = blockRadioButton;
+ blockRadioButton.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.BLOCK,
+ "Selecting a value in the radio group should set the corresponding permission"
+ );
+
+ radioGroup.selectedItem = allowRadioButton;
+ allowRadioButton.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.ALLOW,
+ "Selecting a value in the radio group should set the corresponding permission"
+ );
+ ok(!defaultCheckbox.checked, "The default checkbox should not be checked.");
+
+ defaultCheckbox.checked = true;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.UNKNOWN,
+ "Checking the default checkbox should reset the permission."
+ );
+ is(
+ radioGroup.selectedItem,
+ null,
+ "For cookies, no item should be selected when the checkbox is checked."
+ );
+
+ pageInfo.close();
+ PermissionTestUtils.remove(gBrowser.currentURI, "cookie");
+ });
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_rtl.js b/browser/base/content/test/pageinfo/browser_pageinfo_rtl.js
new file mode 100644
index 0000000000..d0c06a03ff
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_rtl.js
@@ -0,0 +1,28 @@
+async function testPageInfo() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ let pageInfo = BrowserPageInfo();
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+ is(
+ getComputedStyle(pageInfo.document.documentElement).direction,
+ "rtl",
+ "Should be RTL"
+ );
+ ok(true, "Didn't assert or crash");
+ pageInfo.close();
+ }
+ );
+}
+
+add_task(async function test_page_info_rtl() {
+ await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] });
+
+ for (let useOverlayScrollbars of [0, 1]) {
+ info("Testing with overlay scrollbars: " + useOverlayScrollbars);
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.useOverlayScrollbars", useOverlayScrollbars]],
+ });
+ await testPageInfo();
+ }
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_security.js b/browser/base/content/test/pageinfo/browser_pageinfo_security.js
new file mode 100644
index 0000000000..0a8c57a46d
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_security.js
@@ -0,0 +1,354 @@
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+ SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.sys.mjs",
+});
+
+const TEST_ORIGIN = "https://example.com";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_HTTP_ORIGIN = "http://example.com";
+const TEST_SUB_ORIGIN = "https://test1.example.com";
+const REMOVE_DIALOG_URL =
+ "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml";
+const TEST_ORIGIN_CERT_ERROR = "https://expired.example.com";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+// Test opening the correct certificate information when clicking "Show certificate".
+add_task(async function test_ShowCertificate() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_ORIGIN);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_SUB_ORIGIN
+ );
+
+ let pageInfo = BrowserPageInfo(TEST_SUB_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ async function openAboutCertificate() {
+ let loaded = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+ let viewCertButton = pageInfoDoc.getElementById("security-view-cert");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(viewCertButton),
+ "view cert button should be visible."
+ );
+ viewCertButton.click();
+ await loaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let certificateSection = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.querySelector("certificate-section");
+ }, "Certificate section found");
+
+ let commonName = certificateSection.shadowRoot
+ .querySelector(".subject-name")
+ .shadowRoot.querySelector(".common-name")
+ .shadowRoot.querySelector(".info").textContent;
+ is(commonName, "example.com", "Should have the same common name.");
+ });
+
+ gBrowser.removeCurrentTab(); // closes about:certificate
+ }
+
+ await openAboutCertificate();
+
+ gBrowser.selectedTab = tab1;
+
+ await openAboutCertificate();
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Test displaying website identity information when loading images.
+add_task(async function test_image() {
+ let url = TEST_PATH + "moz.png";
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ let pageInfo = BrowserPageInfo(url, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be should be "This website does not supply ownership information." instead got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Mozilla Testing",
+ `Value of verifier should be "Mozilla Testing", instead got "${verifier.value}".`
+ );
+
+ let browser = gBrowser.selectedBrowser;
+
+ await TestUtils.waitForCondition(
+ () => domain.value === browser.currentURI.displayHost,
+ `Value of domain should be ${browser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying website identity information on certificate error pages.
+add_task(async function test_CertificateError() {
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ORIGIN_CERT_ERROR
+ );
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ await pageLoaded;
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN_CERT_ERROR, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be should be "This website does not supply ownership information." instead got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Not specified",
+ `Value of verifier should be "Not specified", instead got "${verifier.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => domain.value === browser.currentURI.displayHost,
+ `Value of domain should be ${browser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying website identity information on http pages.
+add_task(async function test_SecurityHTTP() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_HTTP_ORIGIN);
+
+ let pageInfo = BrowserPageInfo(TEST_HTTP_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be should be "This website does not supply ownership information." instead got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Not specified",
+ `Value of verifier should be "Not specified", instead got "${verifier.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => domain.value === gBrowser.selectedBrowser.currentURI.displayHost,
+ `Value of domain should be ${gBrowser.selectedBrowser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying valid certificate information in page info.
+add_task(async function test_ValidCert() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_ORIGIN);
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be "This website does not supply ownership information.", got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Mozilla Testing",
+ `Value of verifier should be "Mozilla Testing", got "${verifier.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => domain.value === gBrowser.selectedBrowser.currentURI.displayHost,
+ `Value of domain should be ${gBrowser.selectedBrowser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying and removing quota managed data.
+add_task(async function test_SiteData() {
+ await SiteDataTestUtils.addToIndexedDB(TEST_ORIGIN);
+
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) {
+ let totalUsage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
+ Assert.greater(totalUsage, 0, "The total usage should not be 0");
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+
+ let label = pageInfoDoc.getElementById("security-privacy-sitedata-value");
+ let clearButton = pageInfoDoc.getElementById("security-clear-sitedata");
+
+ let size = DownloadUtils.convertByteUnits(totalUsage);
+
+ // The usage details are filled asynchronously, so we assert that they're present by
+ // waiting for them to be filled in.
+ // We only wait for the right unit to appear, since this number is intermittently
+ // varying by slight amounts on infra machines.
+ await TestUtils.waitForCondition(
+ () => label.textContent.includes(size[1]),
+ "Should show site data usage in the security section."
+ );
+ let siteDataUpdated = TestUtils.topicObserved(
+ "sitedatamanager:sites-updated"
+ );
+
+ let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ clearButton.click();
+ await removeDialogPromise;
+
+ await siteDataUpdated;
+
+ totalUsage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
+ is(totalUsage, 0, "The total usage should be 0");
+
+ await TestUtils.waitForCondition(
+ () => label.textContent == "No",
+ "Should show no site data usage in the security section."
+ );
+
+ pageInfo.close();
+ });
+});
+
+// Test displaying and removing cookies.
+add_task(async function test_Cookies() {
+ // Add some test cookies.
+ SiteDataTestUtils.addToCookies({
+ origin: TEST_ORIGIN,
+ name: "test1",
+ value: "1",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: TEST_ORIGIN,
+ name: "test2",
+ value: "2",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: TEST_SUB_ORIGIN,
+ name: "test1",
+ value: "1",
+ });
+
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) {
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+
+ let pageInfoDoc = pageInfo.document;
+
+ let label = pageInfoDoc.getElementById("security-privacy-sitedata-value");
+ let clearButton = pageInfoDoc.getElementById("security-clear-sitedata");
+
+ // The usage details are filled asynchronously, so we assert that they're present by
+ // waiting for them to be filled in.
+ await TestUtils.waitForCondition(
+ () => label.textContent.includes("cookies"),
+ "Should show cookies in the security section."
+ );
+
+ let cookiesCleared = TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted"
+ );
+
+ let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ clearButton.click();
+ await removeDialogPromise;
+
+ await cookiesCleared;
+
+ let uri = Services.io.newURI(TEST_ORIGIN);
+ is(
+ Services.cookies.countCookiesFromHost(uri.host),
+ 0,
+ "Cookies from the base domain should be cleared"
+ );
+
+ await TestUtils.waitForCondition(
+ () => label.textContent == "No",
+ "Should show no cookies in the security section."
+ );
+
+ pageInfo.close();
+ });
+});
+
+// Clean up in case we missed anything...
+add_task(async function cleanup() {
+ await SiteDataTestUtils.clear();
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js b/browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js
new file mode 100644
index 0000000000..ac93b7ddb2
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js
@@ -0,0 +1,49 @@
+/**
+ * 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,
+ "https://example.com"
+ );
+ let browser = tab.linkedBrowser;
+ let pageInfo = BrowserPageInfo(browser.currentURI.spec);
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+ Assert.strictEqual(
+ pageInfo.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing,
+ false,
+ "non-private window opened private page info window"
+ );
+
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ "https://example.com"
+ );
+ let privateBrowser = privateTab.linkedBrowser;
+ let privatePageInfo = privateWindow.BrowserPageInfo(
+ privateBrowser.currentURI.spec
+ );
+ await BrowserTestUtils.waitForEvent(privatePageInfo, "page-info-init");
+ Assert.strictEqual(
+ privatePageInfo.docShell.QueryInterface(Ci.nsILoadContext)
+ .usePrivateBrowsing,
+ true,
+ "private window opened non-private page info window"
+ );
+
+ Assert.notEqual(
+ pageInfo,
+ privatePageInfo,
+ "private and non-private windows shouldn't have shared the same page info window"
+ );
+ pageInfo.close();
+ privatePageInfo.close();
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js b/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js
new file mode 100644
index 0000000000..547c6158ad
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js
@@ -0,0 +1,34 @@
+const URI =
+ "https://example.com/browser/browser/base/content/test/pageinfo/svg_image.html";
+
+add_task(async function () {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, URI);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, URI);
+
+ const pageInfo = BrowserPageInfo(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab"
+ );
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ const imageTree = pageInfo.document.getElementById("imagetree");
+ const imageRowsNum = imageTree.view.rowCount;
+
+ ok(imageTree, "Image tree is null (media tab is broken)");
+
+ is(imageRowsNum, 1, "should have one image");
+
+ // Only bother running this if we've got the right number of rows.
+ if (imageRowsNum == 1) {
+ is(
+ imageTree.view.getCellText(0, imageTree.columns[0]),
+ "https://example.com/browser/browser/base/content/test/pageinfo/title_test.svg",
+ "The URL should be the svg image."
+ );
+ }
+
+ pageInfo.close();
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/pageinfo/iframes.html b/browser/base/content/test/pageinfo/iframes.html
new file mode 100644
index 0000000000..b29680cbd1
--- /dev/null
+++ b/browser/base/content/test/pageinfo/iframes.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Test for media tab with iframe</title>
+ </head>
+ <body style='background-image:url(about:logo?a);'>
+ <iframe width="420" height="345" src="moz.png"></iframe>
+ </body>
+</html>");
diff --git a/browser/base/content/test/pageinfo/image.html b/browser/base/content/test/pageinfo/image.html
new file mode 100644
index 0000000000..1261be8e7b
--- /dev/null
+++ b/browser/base/content/test/pageinfo/image.html
@@ -0,0 +1,5 @@
+<html>
+ <img src='moz.png' height=100 width=150 id='test-image'>
+ <video src='video.ogg' id='test-video'></video>
+ <audio src='audio.ogg' id='test-audio'></audio>
+</html>
diff --git a/browser/base/content/test/pageinfo/svg_image.html b/browser/base/content/test/pageinfo/svg_image.html
new file mode 100644
index 0000000000..7ab17c33a0
--- /dev/null
+++ b/browser/base/content/test/pageinfo/svg_image.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for page info svg images</title>
+ </head>
+ <body>
+ <svg width="20" height="20">
+ <image xlink:href="title_test.svg" width="20" height="20">
+ </svg>
+ </body>
+</html>
diff --git a/browser/base/content/test/performance/PerfTestHelpers.sys.mjs b/browser/base/content/test/performance/PerfTestHelpers.sys.mjs
new file mode 100644
index 0000000000..075e436331
--- /dev/null
+++ b/browser/base/content/test/performance/PerfTestHelpers.sys.mjs
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+
+export var PerfTestHelpers = {
+ /**
+ * Maps the entries in the given iterable to the given
+ * promise-returning task function, and waits for all returned
+ * promises to have resolved. At most `limit` promises may remain
+ * unresolved at a time. When the limit is reached, the function will
+ * wait for some to resolve before spawning more tasks.
+ */
+ async throttledMapPromises(iterable, task, limit = 64) {
+ let pending = new Set();
+ let promises = [];
+ for (let data of iterable) {
+ while (pending.size >= limit) {
+ await Promise.race(pending);
+ }
+
+ let promise = task(data);
+ promises.push(promise);
+ if (promise) {
+ promise.finally(() => pending.delete(promise));
+ pending.add(promise);
+ }
+ }
+
+ return Promise.all(promises);
+ },
+
+ /**
+ * Returns a promise which resolves to true if the resource at the
+ * given URI exists, false if it doesn't. This should only be used
+ * with local resources, such as from resource:/chrome:/jar:/file:
+ * URIs.
+ */
+ checkURIExists(uri) {
+ return new Promise(resolve => {
+ try {
+ let channel = lazy.NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ // Avoid crashing for non-existant files. If the file not existing
+ // is bad, we can deal with it in the test instead.
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_FETCH,
+ });
+
+ channel.asyncOpen({
+ onStartRequest(request) {
+ resolve(Components.isSuccessCode(request.status));
+ request.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest(request, status) {
+ // We should have already resolved from `onStartRequest`, but
+ // we resolve again here just as a failsafe.
+ resolve(Components.isSuccessCode(status));
+ },
+ });
+ } catch (e) {
+ if (
+ e.result != Cr.NS_ERROR_FILE_NOT_FOUND &&
+ e.result != Cr.NS_ERROR_NOT_AVAILABLE
+ ) {
+ throw e;
+ }
+ resolve(false);
+ }
+ });
+ },
+};
diff --git a/browser/base/content/test/performance/StartupContentSubframe.sys.mjs b/browser/base/content/test/performance/StartupContentSubframe.sys.mjs
new file mode 100644
index 0000000000..a78e456afb
--- /dev/null
+++ b/browser/base/content/test/performance/StartupContentSubframe.sys.mjs
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * test helper JSWindowActors used by the browser_startup_content_subframe.js test.
+ */
+
+export class StartupContentSubframeParent extends JSWindowActorParent {
+ receiveMessage(msg) {
+ // Tell the test about the data we received from the content process.
+ Services.obs.notifyObservers(
+ msg.data,
+ "startup-content-subframe-loaded-scripts"
+ );
+ }
+}
+
+export class StartupContentSubframeChild extends JSWindowActorChild {
+ async handleEvent(event) {
+ // When the remote subframe is loaded, an event will be fired to this actor,
+ // which will cause us to send the `LoadedScripts` message to the parent
+ // process.
+ // Wait a spin of the event loop before doing so to ensure we don't
+ // miss any scripts loaded immediately after the load event.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ const Cm = Components.manager;
+ Cm.QueryInterface(Ci.nsIServiceManager);
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ let collectStacks = AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG;
+
+ let modules = {};
+ for (let module of Cu.loadedJSModules) {
+ modules[module] = collectStacks ? Cu.getModuleImportStack(module) : "";
+ }
+ for (let module of Cu.loadedESModules) {
+ modules[module] = collectStacks ? Cu.getModuleImportStack(module) : "";
+ }
+
+ let services = {};
+ for (let contractID of Object.keys(Cc)) {
+ try {
+ if (Cm.isServiceInstantiatedByContractID(contractID, Ci.nsISupports)) {
+ services[contractID] = "";
+ }
+ } catch (e) {}
+ }
+ this.sendAsyncMessage("LoadedScripts", {
+ modules,
+ services,
+ });
+ }
+}
diff --git a/browser/base/content/test/performance/browser.ini b/browser/base/content/test/performance/browser.ini
new file mode 100644
index 0000000000..d50b8debcc
--- /dev/null
+++ b/browser/base/content/test/performance/browser.ini
@@ -0,0 +1,90 @@
+[DEFAULT]
+# to avoid overhead when running the browser normally, StartupRecorder.sys.mjs will
+# do almost nothing unless browser.startup.record is true.
+# gfx.canvas.willReadFrequently.enable is just an optimization, but needs to be
+# set during early startup to have an impact as a canvas will be used by
+# StartupRecorder.sys.mjs
+prefs =
+ # Skip migration work in BG__migrateUI for browser_startup.js since it isn't
+ # representative of common startup.
+ browser.migration.version=9999999
+ browser.startup.record=true
+ gfx.canvas.willReadFrequently.enable=true
+ # The form autofill framescript is only used in certain locales if this
+ # pref is set to 'detect', which is the default value on non-Nightly.
+ extensions.formautofill.addresses.available='on'
+ extensions.formautofill.creditCards.available='on'
+ browser.urlbar.disableExtendForTests=true
+ # For perfomance tests do not enable the remote control cue, which gets set
+ # when Marionette is enabled, but users normally don't see.
+ browser.chrome.disableRemoteControlCueForTests=true
+ # The Screenshots extension is disabled by default in Mochitests. We re-enable
+ # it here, since it's a more realistic configuration.
+ extensions.screenshots.disabled=false
+support-files =
+ head.js
+
+[browser_appmenu.js]
+skip-if =
+ asan
+ debug
+ os == "win" # Bug 1775626
+ os == "linux" && socketprocess_networking # Bug 1382809, bug 1369959
+[browser_panel_vsync.js]
+support-files =
+ !/browser/components/downloads/test/browser/head.js
+[browser_preferences_usage.js]
+https_first_disabled = true
+skip-if =
+ !debug
+ apple_catalina # platform migration
+ socketprocess_networking
+[browser_startup.js]
+[browser_startup_content.js]
+support-files =
+ file_empty.html
+[browser_startup_content_subframe.js]
+skip-if = !fission
+support-files =
+ file_empty.html
+ StartupContentSubframe.sys.mjs
+[browser_startup_flicker.js]
+run-if =
+ debug
+ nightly_build # Requires StartupRecorder.sys.mjs, which isn't shipped everywhere by default
+[browser_startup_hiddenwindow.js]
+skip-if =
+ os == "mac"
+[browser_tabclose.js]
+skip-if =
+ os == "linux" && devedition # Bug 1737131
+ os == "mac" # Bug 1531417
+ os == "win" # Bug 1488537, Bug 1497713
+[browser_tabclose_grow.js]
+[browser_tabdetach.js]
+[browser_tabopen.js]
+skip-if =
+ apple_catalina # Bug 1594274
+ os == "mac" && !debug # Bug 1705492
+ os == "linux" && !debug # Bug 1705492
+[browser_tabopen_squeeze.js]
+[browser_tabstrip_overflow_underflow.js]
+skip-if =
+ os == "win" && verify && !debug
+ os == 'win' && bits == 32
+[browser_tabswitch.js]
+skip-if =
+ os == "win" #Bug 1455054
+[browser_toolbariconcolor_restyles.js]
+[browser_urlbar_keyed_search.js]
+skip-if =
+ os == "win" && bits == 32 # # Disabled on Win32 because of intermittent OOM failures (bug 1448241)
+[browser_urlbar_search.js]
+skip-if =
+ os == "linux" && (debug || ccov) # Disabled on Linux debug and ccov due to intermittent timeouts. Bug 1414126.
+ os == "win" && (debug || ccov) # Disabled on Windows debug and ccov due to intermittent timeouts. bug 1426611.
+ os == "win" && bits == 32
+[browser_vsync_accessibility.js]
+[browser_window_resize.js]
+[browser_windowclose.js]
+[browser_windowopen.js]
diff --git a/browser/base/content/test/performance/browser_appmenu.js b/browser/base/content/test/performance/browser_appmenu.js
new file mode 100644
index 0000000000..3d554d8881
--- /dev/null
+++ b/browser/base/content/test/performance/browser_appmenu.js
@@ -0,0 +1,129 @@
+"use strict";
+/* global PanelUI */
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+/**
+ * WHOA THERE: We should never be adding new things to
+ * EXPECTED_APPMENU_OPEN_REFLOWS. This list should slowly go
+ * away as we improve the performance of the front-end. Instead of adding more
+ * reflows to the list, you should be modifying your code to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_APPMENU_OPEN_REFLOWS = [
+ {
+ stack: [
+ "openPopup/this._openPopupPromise<@resource:///modules/PanelMultiView.sys.mjs",
+ ],
+ },
+
+ {
+ stack: [
+ "_calculateMaxHeight@resource:///modules/PanelMultiView.sys.mjs",
+ "handleEvent@resource:///modules/PanelMultiView.sys.mjs",
+ ],
+
+ maxCount: 7, // This number should only ever go down - never up.
+ },
+];
+
+add_task(async function () {
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+ let menuButtonRect = document
+ .getElementById("PanelUI-menu-button")
+ .getBoundingClientRect();
+ let firstTabRect = gBrowser.selectedTab.getBoundingClientRect();
+ let frameExpectations = {
+ filter: rects => {
+ // We expect the menu button to get into the active state.
+ //
+ // XXX For some reason the menu panel isn't in our screenshots, but
+ // that's where we actually expect many changes.
+ return rects.filter(r => !rectInBoundingClientRect(r, menuButtonRect));
+ },
+ exceptions: [
+ {
+ name: "the urlbar placeholder moves up and down by a few pixels",
+ condition: r => rectInBoundingClientRect(r, textBoxRect),
+ },
+ {
+ name: "bug 1547341 - a first tab gets drawn early",
+ condition: r => rectInBoundingClientRect(r, firstTabRect),
+ },
+ ],
+ };
+
+ // First, open the appmenu.
+ await withPerfObserver(() => gCUITestUtils.openMainMenu(), {
+ expectedReflows: EXPECTED_APPMENU_OPEN_REFLOWS,
+ frames: frameExpectations,
+ });
+
+ // Now open a series of subviews, and then close the appmenu. We
+ // should not reflow during any of this.
+ await withPerfObserver(
+ async function () {
+ // This recursive function will take the current main or subview,
+ // find all of the buttons that navigate to subviews inside it,
+ // and click each one individually. Upon entering the new view,
+ // we recurse. When the subviews within a view have been
+ // exhausted, we go back up a level.
+ async function openSubViewsRecursively(currentView) {
+ let navButtons = Array.from(
+ // Ensure that only enabled buttons are tested
+ currentView.querySelectorAll(".subviewbutton-nav:not([disabled])")
+ );
+ if (!navButtons) {
+ return;
+ }
+
+ for (let button of navButtons) {
+ info("Click " + button.id);
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ PanelUI.panel,
+ "ViewShown"
+ );
+ button.click();
+ let viewShownEvent = await promiseViewShown;
+
+ // Workaround until bug 1363756 is fixed, then this can be removed.
+ let container = PanelUI.multiView.querySelector(
+ ".panel-viewcontainer"
+ );
+ await TestUtils.waitForCondition(() => {
+ return !container.hasAttribute("width");
+ });
+
+ info("Shown " + viewShownEvent.originalTarget.id);
+ await openSubViewsRecursively(viewShownEvent.originalTarget);
+ promiseViewShown = BrowserTestUtils.waitForEvent(
+ currentView,
+ "ViewShown"
+ );
+ PanelUI.multiView.goBack();
+ await promiseViewShown;
+
+ // Workaround until bug 1363756 is fixed, then this can be removed.
+ await TestUtils.waitForCondition(() => {
+ return !container.hasAttribute("width");
+ });
+ }
+ }
+
+ await openSubViewsRecursively(PanelUI.mainView);
+
+ await gCUITestUtils.hideMainMenu();
+ },
+ { expectedReflows: [], frames: frameExpectations }
+ );
+});
diff --git a/browser/base/content/test/performance/browser_panel_vsync.js b/browser/base/content/test/performance/browser_panel_vsync.js
new file mode 100644
index 0000000000..73c56b9095
--- /dev/null
+++ b/browser/base/content/test/performance/browser_panel_vsync.js
@@ -0,0 +1,69 @@
+/* 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/downloads/test/browser/head.js",
+ this
+);
+
+add_task(
+ async function test_opening_panel_and_closing_should_not_leave_vsync() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ await promiseButtonShown("downloads-button");
+
+ const downloadsButton = document.getElementById("downloads-button");
+ const shownPromise = promisePanelOpened();
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "click",
+ target: downloadsButton,
+ atCenter: true,
+ });
+ await shownPromise;
+
+ is(DownloadsPanel.panel.state, "open", "Check that panel state is 'open'");
+
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "Make sure vsync disabled"
+ );
+ // Should not already be using vsync
+ ok(!ChromeUtils.vsyncEnabled(), "vsync should be off initially");
+
+ if (
+ AppConstants.platform == "linux" &&
+ DownloadsPanel.panel.state != "open"
+ ) {
+ // Panels sometime receive spurious popuphiding events on Linux.
+ // Given the main target of this test is Windows, avoid causing
+ // intermittent failures and just make the test return early.
+ todo(
+ false,
+ "panel should still be 'open', current state: " +
+ DownloadsPanel.panel.state
+ );
+ return;
+ }
+
+ const hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
+ await hiddenPromise;
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "wait for vsync to be disabled again"
+ );
+
+ ok(!ChromeUtils.vsyncEnabled(), "vsync should still be off");
+ is(
+ DownloadsPanel.panel.state,
+ "closed",
+ "Check that panel state is 'closed'"
+ );
+ }
+);
diff --git a/browser/base/content/test/performance/browser_preferences_usage.js b/browser/base/content/test/performance/browser_preferences_usage.js
new file mode 100644
index 0000000000..d62cb2316b
--- /dev/null
+++ b/browser/base/content/test/performance/browser_preferences_usage.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+if (SpecialPowers.useRemoteSubframes) {
+ requestLongerTimeout(2);
+}
+
+const DEFAULT_PROCESS_COUNT = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref("dom.ipc.processCount");
+
+/**
+ * A test that checks whether any preference getter from the given list
+ * of stats was called more often than the max parameter.
+ *
+ * @param {Array} stats - an array of [prefName, accessCount] tuples
+ * @param {Number} max - the maximum number of times any of the prefs should
+ * have been called.
+ * @param {Object} knownProblematicPrefs (optional) - an object that defines
+ * prefs that should be exempt from checking the
+ * maximum access. It looks like the following:
+ *
+ * pref_name: {
+ * min: [Number] the minimum amount of times this should have
+ * been called (to avoid keeping around dead items)
+ * max: [Number] the maximum amount of times this should have
+ * been called (to avoid this creeping up further)
+ * }
+ */
+function checkPrefGetters(stats, max, knownProblematicPrefs = {}) {
+ let getterStats = Object.entries(stats).sort(
+ ([, val1], [, val2]) => val2 - val1
+ );
+
+ // Clone the list to be able to delete entries to check if we
+ // forgot any later on.
+ knownProblematicPrefs = Object.assign({}, knownProblematicPrefs);
+
+ for (let [pref, count] of getterStats) {
+ let prefLimits = knownProblematicPrefs[pref];
+ if (!prefLimits) {
+ Assert.lessOrEqual(
+ count,
+ max,
+ `${pref} should not be accessed more than ${max} times.`
+ );
+ } else {
+ // Still record how much this pref was accessed even if we don't do any real assertions.
+ if (!prefLimits.min && !prefLimits.max) {
+ info(
+ `${pref} should not be accessed more than ${max} times and was accessed ${count} times.`
+ );
+ }
+
+ if (prefLimits.min) {
+ Assert.lessOrEqual(
+ prefLimits.min,
+ count,
+ `${pref} should be accessed at least ${prefLimits.min} times.`
+ );
+ }
+ if (prefLimits.max) {
+ Assert.lessOrEqual(
+ count,
+ prefLimits.max,
+ `${pref} should be accessed at most ${prefLimits.max} times.`
+ );
+ }
+ delete knownProblematicPrefs[pref];
+ }
+ }
+
+ // This pref will be accessed by mozJSComponentLoader when loading modules,
+ // which fails TV runs since they run the test multiple times without restarting.
+ // We just ignore this pref, since it's for testing only anyway.
+ if (knownProblematicPrefs["browser.startup.record"]) {
+ delete knownProblematicPrefs["browser.startup.record"];
+ }
+
+ let unusedPrefs = Object.keys(knownProblematicPrefs);
+ is(
+ unusedPrefs.length,
+ 0,
+ `Should have accessed all known problematic prefs. Remaining: ${unusedPrefs}`
+ );
+}
+
+/**
+ * A helper function to read preference access data
+ * using the Services.prefs.readStats() function.
+ */
+function getPreferenceStats() {
+ let stats = {};
+ Services.prefs.readStats((key, value) => (stats[key] = value));
+ return stats;
+}
+
+add_task(async function debug_only() {
+ ok(AppConstants.DEBUG, "You need to run this test on a debug build.");
+});
+
+// Just checks how many prefs were accessed during startup.
+add_task(async function startup() {
+ let max = 40;
+
+ let knownProblematicPrefs = {
+ "browser.startup.record": {
+ // This pref is accessed in Nighly and debug builds only.
+ min: 200,
+ max: 400,
+ },
+ "network.loadinfo.skip_type_assertion": {
+ // This is accessed in debug only.
+ },
+ "chrome.override_package.global": {
+ min: 0,
+ max: 50,
+ },
+ };
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ ok(startupRecorder.data.prefStats, "startupRecorder has prefStats");
+
+ checkPrefGetters(startupRecorder.data.prefStats, max, knownProblematicPrefs);
+});
+
+// This opens 10 tabs and checks pref getters.
+add_task(async function open_10_tabs() {
+ // This is somewhat arbitrary. When we had a default of 4 content processes
+ // the value was 15. We need to scale it as we increase the number of
+ // content processes so we approximate with 4 * process_count.
+ const max = 4 * DEFAULT_PROCESS_COUNT;
+
+ let knownProblematicPrefs = {
+ "browser.startup.record": {
+ max: 20,
+ },
+ "browser.tabs.remote.logSwitchTiming": {
+ max: 35,
+ },
+ "network.loadinfo.skip_type_assertion": {
+ // This is accessed in debug only.
+ },
+ };
+
+ Services.prefs.resetStats();
+
+ let tabs = [];
+ while (tabs.length < 10) {
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true,
+ true
+ )
+ );
+ }
+
+ for (let tab of tabs) {
+ await BrowserTestUtils.removeTab(tab);
+ }
+
+ checkPrefGetters(getPreferenceStats(), max, knownProblematicPrefs);
+});
+
+// This navigates to 50 sites and checks pref getters.
+add_task(async function navigate_around() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable bfcache so that we can measure more accurately the number of
+ // pref accesses in the child processes.
+ // If bfcache is enabled on Fission
+ // dom.ipc.keepProcessesAlive.webIsolated.perOrigin and
+ // security.sandbox.content.force-namespace are accessed only a couple of
+ // times.
+ ["browser.sessionhistory.max_total_viewers", 0],
+ ],
+ });
+
+ let max = 40;
+
+ let knownProblematicPrefs = {
+ "network.loadinfo.skip_type_assertion": {
+ // This is accessed in debug only.
+ },
+ };
+
+ if (Services.prefs.getBoolPref("browser.translations.enable")) {
+ // The translations pref logs the translation decision on each DOMContentLoaded,
+ // and only shows the log by the preferences set in the console.createInstance.
+ // See Bug 1835693. This means that it is invoked on each page load.
+ knownProblematicPrefs["browser.translations.logLevel"] = {
+ min: 50,
+ max: 50,
+ };
+ }
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // We access this when considering starting a new content process.
+ // Because there is no complete list of content process types,
+ // caching this is not trivial. Opening 50 different content
+ // processes and throwing them away immediately is a bit artificial;
+ // we're more likely to keep some around so this shouldn't be quite
+ // this bad in practice. Fixing this is
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1600266
+ knownProblematicPrefs["dom.ipc.processCount.webIsolated"] = {
+ min: 50,
+ max: 51,
+ };
+ // This pref is only accessed in automation to speed up tests.
+ knownProblematicPrefs["dom.ipc.keepProcessesAlive.webIsolated.perOrigin"] =
+ {
+ min: 100,
+ max: 102,
+ };
+ if (AppConstants.platform == "linux") {
+ // The following sandbox pref is covered by
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1600189
+ knownProblematicPrefs["security.sandbox.content.force-namespace"] = {
+ min: 45,
+ max: 55,
+ };
+ } else if (AppConstants.platform == "win") {
+ // The following 2 graphics prefs are covered by
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1639497
+ knownProblematicPrefs["gfx.canvas.azure.backends"] = {
+ min: 90,
+ max: 110,
+ };
+ knownProblematicPrefs["gfx.content.azure.backends"] = {
+ min: 90,
+ max: 110,
+ };
+ // The following 2 sandbox prefs are covered by
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1639494
+ knownProblematicPrefs["security.sandbox.content.read_path_whitelist"] = {
+ min: 47,
+ max: 55,
+ };
+ knownProblematicPrefs["security.sandbox.logging.enabled"] = {
+ min: 47,
+ max: 55,
+ };
+ }
+ }
+
+ Services.prefs.resetStats();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true,
+ true
+ );
+
+ let urls = [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/",
+ "https://example.com/",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/",
+ "https://example.org/",
+ ];
+
+ for (let i = 0; i < 50; i++) {
+ let url = urls[i % urls.length];
+ info(`Navigating to ${url}...`);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
+ info(`Loaded ${url}.`);
+ }
+
+ await BrowserTestUtils.removeTab(tab);
+
+ checkPrefGetters(getPreferenceStats(), max, knownProblematicPrefs);
+});
diff --git a/browser/base/content/test/performance/browser_startup.js b/browser/base/content/test/performance/browser_startup.js
new file mode 100644
index 0000000000..83c949456c
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records at which phase of startup the JS modules are first
+ * loaded.
+ * If you made changes that cause this test to fail, it's likely because you
+ * are loading more JS code during startup.
+ * Most code has no reason to run off of the app-startup notification
+ * (this is very early, before we have selected the user profile, so
+ * preferences aren't accessible yet).
+ * If your code isn't strictly required to show the first browser window,
+ * it shouldn't be loaded before we are done with first paint.
+ * Finally, if your code isn't really needed during startup, it should not be
+ * loaded before we have started handling user events.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+const startupPhases = {
+ // For app-startup, we have an allowlist of acceptable JS files.
+ // Anything loaded during app-startup must have a compelling reason
+ // to run before we have even selected the user profile.
+ // Consider loading your code after first paint instead,
+ // eg. from BrowserGlue.sys.mjs' _onFirstWindowLoaded method).
+ "before profile selection": {
+ allowlist: {
+ modules: new Set([
+ "resource:///modules/BrowserGlue.sys.mjs",
+ "resource:///modules/StartupRecorder.sys.mjs",
+ "resource://gre/modules/AppConstants.sys.mjs",
+ "resource://gre/modules/ActorManagerParent.sys.mjs",
+ "resource://gre/modules/CustomElementsListener.sys.mjs",
+ "resource://gre/modules/MainProcessSingleton.sys.mjs",
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+ ]),
+ },
+ },
+
+ // For the following phases of startup we have only a list of files that
+ // are **not** allowed to load in this phase, as too many other scripts
+ // load during this time.
+
+ // We are at this phase after creating the first browser window (ie. after final-ui-startup).
+ "before opening first browser window": {
+ denylist: {
+ modules: new Set([]),
+ },
+ },
+
+ // We reach this phase right after showing the first browser window.
+ // This means that anything already loaded at this point has been loaded
+ // before first paint and delayed it.
+ "before first paint": {
+ denylist: {
+ modules: new Set([
+ "resource:///modules/AboutNewTab.jsm",
+ "resource:///modules/BrowserUsageTelemetry.jsm",
+ "resource:///modules/ContentCrashHandlers.jsm",
+ "resource:///modules/ShellService.sys.mjs",
+ "resource://gre/modules/NewTabUtils.sys.mjs",
+ "resource://gre/modules/PageThumbs.sys.mjs",
+ "resource://gre/modules/PlacesUtils.sys.mjs",
+ "resource://gre/modules/Preferences.sys.mjs",
+ "resource://gre/modules/SearchService.sys.mjs",
+ "resource://gre/modules/Sqlite.sys.mjs",
+ ]),
+ services: new Set(["@mozilla.org/browser/search-service;1"]),
+ },
+ },
+
+ // We are at this phase once we are ready to handle user events.
+ // Anything loaded at this phase or before gets in the way of the user
+ // interacting with the first browser window.
+ "before handling user events": {
+ denylist: {
+ modules: new Set([
+ "resource://gre/modules/Blocklist.sys.mjs",
+ // Bug 1391495 - BrowserWindowTracker.jsm is intermittently used.
+ // "resource:///modules/BrowserWindowTracker.jsm",
+ "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
+ "resource://gre/modules/Bookmarks.sys.mjs",
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+ "resource://gre/modules/FxAccounts.sys.mjs",
+ "resource://gre/modules/FxAccountsStorage.sys.mjs",
+ "resource://gre/modules/PlacesBackups.sys.mjs",
+ "resource://gre/modules/PlacesExpiration.sys.mjs",
+ "resource://gre/modules/PlacesSyncUtils.sys.mjs",
+ "resource://gre/modules/PushComponents.sys.mjs",
+ ]),
+ services: new Set(["@mozilla.org/browser/nav-bookmarks-service;1"]),
+ },
+ },
+
+ // Things that are expected to be completely out of the startup path
+ // and loaded lazily when used for the first time by the user should
+ // be listed here.
+ "before becoming idle": {
+ denylist: {
+ modules: new Set([
+ "resource://gre/modules/AsyncPrefs.sys.mjs",
+ "resource://gre/modules/LoginManagerContextMenu.sys.mjs",
+ "resource://pdf.js/PdfStreamConverter.sys.mjs",
+ ]),
+ },
+ },
+};
+
+if (
+ Services.prefs.getBoolPref("browser.startup.blankWindow") &&
+ Services.prefs.getCharPref(
+ "extensions.activeThemeID",
+ "default-theme@mozilla.org"
+ ) == "default-theme@mozilla.org"
+) {
+ startupPhases["before profile selection"].allowlist.modules.add(
+ "resource://gre/modules/XULStore.sys.mjs"
+ );
+}
+
+if (AppConstants.MOZ_CRASHREPORTER) {
+ startupPhases["before handling user events"].denylist.modules.add(
+ "resource://gre/modules/CrashSubmit.sys.mjs"
+ );
+}
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ let data = Cu.cloneInto(startupRecorder.data.code, {});
+ function getStack(scriptType, name) {
+ if (scriptType == "modules") {
+ return Cu.getModuleImportStack(name);
+ }
+ return "";
+ }
+
+ // This block only adds debug output to help find the next bugs to file,
+ // it doesn't contribute to the actual test.
+ SimpleTest.requestCompleteLog();
+ let previous;
+ for (let phase in data) {
+ for (let scriptType in data[phase]) {
+ for (let f of data[phase][scriptType]) {
+ // phases are ordered, so if a script wasn't loaded yet at the immediate
+ // previous phase, it wasn't loaded during any of the previous phases
+ // either, and is new in the current phase.
+ if (!previous || !data[previous][scriptType].includes(f)) {
+ info(`${scriptType} loaded ${phase}: ${f}`);
+ if (kDumpAllStacks) {
+ info(getStack(scriptType, f));
+ }
+ }
+ }
+ }
+ previous = phase;
+ }
+
+ for (let phase in startupPhases) {
+ let loadedList = data[phase];
+ let allowlist = startupPhases[phase].allowlist || null;
+ if (allowlist) {
+ for (let scriptType in allowlist) {
+ loadedList[scriptType] = loadedList[scriptType].filter(c => {
+ if (!allowlist[scriptType].has(c)) {
+ return true;
+ }
+ allowlist[scriptType].delete(c);
+ return false;
+ });
+ is(
+ loadedList[scriptType].length,
+ 0,
+ `should have no unexpected ${scriptType} loaded ${phase}`
+ );
+ for (let script of loadedList[scriptType]) {
+ let message = `unexpected ${scriptType}: ${script}`;
+ record(false, message, undefined, getStack(scriptType, script));
+ }
+ is(
+ allowlist[scriptType].size,
+ 0,
+ `all ${scriptType} allowlist entries should have been used`
+ );
+ for (let script of allowlist[scriptType]) {
+ ok(false, `unused ${scriptType} allowlist entry: ${script}`);
+ }
+ }
+ }
+ let denylist = startupPhases[phase].denylist || null;
+ if (denylist) {
+ for (let scriptType in denylist) {
+ for (let file of denylist[scriptType]) {
+ let loaded = loadedList[scriptType].includes(file);
+ let message = `${file} is not allowed ${phase}`;
+ if (!loaded) {
+ ok(true, message);
+ } else {
+ record(false, message, undefined, getStack(scriptType, file));
+ }
+ }
+ }
+
+ if (denylist.modules) {
+ let results = await PerfTestHelpers.throttledMapPromises(
+ denylist.modules,
+ async uri => ({
+ uri,
+ exists: await PerfTestHelpers.checkURIExists(uri),
+ })
+ );
+
+ for (let { uri, exists } of results) {
+ ok(exists, `denylist entry ${uri} for phase "${phase}" must exist`);
+ }
+ }
+
+ if (denylist.services) {
+ for (let contract of denylist.services) {
+ ok(
+ contract in Cc,
+ `denylist entry ${contract} for phase "${phase}" must exist`
+ );
+ }
+ }
+ }
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_content.js b/browser/base/content/test/performance/browser_startup_content.js
new file mode 100644
index 0000000000..330f9b7655
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records which services, frame scripts, process scripts, and
+ * JS modules are loaded when creating a new content process.
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are loading more JS code during content process startup. Please try to
+ * avoid this.
+ *
+ * If your code isn't strictly required to show a page, consider loading it
+ * lazily. If you can't, consider delaying its load until after we have started
+ * handling user events.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+const known_scripts = {
+ modules: new Set([
+ "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs",
+
+ // General utilities
+ "resource://gre/modules/AppConstants.sys.mjs",
+ "resource://gre/modules/Timer.sys.mjs",
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+
+ // Logging related
+ "resource://gre/modules/Log.sys.mjs",
+
+ // Browser front-end
+ "resource:///actors/AboutReaderChild.sys.mjs",
+ "resource:///actors/LinkHandlerChild.sys.mjs",
+ "resource:///actors/SearchSERPTelemetryChild.sys.mjs",
+ "resource://gre/actors/ContentMetaChild.sys.mjs",
+ "resource://gre/modules/Readerable.sys.mjs",
+
+ // Telemetry
+ "resource://gre/modules/TelemetryControllerBase.sys.mjs", // bug 1470339
+ "resource://gre/modules/TelemetryControllerContent.sys.mjs", // bug 1470339
+
+ // Extensions
+ "resource://gre/modules/ExtensionProcessScript.sys.mjs",
+ "resource://gre/modules/ExtensionUtils.sys.mjs",
+ ]),
+ frameScripts: new Set([
+ // Test related
+ "chrome://mochikit/content/shutdown-leaks-collector.js",
+ ]),
+ processScripts: new Set([
+ "chrome://global/content/process-content.js",
+ "resource://gre/modules/extensionProcessScriptLoader.js",
+ ]),
+};
+
+if (!Services.appinfo.sessionHistoryInParent) {
+ known_scripts.modules.add(
+ "resource:///modules/sessionstore/ContentSessionStore.sys.mjs"
+ );
+}
+
+if (AppConstants.NIGHTLY_BUILD) {
+ // Browser front-end.
+ known_scripts.modules.add("resource:///actors/InteractionsChild.sys.mjs");
+}
+
+// Items on this list *might* load when creating the process, as opposed to
+// items in the main list, which we expect will always load.
+const intermittently_loaded_scripts = {
+ modules: new Set([
+ "resource://gre/modules/nsAsyncShutdown.sys.mjs",
+ "resource://gre/modules/sessionstore/Utils.sys.mjs",
+
+ // Translations code which may be preffed on.
+ "resource://gre/actors/TranslationsChild.sys.mjs",
+ "resource://gre/modules/ConsoleAPIStorage.sys.mjs", // Logging related.
+
+ // Session store.
+ "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
+
+ // Webcompat about:config front-end. This is part of a system add-on which
+ // may not load early enough for the test.
+ "resource://webcompat/AboutCompat.jsm",
+
+ // Cookie banner handling.
+ "resource://gre/actors/CookieBannerChild.sys.mjs",
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+
+ // Test related
+ "chrome://remote/content/marionette/actors/MarionetteEventsChild.sys.mjs",
+ "chrome://remote/content/shared/Log.sys.mjs",
+ "resource://testing-common/BrowserTestUtilsChild.sys.mjs",
+ "resource://testing-common/ContentEventListenerChild.sys.mjs",
+ "resource://specialpowers/AppTestDelegateChild.sys.mjs",
+ "resource://testing-common/SpecialPowersChild.sys.mjs",
+ "resource://testing-common/WrapPrivileged.sys.mjs",
+ ]),
+ frameScripts: new Set([]),
+ processScripts: new Set([
+ // Webcompat about:config front-end. This is presently nightly-only and
+ // part of a system add-on which may not load early enough for the test.
+ "resource://webcompat/aboutPageProcessScript.js",
+ ]),
+};
+
+const forbiddenScripts = {
+ services: new Set([
+ "@mozilla.org/base/telemetry-startup;1",
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1",
+ "@mozilla.org/push/Service;1",
+ ]),
+};
+
+add_task(async function () {
+ SimpleTest.requestCompleteLog();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url:
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_empty.html",
+ forceNewProcess: true,
+ });
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let promise = BrowserTestUtils.waitForMessage(mm, "Test:LoadedScripts");
+
+ // Load a custom frame script to avoid using ContentTask which loads Task.jsm
+ mm.loadFrameScript(
+ "data:text/javascript,(" +
+ function () {
+ /* eslint-env mozilla/frame-script */
+ const Cm = Components.manager;
+ Cm.QueryInterface(Ci.nsIServiceManager);
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ let collectStacks = AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG;
+ let modules = {};
+ for (let module of Cu.loadedJSModules) {
+ modules[module] = collectStacks
+ ? Cu.getModuleImportStack(module)
+ : "";
+ }
+ for (let module of Cu.loadedESModules) {
+ modules[module] = collectStacks
+ ? Cu.getModuleImportStack(module)
+ : "";
+ }
+ let services = {};
+ for (let contractID of Object.keys(Cc)) {
+ try {
+ if (
+ Cm.isServiceInstantiatedByContractID(contractID, Ci.nsISupports)
+ ) {
+ services[contractID] = "";
+ }
+ } catch (e) {}
+ }
+ sendAsyncMessage("Test:LoadedScripts", {
+ modules,
+ services,
+ });
+ } +
+ ")()",
+ false
+ );
+
+ let loadedInfo = await promise;
+
+ // Gather loaded frame scripts.
+ loadedInfo.frameScripts = {};
+ for (let [uri] of Services.mm.getDelayedFrameScripts()) {
+ loadedInfo.frameScripts[uri] = "";
+ }
+
+ // Gather loaded process scripts.
+ loadedInfo.processScripts = {};
+ for (let [uri] of Services.ppmm.getDelayedProcessScripts()) {
+ loadedInfo.processScripts[uri] = "";
+ }
+
+ await checkLoadedScripts({
+ loadedInfo,
+ known: known_scripts,
+ intermittent: intermittently_loaded_scripts,
+ forbidden: forbiddenScripts,
+ dumpAllStacks: kDumpAllStacks,
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/performance/browser_startup_content_mainthreadio.js b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js
new file mode 100644
index 0000000000..bf200b940d
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js
@@ -0,0 +1,438 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records I/O syscalls done on the main thread during startup.
+ *
+ * To run this test similar to try server, you need to run:
+ * ./mach package
+ * ./mach test --appname=dist <path to test>
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are touching more files or directories during startup.
+ * Most code has no reason to use main thread I/O.
+ * If for some reason accessing the file system on the main thread is currently
+ * unavoidable, consider defering the I/O as long as you can, ideally after
+ * the end of startup.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+
+/* This is an object mapping string process types to lists of known cases
+ * of IO happening on the main thread. Ideally, IO should not be on the main
+ * thread, and should happen as late as possible (see above).
+ *
+ * Paths in the entries in these lists can:
+ * - be a full path, eg. "/etc/mime.types"
+ * - have a prefix which will be resolved using Services.dirsvc
+ * eg. "GreD:omni.ja"
+ * It's possible to have only a prefix, in thise case the directory will
+ * still be resolved, eg. "UAppData:"
+ * - use * at the begining and/or end as a wildcard
+ * The folder separator is '/' even for Windows paths, where it'll be
+ * automatically converted to '\'.
+ *
+ * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
+ * without this the test is strict and will fail if the described IO does not
+ * happen.
+ *
+ * Each entry specifies the maximum number of times an operation is expected to
+ * occur.
+ * The operations currently reported by the I/O interposer are:
+ * create/open: only supported on Windows currently. The test currently
+ * ignores these markers to have a shorter initial list of IO operations.
+ * Adding Unix support is bug 1533779.
+ * stat: supported on all platforms when checking the last modified date or
+ * file size. Supported only on Windows when checking if a file exists;
+ * fixing this inconsistency is bug 1536109.
+ * read: supported on all platforms, but unix platforms will only report read
+ * calls going through NSPR.
+ * write: supported on all platforms, but Linux will only report write calls
+ * going through NSPR.
+ * close: supported only on Unix, and only for close calls going through NSPR.
+ * Adding Windows support is bug 1524574.
+ * fsync: supported only on Windows.
+ *
+ * If an entry specifies more than one operation, if at least one of them is
+ * encountered, the test won't report a failure for the entry if other
+ * operations are not encountered. This helps when listing cases where the
+ * reported operations aren't the same on all platforms due to the I/O
+ * interposer inconsistencies across platforms documented above.
+ */
+const processes = {
+ "Web Content": [
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1357205
+ path: "XREAppFeat:formautofill@mozilla.org.xpi",
+ condition: !WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ },
+ {
+ path: "*ShaderCache*", // Bug 1660480 - seen on hardware
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 3,
+ },
+ ],
+ "Privileged Content": [
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+ WebExtensions: [
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+};
+
+function expandPathWithDirServiceKey(path) {
+ if (path.includes(":")) {
+ let [prefix, suffix] = path.split(":");
+ let [key, property] = prefix.split(".");
+ let dir = Services.dirsvc.get(key, Ci.nsIFile);
+ if (property) {
+ dir = dir[property];
+ }
+
+ // Resolve symLinks.
+ let dirPath = dir.path;
+ while (dir && !dir.isSymlink()) {
+ dir = dir.parent;
+ }
+ if (dir) {
+ dirPath = dirPath.replace(dir.path, dir.target);
+ }
+
+ path = dirPath;
+
+ if (suffix) {
+ path += "/" + suffix;
+ }
+ }
+ if (AppConstants.platform == "win") {
+ path = path.replace(/\//g, "\\");
+ }
+ return path;
+}
+
+function getStackFromProfile(profile, stack) {
+ const stackPrefixCol = profile.stackTable.schema.prefix;
+ const stackFrameCol = profile.stackTable.schema.frame;
+ const frameLocationCol = profile.frameTable.schema.location;
+
+ let result = [];
+ while (stack) {
+ let sp = profile.stackTable.data[stack];
+ let frame = profile.frameTable.data[sp[stackFrameCol]];
+ stack = sp[stackPrefixCol];
+ frame = profile.stringTable[frame[frameLocationCol]];
+ if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
+ result.push(frame);
+ }
+ }
+ return result;
+}
+
+function getIOMarkersFromProfile(profile) {
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+
+ let markers = [];
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+
+ if (markerName != "FileIO") {
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (markerData.source == "sqlite-mainthread") {
+ continue;
+ }
+
+ let samples = markerData.stack.samples;
+ let stack = samples.data[0][samples.schema.stack];
+ markers.push({
+ operation: markerData.operation,
+ filename: markerData.filename,
+ source: markerData.source,
+ stackId: stack,
+ });
+ }
+
+ return markers;
+}
+
+function pathMatches(path, filename) {
+ path = path.toLowerCase();
+ return (
+ path == filename || // Full match
+ // Wildcard on both sides of the path
+ (path.startsWith("*") &&
+ path.endsWith("*") &&
+ filename.includes(path.slice(1, -1))) ||
+ // Wildcard suffix
+ (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
+ // Wildcard prefix
+ (path.startsWith("*") && filename.endsWith(path.slice(1)))
+ );
+}
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ TestUtils.assertPackagedBuild();
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ for (let process in processes) {
+ processes[process] = processes[process].filter(
+ entry => !("condition" in entry) || entry.condition
+ );
+ processes[process].forEach(entry => {
+ entry.listedPath = entry.path;
+ entry.path = expandPathWithDirServiceKey(entry.path);
+ });
+ }
+
+ let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase();
+ let shouldPass = true;
+ for (let procName in processes) {
+ let knownIOList = processes[procName];
+ info(
+ `known main thread IO paths for ${procName} process:\n` +
+ knownIOList
+ .map(e => {
+ let operations = Object.keys(e)
+ .filter(k => !["path", "condition"].includes(k))
+ .map(k => `${k}: ${e[k]}`);
+ return ` ${e.path} - ${operations.join(", ")}`;
+ })
+ .join("\n")
+ );
+
+ let profile;
+ for (let process of startupRecorder.data.profile.processes) {
+ if (process.threads[0].processName == procName) {
+ profile = process.threads[0];
+ break;
+ }
+ }
+ if (procName == "Privileged Content" && !profile) {
+ // The Privileged Content is started from an idle task that may not have
+ // been executed yet at the time we captured the startup profile in
+ // startupRecorder.
+ todo(false, `profile for ${procName} process not found`);
+ } else {
+ ok(profile, `Found profile for ${procName} process`);
+ }
+ if (!profile) {
+ continue;
+ }
+
+ let markers = getIOMarkersFromProfile(profile);
+ for (let marker of markers) {
+ if (marker.operation == "create/open") {
+ // TODO: handle these I/O markers once they are supported on
+ // non-Windows platforms.
+ continue;
+ }
+
+ if (!marker.filename) {
+ // We are still missing the filename on some mainthreadio markers,
+ // these markers are currently useless for the purpose of this test.
+ continue;
+ }
+
+ // Convert to lower case before comparing because the OS X test machines
+ // have the 'Firefox' folder in 'Library/Application Support' created
+ // as 'firefox' for some reason.
+ let filename = marker.filename.toLowerCase();
+
+ if (!WIN && filename == "/dev/urandom") {
+ continue;
+ }
+
+ // /dev/shm is always tmpfs (a memory filesystem); this isn't
+ // really I/O any more than mmap/munmap are.
+ if (LINUX && filename.startsWith("/dev/shm/")) {
+ continue;
+ }
+
+ // "Files" from memfd_create() are similar to tmpfs but never
+ // exist in the filesystem; however, they have names which are
+ // exposed in procfs, and the I/O interposer observes when
+ // they're close()d.
+ if (LINUX && filename.startsWith("/memfd:")) {
+ continue;
+ }
+
+ // Shared memory uses temporary files on MacOS <= 10.11 to avoid
+ // a kernel security bug that will never be patched (see
+ // https://crbug.com/project-zero/1671 for details). This can
+ // be removed when we no longer support those OS versions.
+ if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
+ continue;
+ }
+
+ let expected = false;
+ for (let entry of knownIOList) {
+ if (pathMatches(entry.path, filename)) {
+ entry[marker.operation] = (entry[marker.operation] || 0) - 1;
+ entry._used = true;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ record(
+ false,
+ `unexpected ${marker.operation} on ${marker.filename} in ${procName} process`,
+ undefined,
+ " " + getStackFromProfile(profile, marker.stackId).join("\n ")
+ );
+ shouldPass = false;
+ }
+ info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
+ if (kDumpAllStacks) {
+ info(
+ getStackFromProfile(profile, marker.stackId)
+ .map(f => " " + f)
+ .join("\n")
+ );
+ }
+ }
+
+ if (!knownIOList.length) {
+ continue;
+ }
+ // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have
+ // no I/O marker in that case, but it's good to keep the test running to check
+ // that we are still able to produce startup profiles.
+ is(
+ !!markers.length,
+ !AppConstants.RELEASE_OR_BETA,
+ procName +
+ " startup profiles should have IO markers in builds that are not RELEASE_OR_BETA"
+ );
+ if (!markers.length) {
+ // If a profile unexpectedly contains no I/O marker, it's better to return
+ // early to avoid having a lot of of confusing "no main thread IO when we
+ // expected some" failures.
+ continue;
+ }
+
+ for (let entry of knownIOList) {
+ for (let op in entry) {
+ if (
+ [
+ "listedPath",
+ "path",
+ "condition",
+ "ignoreIfUnused",
+ "_used",
+ ].includes(op)
+ ) {
+ continue;
+ }
+ let message = `${op} on ${entry.path} `;
+ if (entry[op] == 0) {
+ message += "as many times as expected";
+ } else if (entry[op] > 0) {
+ message += `allowed ${entry[op]} more times`;
+ } else {
+ message += `${entry[op] * -1} more times than expected`;
+ }
+ ok(entry[op] >= 0, `${message} in ${procName} process`);
+ }
+ if (!("_used" in entry) && !entry.ignoreIfUnused) {
+ ok(
+ false,
+ `no main thread IO when we expected some for process ${procName}: ${entry.path} (${entry.listedPath})`
+ );
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected main thread I/O during startup");
+ } else {
+ const filename = "profile_startup_content_mainthreadio.json";
+ let path = Services.env.get("MOZ_UPLOAD_DIR");
+ let profilePath = PathUtils.join(path, filename);
+ await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
+ ok(
+ false,
+ "Unexpected main thread I/O behavior during child process startup; " +
+ `open the ${filename} artifact in the Firefox Profiler to see what happened`
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_content_subframe.js b/browser/base/content/test/performance/browser_startup_content_subframe.js
new file mode 100644
index 0000000000..204d0ac1ba
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_subframe.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records which services, JS components, frame scripts, process
+ * scripts, and JS modules are loaded when creating a new content process for a
+ * subframe.
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are loading more JS code during content process startup. Please try to
+ * avoid this.
+ *
+ * If your code isn't strictly required to show an iframe, consider loading it
+ * lazily. If you can't, consider delaying its load until after we have started
+ * handling user events.
+ *
+ * This test differs from browser_startup_content.js in that it tests a process
+ * with no toplevel browsers opened, but with a single subframe document
+ * loaded. This leads to a different set of scripts being loaded.
+ */
+
+"use strict";
+
+const actorModuleURI =
+ getRootDirectory(gTestPath) + "StartupContentSubframe.sys.mjs";
+const subframeURI =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_empty.html";
+
+// Set this to true only for debugging purpose; it makes the output noisy.
+const kDumpAllStacks = false;
+
+const known_scripts = {
+ modules: new Set([
+ // Loaded by this test
+ actorModuleURI,
+
+ // General utilities
+ "resource://gre/modules/AppConstants.sys.mjs",
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+
+ // Logging related
+ "resource://gre/modules/Log.sys.mjs",
+
+ // Telemetry
+ "resource://gre/modules/TelemetryControllerBase.sys.mjs", // bug 1470339
+ "resource://gre/modules/TelemetryControllerContent.sys.mjs", // bug 1470339
+
+ // Extensions
+ "resource://gre/modules/ExtensionProcessScript.sys.mjs",
+ "resource://gre/modules/ExtensionUtils.sys.mjs",
+ ]),
+ processScripts: new Set([
+ "chrome://global/content/process-content.js",
+ "resource://gre/modules/extensionProcessScriptLoader.js",
+ ]),
+};
+
+// Items on this list *might* load when creating the process, as opposed to
+// items in the main list, which we expect will always load.
+const intermittently_loaded_scripts = {
+ modules: new Set([
+ "resource://gre/modules/nsAsyncShutdown.sys.mjs",
+
+ // Cookie banner handling.
+ "resource://gre/actors/CookieBannerChild.sys.mjs",
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+
+ // Test related
+ "chrome://remote/content/marionette/actors/MarionetteEventsChild.sys.mjs",
+ "chrome://remote/content/shared/Log.sys.mjs",
+ "resource://testing-common/BrowserTestUtilsChild.sys.mjs",
+ "resource://testing-common/ContentEventListenerChild.sys.mjs",
+ "resource://testing-common/SpecialPowersChild.sys.mjs",
+ "resource://specialpowers/AppTestDelegateChild.sys.mjs",
+ "resource://testing-common/WrapPrivileged.sys.mjs",
+ ]),
+ processScripts: new Set([]),
+};
+
+const forbiddenScripts = {
+ services: new Set([
+ "@mozilla.org/base/telemetry-startup;1",
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1",
+ "@mozilla.org/push/Service;1",
+ ]),
+};
+
+add_task(async function () {
+ SimpleTest.requestCompleteLog();
+
+ // Increase the maximum number of webIsolated content processes to make sure
+ // our newly-created iframe is spawned into a new content process.
+ //
+ // Unfortunately, we don't have something like `forceNewProcess` for subframe
+ // loads.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount.webIsolated", 10]],
+ });
+ Services.ppmm.releaseCachedProcesses();
+
+ // Register a custom window actor which will send us a notification when the
+ // script loading information is available.
+ ChromeUtils.registerWindowActor("StartupContentSubframe", {
+ parent: {
+ esModuleURI: actorModuleURI,
+ },
+ child: {
+ esModuleURI: actorModuleURI,
+ events: {
+ load: { mozSystemGroup: true, capture: true },
+ },
+ },
+ matches: [subframeURI],
+ allFrames: true,
+ });
+
+ // Create a tab, and load a remote subframe with the specific URI in it.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ SpecialPowers.spawn(tab.linkedBrowser, [subframeURI], uri => {
+ let iframe = content.document.createElement("iframe");
+ iframe.src = uri;
+ content.document.body.appendChild(iframe);
+ });
+
+ // Wait for the reply to come in, remove the XPCOM wrapper, and unregister our actor.
+ let [subject] = await TestUtils.topicObserved(
+ "startup-content-subframe-loaded-scripts"
+ );
+ let loadedInfo = subject.wrappedJSObject;
+
+ ChromeUtils.unregisterWindowActor("StartupContentSubframe");
+ BrowserTestUtils.removeTab(tab);
+
+ // Gather loaded process scripts.
+ loadedInfo.processScripts = {};
+ for (let [uri] of Services.ppmm.getDelayedProcessScripts()) {
+ loadedInfo.processScripts[uri] = "";
+ }
+
+ await checkLoadedScripts({
+ loadedInfo,
+ known: known_scripts,
+ intermittent: intermittently_loaded_scripts,
+ forbidden: forbiddenScripts,
+ dumpAllStacks: kDumpAllStacks,
+ });
+});
diff --git a/browser/base/content/test/performance/browser_startup_flicker.js b/browser/base/content/test/performance/browser_startup_flicker.js
new file mode 100644
index 0000000000..8279a0a601
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_flicker.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * This test ensures that there is no unexpected flicker
+ * on the first window opened during startup.
+ */
+
+add_task(async function () {
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ // Ensure all the frame data is in the test compartment to avoid traversing
+ // a cross compartment wrapper for each pixel.
+ let frames = Cu.cloneInto(startupRecorder.data.frames, {});
+ ok(!!frames.length, "Should have captured some frames.");
+
+ let unexpectedRects = 0;
+ let alreadyFocused = false;
+ for (let i = 1; i < frames.length; ++i) {
+ let frame = frames[i],
+ previousFrame = frames[i - 1];
+ let rects = compareFrames(frame, previousFrame);
+
+ if (!alreadyFocused) {
+ // The first screenshot we get shows an unfocused browser window for some
+ // reason. See bug 1445161.
+ //
+ // We'll assume the changes we are seeing are due to this focus change if
+ // there are at least 5 areas that changed near the top of the screen,
+ // but will only ignore this once (hence the alreadyFocused variable).
+ //
+ // On Linux we expect just one rect because we don't draw titlebar
+ // buttons in the tab bar, so we just get a whole-tab-bar color-switch.
+ const minRects = AppConstants.platform == "linux" ? 0 : 5;
+ if (rects.length > minRects && rects.every(r => r.y2 < 100)) {
+ alreadyFocused = true;
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ continue;
+ }
+ }
+
+ rects = rects.filter(rect => {
+ let width = frame.width;
+
+ let exceptions = [
+ /**
+ * Please don't add anything new unless justified!
+ */
+ ];
+
+ let rectText = `${rect.toSource()}, window width: ${width}`;
+ for (let e of exceptions) {
+ if (e.condition(rect)) {
+ todo(false, e.name + ", " + rectText);
+ return false;
+ }
+ }
+
+ ok(false, "unexpected changed rect: " + rectText);
+ return true;
+ });
+ if (!rects.length) {
+ info("ignoring identical frame");
+ continue;
+ }
+
+ // Before dumping a frame with unexpected differences for the first time,
+ // ensure at least one previous frame has been logged so that it's possible
+ // to see the differences when examining the log.
+ if (!unexpectedRects) {
+ dumpFrame(previousFrame);
+ }
+ unexpectedRects += rects.length;
+ dumpFrame(frame);
+ }
+ is(unexpectedRects, 0, "should have 0 unknown flickering areas");
+});
diff --git a/browser/base/content/test/performance/browser_startup_hiddenwindow.js b/browser/base/content/test/performance/browser_startup_hiddenwindow.js
new file mode 100644
index 0000000000..7ad611be94
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_hiddenwindow.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ let extras = Cu.cloneInto(startupRecorder.data.extras, {});
+
+ let phasesExpectations = {
+ "before profile selection": false,
+ "before opening first browser window": false,
+ "before first paint": !Services.prefs.getBoolPref(
+ "toolkit.lazyHiddenWindow"
+ ),
+
+ // Bug 1531854
+ "before handling user events": true,
+ "before becoming idle": true,
+ };
+
+ for (let phase in extras) {
+ if (!(phase in phasesExpectations)) {
+ ok(false, `Startup phase '${phase}' should be specified.`);
+ continue;
+ }
+
+ is(
+ extras[phase].hiddenWindowLoaded,
+ phasesExpectations[phase],
+ `Hidden window loaded at '${phase}': ${phasesExpectations[phase]}`
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_images.js b/browser/base/content/test/performance/browser_startup_images.js
new file mode 100644
index 0000000000..5a27b9a8dd
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_images.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test checks that any images we load on startup are actually used,
+ * so we don't waste IO and cycles loading images the user doesn't see.
+ * It has a list of known problematic images that we aim to reduce to
+ * empty.
+ */
+
+/* A list of images that are loaded at startup but not shown.
+ * List items support the following attributes:
+ * - file: The location of the loaded image file.
+ * - hidpi: An alternative hidpi file location for retina screens, if one exists.
+ * May be the magic string <not loaded> in strange cases where
+ * only the low-resolution image is loaded but not shown.
+ * - platforms: An array of the platforms where the issue is occurring.
+ * Possible values are linux, win, macosx.
+ * - intermittentNotLoaded: an array of platforms where this image is
+ * intermittently not loaded, e.g. because it is
+ * loaded during the time we stop recording.
+ * - intermittentShown: An array of platforms where this image is
+ * intermittently shown, even though the list implies
+ * it might not be shown.
+ *
+ * PLEASE do not add items to this list.
+ *
+ * PLEASE DO remove items from this list.
+ */
+const knownUnshownImages = [
+ {
+ file: "chrome://global/skin/icons/arrow-left.svg",
+ platforms: ["linux", "win", "macosx"],
+ },
+
+ {
+ file: "chrome://browser/skin/toolbar-drag-indicator.svg",
+ platforms: ["linux", "win", "macosx"],
+ },
+
+ {
+ file: "chrome://global/skin/icons/chevron.svg",
+ platforms: ["win", "linux", "macosx"],
+ intermittentShown: ["win", "linux"],
+ },
+
+ {
+ file: "chrome://browser/skin/window-controls/maximize.svg",
+ platforms: ["win"],
+ // This is to prevent perma-fails in case Windows machines
+ // go back to running tests in non-maximized windows.
+ intermittentShown: ["win"],
+ // This file is not loaded on Windows 7/8.
+ intermittentNotLoaded: ["win"],
+ },
+];
+
+add_task(async function () {
+ if (!AppConstants.DEBUG) {
+ ok(false, "You need to run this test on a debug build.");
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ let data = Cu.cloneInto(startupRecorder.data.images, {});
+ let knownImagesForPlatform = knownUnshownImages.filter(el => {
+ return el.platforms.includes(AppConstants.platform);
+ });
+
+ {
+ let results = await PerfTestHelpers.throttledMapPromises(
+ knownImagesForPlatform,
+ async image => ({
+ uri: image.file,
+ exists: await PerfTestHelpers.checkURIExists(image.file),
+ })
+ );
+ for (let { uri, exists } of results) {
+ ok(exists, `Unshown image entry ${uri} must exist`);
+ }
+ }
+
+ let loadedImages = data["image-loading"];
+ let shownImages = data["image-drawing"];
+
+ for (let loaded of loadedImages.values()) {
+ let knownImage = knownImagesForPlatform.find(el => {
+ if (window.devicePixelRatio >= 2 && el.hidpi && el.hidpi == loaded) {
+ return true;
+ }
+ return el.file == loaded;
+ });
+ if (knownImage) {
+ if (
+ !knownImage.intermittentShown ||
+ !knownImage.intermittentShown.includes(AppConstants.platform)
+ ) {
+ todo(
+ shownImages.has(loaded),
+ `Image ${loaded} should not have been shown.`
+ );
+ }
+ continue;
+ }
+ ok(
+ shownImages.has(loaded),
+ `Loaded image ${loaded} should have been shown.`
+ );
+ }
+
+ // Check for known images that are no longer used.
+ for (let item of knownImagesForPlatform) {
+ if (
+ !item.intermittentNotLoaded ||
+ !item.intermittentNotLoaded.includes(AppConstants.platform)
+ ) {
+ if (window.devicePixelRatio >= 2 && item.hidpi) {
+ if (item.hidpi != "<not loaded>") {
+ ok(
+ loadedImages.has(item.hidpi),
+ `Image ${item.hidpi} should have been loaded.`
+ );
+ }
+ } else {
+ ok(
+ loadedImages.has(item.file),
+ `Image ${item.file} should have been loaded.`
+ );
+ }
+ }
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_mainthreadio.js b/browser/base/content/test/performance/browser_startup_mainthreadio.js
new file mode 100644
index 0000000000..3b34af5ad0
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_mainthreadio.js
@@ -0,0 +1,881 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records I/O syscalls done on the main thread during startup.
+ *
+ * To run this test similar to try server, you need to run:
+ * ./mach package
+ * ./mach test --appname=dist <path to test>
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are touching more files or directories during startup.
+ * Most code has no reason to use main thread I/O.
+ * If for some reason accessing the file system on the main thread is currently
+ * unavoidable, consider defering the I/O as long as you can, ideally after
+ * the end of startup.
+ * If your code isn't strictly required to show the first browser window,
+ * it shouldn't be loaded before we are done with first paint.
+ * Finally, if your code isn't really needed during startup, it should not be
+ * loaded before we have started handling user events.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+
+const kSharedFontList = SpecialPowers.getBoolPref("gfx.e10s.font-list.shared");
+
+/* This is an object mapping string phases of startup to lists of known cases
+ * of IO happening on the main thread. Ideally, IO should not be on the main
+ * thread, and should happen as late as possible (see above).
+ *
+ * Paths in the entries in these lists can:
+ * - be a full path, eg. "/etc/mime.types"
+ * - have a prefix which will be resolved using Services.dirsvc
+ * eg. "GreD:omni.ja"
+ * It's possible to have only a prefix, in thise case the directory will
+ * still be resolved, eg. "UAppData:"
+ * - use * at the begining and/or end as a wildcard
+ * The folder separator is '/' even for Windows paths, where it'll be
+ * automatically converted to '\'.
+ *
+ * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
+ * without this the test is strict and will fail if the described IO does not
+ * happen.
+ *
+ * Each entry specifies the maximum number of times an operation is expected to
+ * occur.
+ * The operations currently reported by the I/O interposer are:
+ * create/open: only supported on Windows currently. The test currently
+ * ignores these markers to have a shorter initial list of IO operations.
+ * Adding Unix support is bug 1533779.
+ * stat: supported on all platforms when checking the last modified date or
+ * file size. Supported only on Windows when checking if a file exists;
+ * fixing this inconsistency is bug 1536109.
+ * read: supported on all platforms, but unix platforms will only report read
+ * calls going through NSPR.
+ * write: supported on all platforms, but Linux will only report write calls
+ * going through NSPR.
+ * close: supported only on Unix, and only for close calls going through NSPR.
+ * Adding Windows support is bug 1524574.
+ * fsync: supported only on Windows.
+ *
+ * If an entry specifies more than one operation, if at least one of them is
+ * encountered, the test won't report a failure for the entry if other
+ * operations are not encountered. This helps when listing cases where the
+ * reported operations aren't the same on all platforms due to the I/O
+ * interposer inconsistencies across platforms documented above.
+ */
+const startupPhases = {
+ // Anything done before or during app-startup must have a compelling reason
+ // to run before we have even selected the user profile.
+ "before profile selection": [
+ {
+ // bug 1541200
+ path: "UAppData:Crash Reports/InstallTime20*",
+ condition: AppConstants.MOZ_CRASHREPORTER,
+ stat: 1, // only caught on Windows.
+ read: 1,
+ write: 2,
+ close: 1,
+ },
+ {
+ // bug 1541200
+ path: "UAppData:Crash Reports/LastCrash",
+ condition: WIN && AppConstants.MOZ_CRASHREPORTER,
+ stat: 1, // only caught on Windows.
+ read: 1,
+ },
+ {
+ // bug 1541200
+ path: "UAppData:Crash Reports/LastCrash",
+ condition: !WIN && AppConstants.MOZ_CRASHREPORTER,
+ ignoreIfUnused: true, // only if we ever crashed on this machine
+ read: 1,
+ close: 1,
+ },
+ {
+ // At least the read seems unavoidable for a regular startup.
+ path: "UAppData:profiles.ini",
+ ignoreIfUnused: true,
+ condition: MAC,
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ {
+ // At least the read seems unavoidable for a regular startup.
+ path: "UAppData:profiles.ini",
+ condition: WIN,
+ ignoreIfUnused: true, // only if a real profile exists on the system.
+ read: 1,
+ stat: 1,
+ },
+ {
+ // bug 1541226, bug 1363586, bug 1541593
+ path: "ProfD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ path: "ProfLD:.startup-incomplete",
+ condition: !WIN, // Visible on Windows with an open marker
+ close: 1,
+ },
+ {
+ // bug 1541491 to stop using this file, bug 1541494 to write correctly.
+ path: "ProfLD:compatibility.ini",
+ write: 18,
+ close: 1,
+ },
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ path: "ProfD:parent.lock",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1541603
+ path: "ProfD:minidumps",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1543746
+ path: "XCurProcD:defaults/preferences",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache-child-current.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache-child.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache-current.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1544034
+ path: "ProfLDS:startupCache/scriptCache.bin",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1541601
+ path: "PrfDef:channel-prefs.js",
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ {
+ // At least the read seems unavoidable
+ path: "PrefD:prefs.js",
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ {
+ // bug 1543752
+ path: "PrefD:user.js",
+ stat: 1,
+ read: 1,
+ close: 1,
+ },
+ ],
+
+ "before opening first browser window": [
+ {
+ // bug 1541226
+ path: "ProfD:",
+ condition: WIN,
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ stat: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-journal",
+ condition: !LINUX,
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ stat: 3,
+ write: 4,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite",
+ condition: !LINUX,
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ stat: 2,
+ read: 3,
+ write: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-wal",
+ ignoreIfUnused: true, // Sometimes happens in the next phase
+ condition: WIN,
+ stat: 2,
+ },
+ {
+ // Seems done by OS X and outside of our control.
+ path: "*.savedState/restorecount.plist",
+ condition: MAC,
+ ignoreIfUnused: true,
+ write: 1,
+ },
+ {
+ // Side-effect of bug 1412090, via sandboxing (but the real
+ // problem there is main-thread CPU use; see bug 1439412)
+ path: "*ld.so.conf*",
+ condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE && !kSharedFontList,
+ read: 22,
+ close: 11,
+ },
+ {
+ // bug 1541246
+ path: "ProfD:extensions",
+ ignoreIfUnused: true, // bug 1649590
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1541246
+ path: "UAppData:",
+ ignoreIfUnused: true, // sometimes before opening first browser window,
+ // sometimes before first paint
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1833104 has context - this is artifact-only so doesn't affect
+ // any real users, will just show up for developer builds and
+ // artifact trypushes so we include it here.
+ path: "GreD:jogfile.json",
+ condition:
+ WIN && Services.prefs.getBoolPref("telemetry.fog.artifact_build"),
+ stat: 1,
+ },
+ ],
+
+ // We reach this phase right after showing the first browser window.
+ // This means that any I/O at this point delayed first paint.
+ "before first paint": [
+ {
+ // bug 1545119
+ path: "OldUpdRootD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1446012
+ path: "UpdRootD:updates/0/update.status",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ path: "XREAppFeat:formautofill@mozilla.org.xpi",
+ condition: !WIN,
+ stat: 1,
+ close: 1,
+ },
+ {
+ path: "XREAppFeat:webcompat@mozilla.org.xpi",
+ condition: LINUX,
+ ignoreIfUnused: true, // Sometimes happens in the previous phase
+ close: 1,
+ },
+ {
+ // We only hit this for new profiles.
+ path: "XREAppDist:distribution.ini",
+ // check we're not msix to disambiguate from the next entry...
+ condition: WIN && !Services.sysinfo.getProperty("hasWinPackageId"),
+ stat: 1,
+ },
+ {
+ // On MSIX, we actually read this file - bug 1833341.
+ path: "XREAppDist:distribution.ini",
+ condition: WIN && Services.sysinfo.getProperty("hasWinPackageId"),
+ stat: 1,
+ read: 1,
+ },
+ {
+ // bug 1545139
+ path: "*Fonts/StaticCache.dat",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7
+ read: 1,
+ },
+ {
+ // Bug 1626738
+ path: "SysD:spool/drivers/color/*",
+ condition: WIN,
+ read: 1,
+ },
+ {
+ // Sandbox policy construction
+ path: "*ld.so.conf*",
+ condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE,
+ read: 22,
+ close: 11,
+ },
+ {
+ // bug 1541246
+ path: "UAppData:",
+ ignoreIfUnused: true, // sometimes before opening first browser window,
+ // sometimes before first paint
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // Not in packaged builds; useful for artifact builds.
+ path: "GreD:ScalarArtifactDefinitions.json",
+ condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
+ stat: 1,
+ },
+ {
+ // Not in packaged builds; useful for artifact builds.
+ path: "GreD:EventArtifactDefinitions.json",
+ condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
+ stat: 1,
+ },
+ {
+ // bug 1541226
+ path: "ProfD:",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-journal",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 3,
+ write: 4,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 2,
+ read: 3,
+ write: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-wal",
+ condition: WIN,
+ ignoreIfUnused: true, // Usually happens in the previous phase
+ stat: 2,
+ },
+ ],
+
+ // We are at this phase once we are ready to handle user events.
+ // Any IO at this phase or before gets in the way of the user
+ // interacting with the first browser window.
+ "before handling user events": [
+ {
+ path: "GreD:update.test",
+ ignoreIfUnused: true,
+ condition: LINUX,
+ close: 1,
+ },
+ {
+ path: "XREAppFeat:webcompat-reporter@mozilla.org.xpi",
+ condition: !WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ close: 1,
+ },
+ {
+ // Bug 1660582 - access while running on windows10 hardware.
+ path: "ProfD:wmfvpxvideo.guard",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ close: 1,
+ },
+ {
+ // Bug 1649590
+ path: "ProfD:extensions",
+ ignoreIfUnused: true,
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+
+ // Things that are expected to be completely out of the startup path
+ // and loaded lazily when used for the first time by the user should
+ // be listed here.
+ "before becoming idle": [
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:cert9.db`,
+ condition: WIN,
+ read: 5,
+ stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:cert9.db-journal`,
+ condition: WIN,
+ stat: 3,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:cert9.db-wal`,
+ condition: WIN,
+ stat: 3,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:pkcs11.txt",
+ condition: WIN,
+ read: 2,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:key4.db`,
+ condition: WIN,
+ read: 10,
+ stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:key4.db-journal`,
+ condition: WIN,
+ stat: 7,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: `ProfD:key4.db-wal`,
+ condition: WIN,
+ stat: 7,
+ },
+ {
+ path: "XREAppFeat:screenshots@mozilla.org.xpi",
+ ignoreIfUnused: true,
+ close: 1,
+ },
+ {
+ path: "XREAppFeat:webcompat-reporter@mozilla.org.xpi",
+ ignoreIfUnused: true,
+ stat: 1,
+ close: 1,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite-journal",
+ ignoreIfUnused: true,
+ fsync: 1,
+ stat: 4,
+ read: 1,
+ write: 2,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite-wal",
+ ignoreIfUnused: true,
+ stat: 4,
+ fsync: 3,
+ read: 51,
+ write: 178,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite-shm",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:places.sqlite",
+ ignoreIfUnused: true,
+ fsync: 2,
+ read: 4,
+ stat: 3,
+ write: 1324,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite-journal",
+ ignoreIfUnused: true,
+ fsync: 2,
+ stat: 7,
+ read: 2,
+ write: 7,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite-wal",
+ ignoreIfUnused: true,
+ fsync: 2,
+ stat: 7,
+ read: 7,
+ write: 15,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite-shm",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 2,
+ },
+ {
+ // bug 1391590
+ path: "ProfD:favicons.sqlite",
+ ignoreIfUnused: true,
+ fsync: 3,
+ read: 8,
+ stat: 4,
+ write: 1300,
+ },
+ {
+ path: "ProfD:",
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 3,
+ },
+ ],
+};
+
+for (let name of ["d3d11layers", "glcontext", "wmfvpxvideo"]) {
+ startupPhases["before first paint"].push({
+ path: `ProfD:${name}.guard`,
+ ignoreIfUnused: true,
+ stat: 1,
+ });
+}
+
+function expandPathWithDirServiceKey(path) {
+ if (path.includes(":")) {
+ let [prefix, suffix] = path.split(":");
+ let [key, property] = prefix.split(".");
+ let dir = Services.dirsvc.get(key, Ci.nsIFile);
+ if (property) {
+ dir = dir[property];
+ }
+
+ // Resolve symLinks.
+ let dirPath = dir.path;
+ while (dir && !dir.isSymlink()) {
+ dir = dir.parent;
+ }
+ if (dir) {
+ dirPath = dirPath.replace(dir.path, dir.target);
+ }
+
+ path = dirPath;
+
+ if (suffix) {
+ path += "/" + suffix;
+ }
+ }
+ if (AppConstants.platform == "win") {
+ path = path.replace(/\//g, "\\");
+ }
+ return path;
+}
+
+function getStackFromProfile(profile, stack) {
+ const stackPrefixCol = profile.stackTable.schema.prefix;
+ const stackFrameCol = profile.stackTable.schema.frame;
+ const frameLocationCol = profile.frameTable.schema.location;
+
+ let result = [];
+ while (stack) {
+ let sp = profile.stackTable.data[stack];
+ let frame = profile.frameTable.data[sp[stackFrameCol]];
+ stack = sp[stackPrefixCol];
+ frame = profile.stringTable[frame[frameLocationCol]];
+ if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
+ result.push(frame);
+ }
+ }
+ return result;
+}
+
+function pathMatches(path, filename) {
+ path = path.toLowerCase();
+ return (
+ path == filename || // Full match
+ // Wildcard on both sides of the path
+ (path.startsWith("*") &&
+ path.endsWith("*") &&
+ filename.includes(path.slice(1, -1))) ||
+ // Wildcard suffix
+ (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
+ // Wildcard prefix
+ (path.startsWith("*") && filename.endsWith(path.slice(1)))
+ );
+}
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ TestUtils.assertPackagedBuild();
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ // Add system add-ons to the list of known IO dynamically.
+ // They should go in the omni.ja file (bug 1357205).
+ {
+ let addons = await AddonManager.getAddonsByTypes(["extension"]);
+ for (let addon of addons) {
+ if (addon.isSystem) {
+ startupPhases["before opening first browser window"].push({
+ path: `XREAppFeat:${addon.id}.xpi`,
+ stat: 3,
+ close: 2,
+ });
+ startupPhases["before handling user events"].push({
+ path: `XREAppFeat:${addon.id}.xpi`,
+ condition: WIN,
+ stat: 2,
+ });
+ }
+ }
+ }
+
+ // Check for main thread I/O markers in the startup profile.
+ let profile = startupRecorder.data.profile.threads[0];
+
+ let phases = {};
+ {
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+
+ let markersForCurrentPhase = [];
+ let foundIOMarkers = false;
+
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+ if (markerName.startsWith("startupRecorder:")) {
+ phases[markerName.split("startupRecorder:")[1]] =
+ markersForCurrentPhase;
+ markersForCurrentPhase = [];
+ continue;
+ }
+
+ if (markerName != "FileIO") {
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (markerData.source == "sqlite-mainthread") {
+ continue;
+ }
+
+ let samples = markerData.stack.samples;
+ let stack = samples.data[0][samples.schema.stack];
+ markersForCurrentPhase.push({
+ operation: markerData.operation,
+ filename: markerData.filename,
+ source: markerData.source,
+ stackId: stack,
+ });
+ foundIOMarkers = true;
+ }
+
+ // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have
+ // no I/O marker in that case, but it's good to keep the test running to check
+ // that we are still able to produce startup profiles.
+ is(
+ foundIOMarkers,
+ !AppConstants.RELEASE_OR_BETA,
+ "The IO interposer should be enabled in builds that are not RELEASE_OR_BETA"
+ );
+ if (!foundIOMarkers) {
+ // If a profile unexpectedly contains no I/O marker, it's better to return
+ // early to avoid having a lot of of confusing "no main thread IO when we
+ // expected some" failures.
+ return;
+ }
+ }
+
+ for (let phase in startupPhases) {
+ startupPhases[phase] = startupPhases[phase].filter(
+ entry => !("condition" in entry) || entry.condition
+ );
+ startupPhases[phase].forEach(entry => {
+ entry.listedPath = entry.path;
+ entry.path = expandPathWithDirServiceKey(entry.path);
+ });
+ }
+
+ let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase();
+ let shouldPass = true;
+ for (let phase in phases) {
+ let knownIOList = startupPhases[phase];
+ info(
+ `known main thread IO paths during ${phase}:\n` +
+ knownIOList
+ .map(e => {
+ let operations = Object.keys(e)
+ .filter(k => k != "path")
+ .map(k => `${k}: ${e[k]}`);
+ return ` ${e.path} - ${operations.join(", ")}`;
+ })
+ .join("\n")
+ );
+
+ let markers = phases[phase];
+ for (let marker of markers) {
+ if (marker.operation == "create/open") {
+ // TODO: handle these I/O markers once they are supported on
+ // non-Windows platforms.
+ continue;
+ }
+
+ if (!marker.filename) {
+ // We are still missing the filename on some mainthreadio markers,
+ // these markers are currently useless for the purpose of this test.
+ continue;
+ }
+
+ // Convert to lower case before comparing because the OS X test machines
+ // have the 'Firefox' folder in 'Library/Application Support' created
+ // as 'firefox' for some reason.
+ let filename = marker.filename.toLowerCase();
+
+ if (!WIN && filename == "/dev/urandom") {
+ continue;
+ }
+
+ // /dev/shm is always tmpfs (a memory filesystem); this isn't
+ // really I/O any more than mmap/munmap are.
+ if (LINUX && filename.startsWith("/dev/shm/")) {
+ continue;
+ }
+
+ // "Files" from memfd_create() are similar to tmpfs but never
+ // exist in the filesystem; however, they have names which are
+ // exposed in procfs, and the I/O interposer observes when
+ // they're close()d.
+ if (LINUX && filename.startsWith("/memfd:")) {
+ continue;
+ }
+
+ // Shared memory uses temporary files on MacOS <= 10.11 to avoid
+ // a kernel security bug that will never be patched (see
+ // https://crbug.com/project-zero/1671 for details). This can
+ // be removed when we no longer support those OS versions.
+ if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
+ continue;
+ }
+
+ let expected = false;
+ for (let entry of knownIOList) {
+ if (pathMatches(entry.path, filename)) {
+ entry[marker.operation] = (entry[marker.operation] || 0) - 1;
+ entry._used = true;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ record(
+ false,
+ `unexpected ${marker.operation} on ${marker.filename} ${phase}`,
+ undefined,
+ " " + getStackFromProfile(profile, marker.stackId).join("\n ")
+ );
+ shouldPass = false;
+ }
+ info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
+ if (kDumpAllStacks) {
+ info(
+ getStackFromProfile(profile, marker.stackId)
+ .map(f => " " + f)
+ .join("\n")
+ );
+ }
+ }
+
+ for (let entry of knownIOList) {
+ for (let op in entry) {
+ if (
+ [
+ "listedPath",
+ "path",
+ "condition",
+ "ignoreIfUnused",
+ "_used",
+ ].includes(op)
+ ) {
+ continue;
+ }
+ let message = `${op} on ${entry.path} `;
+ if (entry[op] == 0) {
+ message += "as many times as expected";
+ } else if (entry[op] > 0) {
+ message += `allowed ${entry[op]} more times`;
+ } else {
+ message += `${entry[op] * -1} more times than expected`;
+ }
+ ok(entry[op] >= 0, `${message} ${phase}`);
+ }
+ if (!("_used" in entry) && !entry.ignoreIfUnused) {
+ ok(
+ false,
+ `no main thread IO when we expected some during ${phase}: ${entry.path} (${entry.listedPath})`
+ );
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected main thread I/O during startup");
+ } else {
+ const filename = "profile_startup_mainthreadio.json";
+ let path = Services.env.get("MOZ_UPLOAD_DIR");
+ let profilePath = PathUtils.join(path, filename);
+ await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
+ ok(
+ false,
+ "Unexpected main thread I/O behavior during startup; open the " +
+ `${filename} artifact in the Firefox Profiler to see what happened`
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_syncIPC.js b/browser/base/content/test/performance/browser_startup_syncIPC.js
new file mode 100644
index 0000000000..41744cf4b8
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_syncIPC.js
@@ -0,0 +1,449 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test sync IPC done on the main thread during startup. */
+
+"use strict";
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+const WEBRENDER = window.windowUtils.layerManagerType.startsWith("WebRender");
+const SKELETONUI = Services.prefs.getBoolPref(
+ "browser.startup.preXulSkeletonUI",
+ false
+);
+
+/*
+ * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
+ * without this the test is strict and will fail if a list entry isn't used.
+ */
+const startupPhases = {
+ // Anything done before or during app-startup must have a compelling reason
+ // to run before we have even selected the user profile.
+ "before profile selection": [],
+
+ "before opening first browser window": [],
+
+ // We reach this phase right after showing the first browser window.
+ // This means that any I/O at this point delayed first paint.
+ "before first paint": [
+ {
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: (MAC || LINUX) && !WEBRENDER,
+ maxCount: 1,
+ },
+ {
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: WIN && !WEBRENDER,
+ maxCount: 3,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: WIN && WEBRENDER,
+ maxCount: 3,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: (MAC || LINUX) && WEBRENDER,
+ maxCount: 1,
+ },
+ {
+ // bug 1373773
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ condition: !WIN,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_MapAndNotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: MAC,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 3,
+ },
+ {
+ name: "PCompositorWidget::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 3,
+ },
+ {
+ name: "PGPU::Msg_AddLayerTreeIdMapping",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 5,
+ },
+ {
+ name: "PCompositorBridge::Msg_MakeSnapshot",
+ condition: WIN && !WEBRENDER,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_GetSnapshot",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true, // Sometimes in the next phase on Windows10 QR
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_WillClose",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 2,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PGPU::Msg_GetDeviceStatus",
+ // bug 1553740 might want to drop the WEBRENDER clause here.
+ // Additionally, the skeleton UI causes us to attach "before first paint" to a
+ // later event, which lets this sneak in.
+ condition: WIN && (WEBRENDER || SKELETONUI),
+ // If Init() completes before we call EnsureGPUReady we won't send GetDeviceStatus
+ // so we can safely ignore if unused.
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ // bug 1784869
+ // We use Resume signal to propagate correct XWindow/wl_surface
+ // to EGL compositor.
+ name: "PCompositorBridge::Msg_Resume",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ ],
+
+ // We are at this phase once we are ready to handle user events.
+ // Any IO at this phase or before gets in the way of the user
+ // interacting with the first browser window.
+ "before handling user events": [
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: MAC,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 2,
+ },
+ {
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: (!MAC && !WEBRENDER) || (WIN && WEBRENDER),
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorWidget::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_WillClose",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_MakeSnapshot",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_GetSnapshot",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true, // Sometimes in the next phase on Windows10 QR
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ReceiveMouseInputEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // intermittently occurs in "before becoming idle"
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PContent::Reply_BeginDriverCrashGuard",
+ condition: WIN,
+ ignoreIfUnused: true, // Bug 1660590 - found while running test on windows hardware
+ maxCount: 1,
+ },
+ {
+ name: "PContent::Reply_EndDriverCrashGuard",
+ condition: WIN,
+ ignoreIfUnused: true, // Bug 1660590 - found while running test on windows hardware
+ maxCount: 1,
+ },
+ {
+ // bug 1784869
+ // We use Resume signal to propagate correct XWindow/wl_surface
+ // to EGL compositor.
+ name: "PCompositorBridge::Msg_Resume",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before first paint"
+ maxCount: 1,
+ },
+ ],
+
+ // Things that are expected to be completely out of the startup path
+ // and loaded lazily when used for the first time by the user should
+ // be listed here.
+ "before becoming idle": [
+ {
+ // bug 1373773
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ReceiveMouseInputEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ // bug 1554234
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: WIN || LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: (WIN || LINUX) && WEBRENDER,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorWidget::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_MapAndNotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: MAC || SKELETONUI,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: LINUX,
+ ignoreIfUnused: true, // intermittently occurs in "before handling user events"
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_GetSnapshot",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_MakeSnapshot",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_WillClose",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 2,
+ },
+ // Added for the search-detection built-in add-on.
+ {
+ name: "PGPU::Msg_AddLayerTreeIdMapping",
+ condition: WIN,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ ],
+};
+
+add_task(async function () {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ let startupRecorder =
+ Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+ await startupRecorder.done;
+
+ // Check for sync IPC markers in the startup profile.
+ let profile = startupRecorder.data.profile.threads[0];
+
+ let phases = {};
+ {
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+ const startTimeCol = profile.markers.schema.startTime;
+
+ let markersForCurrentPhase = [];
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+ if (markerName.startsWith("startupRecorder:")) {
+ phases[markerName.split("startupRecorder:")[1]] =
+ markersForCurrentPhase;
+ markersForCurrentPhase = [];
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (
+ !markerData ||
+ markerData.category != "Sync IPC" ||
+ !m[startTimeCol]
+ ) {
+ continue;
+ }
+
+ markersForCurrentPhase.push(markerName);
+ }
+ }
+
+ for (let phase in startupPhases) {
+ startupPhases[phase] = startupPhases[phase].filter(
+ entry => !("condition" in entry) || entry.condition
+ );
+ }
+
+ let shouldPass = true;
+ for (let phase in phases) {
+ let knownIPCList = startupPhases[phase];
+ if (knownIPCList.length) {
+ info(
+ `known sync IPC ${phase}:\n` +
+ knownIPCList
+ .map(e => ` ${e.name} - at most ${e.maxCount} times`)
+ .join("\n")
+ );
+ }
+
+ let markers = phases[phase];
+ for (let marker of markers) {
+ let expected = false;
+ for (let entry of knownIPCList) {
+ if (marker == entry.name) {
+ entry.useCount = (entry.useCount || 0) + 1;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ ok(false, `unexpected ${marker} sync IPC ${phase}`);
+ shouldPass = false;
+ }
+ }
+
+ for (let entry of knownIPCList) {
+ // Make sure useCount has been defined.
+ entry.useCount = entry.useCount || 0;
+ let message = `sync IPC ${entry.name} `;
+ if (entry.useCount == entry.maxCount) {
+ message += "happened as many times as expected";
+ } else if (entry.useCount < entry.maxCount) {
+ message += `allowed ${entry.maxCount} but only happened ${entry.useCount} times`;
+ } else {
+ message += `happened ${entry.useCount} but max is ${entry.maxCount}`;
+ shouldPass = false;
+ }
+ ok(entry.useCount <= entry.maxCount, `${message} ${phase}`);
+
+ if (entry.useCount == 0 && !entry.ignoreIfUnused) {
+ ok(false, `unused known IPC entry ${phase}: ${entry.name}`);
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected sync IPC during startup");
+ } else {
+ const filename = "profile_startup_syncIPC.json";
+ let path = Services.env.get("MOZ_UPLOAD_DIR");
+ let profilePath = PathUtils.join(path, filename);
+ await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
+ ok(
+ false,
+ `Unexpected sync IPC behavior during startup; open the ${filename} ` +
+ "artifact in the Firefox Profiler to see what happened"
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_tabclose.js b/browser/base/content/test/performance/browser_tabclose.js
new file mode 100644
index 0000000000..961686587f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabclose.js
@@ -0,0 +1,108 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing new tabs.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await TestUtils.waitForCondition(() => tab._fullyOpen);
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let newTabButtonRect =
+ gBrowser.tabContainer.newTabButton.getBoundingClientRect();
+ let inRange = (val, min, max) => min <= val && val <= max;
+
+ // Add a reflow observer and open a new tab.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ gBrowser.removeTab(tab, { animate: true });
+ await BrowserTestUtils.waitForEvent(tab, "TabAnimationEnd");
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect all changes to be within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // The closed tab should disappear at the same time as the previous
+ // tab gets selected, causing both tab areas to change color at once:
+ // this should be a single rect of the width of 2 tabs, and can
+ // include the '+' button if it starts its animation.
+ ((r.w > gBrowser.selectedTab.clientWidth &&
+ r.x2 <= newTabButtonRect.right) ||
+ // The '+' icon moves with an animation. At the end of the animation
+ // the former and new positions can touch each other causing the rect
+ // to have twice the icon's width.
+ (r.h == 13 && r.w <= 2 * 13 + kMaxEmptyPixels) ||
+ // We sometimes have a rect for the right most 2px of the '+' button.
+ (r.h == 2 && r.w == 2))
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name:
+ "bug 1444886 - the next tab should be selected at the same time" +
+ " as the closed one disappears",
+ condition: r =>
+ // In tab strip
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // Width of one tab plus tab separator(s)
+ inRange(gBrowser.selectedTab.clientWidth - r.w, 0, 2),
+ },
+ {
+ name: "bug 1446449 - spurious tab switch spinner",
+ condition: r =>
+ AppConstants.DEBUG &&
+ // In the content area
+ r.y1 >=
+ document.getElementById("appcontent").getBoundingClientRect()
+ .top,
+ },
+ ],
+ },
+ }
+ );
+ is(EXPECTED_REFLOWS.length, 0, "No reflows are expected when closing a tab");
+});
diff --git a/browser/base/content/test/performance/browser_tabclose_grow.js b/browser/base/content/test/performance/browser_tabclose_grow.js
new file mode 100644
index 0000000000..7ad43809cd
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabclose_grow.js
@@ -0,0 +1,91 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing a tab that will
+ * cause the existing tabs to grow bigger.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // At the time of writing, there are no reflows on tab closing with
+ // tab growth. Mochitest will fail if we have no assertions, so we
+ // add one here to make sure nobody adds any new ones.
+ Assert.equal(
+ EXPECTED_REFLOWS.length,
+ 0,
+ "We shouldn't have added any new expected reflows."
+ );
+
+ // Compute the number of tabs we can put into the strip without
+ // overflowing. If we remove one of the tabs, we know that the
+ // remaining tabs will grow to fill the remaining space in the
+ // tabstrip.
+ const TAB_COUNT_FOR_GROWTH = computeMaxTabCount();
+ await createTabs(TAB_COUNT_FOR_GROWTH);
+
+ let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ await BrowserTestUtils.switchTab(gBrowser, lastTab);
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+
+ function isInTabStrip(r) {
+ return (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // It would make sense for each rect to have a width smaller than
+ // a tab (ie. tabstrip.width / tabcount), but tabs are small enough
+ // that they sometimes get reported in the same rect.
+ // So we accept up to the width of n-1 tabs.
+ r.w <=
+ (gBrowser.tabs.length - 1) *
+ Math.ceil(tabStripRect.width / gBrowser.tabs.length)
+ );
+ }
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ let tab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ gBrowser.removeTab(tab, { animate: true });
+ await BrowserTestUtils.waitForEvent(tab, "TabAnimationEnd");
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects => rects.filter(r => !isInTabStrip(r)),
+ },
+ }
+ );
+
+ await removeAllButFirstTab();
+});
diff --git a/browser/base/content/test/performance/browser_tabdetach.js b/browser/base/content/test/performance/browser_tabdetach.js
new file mode 100644
index 0000000000..a860362f1f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabdetach.js
@@ -0,0 +1,118 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * list should slowly go away as we improve the performance of the front-end.
+ * Instead of adding more reflows to the list, you should be modifying your code
+ * to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ {
+ stack: [
+ "clientX@chrome://browser/content/tabbrowser-tabs.js",
+ "startTabDrag@chrome://browser/content/tabbrowser-tabs.js",
+ "on_dragstart@chrome://browser/content/tabbrowser-tabs.js",
+ "handleEvent@chrome://browser/content/tabbrowser-tabs.js",
+ "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ ],
+ maxCount: 2,
+ },
+
+ {
+ stack: [
+ "startTabDrag@chrome://browser/content/tabbrowser-tabs.js",
+ "on_dragstart@chrome://browser/content/tabbrowser-tabs.js",
+ "handleEvent@chrome://browser/content/tabbrowser-tabs.js",
+ "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ ],
+ },
+];
+
+/**
+ * This test ensures that there are no unexpected uninterruptible reflows when
+ * detaching a tab via drag and drop. The first testcase tests a non-overflowed
+ * tab strip, and the second tests an overflowed one.
+ */
+
+add_task(async function test_detach_not_overflowed() {
+ await ensureNoPreloadedBrowser();
+ await createTabs(1);
+
+ // Make sure we didn't overflow, as expected
+ await TestUtils.waitForCondition(() => {
+ return !gBrowser.tabContainer.hasAttribute("overflow");
+ });
+
+ let win;
+ await withPerfObserver(
+ async function () {
+ win = await detachTab(gBrowser.tabs[1]);
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ // we are opening a whole new window, so there's no point in tracking
+ // rects being painted
+ frames: { filter: rects => [] },
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ win = null;
+});
+
+add_task(async function test_detach_overflowed() {
+ const TAB_COUNT_FOR_OVERFLOW = computeMaxTabCount();
+ await createTabs(TAB_COUNT_FOR_OVERFLOW + 1);
+
+ // Make sure we overflowed, as expected
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.hasAttribute("overflow");
+ });
+
+ let win;
+ await withPerfObserver(
+ async function () {
+ win = await detachTab(
+ gBrowser.tabs[Math.floor(TAB_COUNT_FOR_OVERFLOW / 2)]
+ );
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ // we are opening a whole new window, so there's no point in tracking
+ // rects being painted
+ frames: { filter: rects => [] },
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ win = null;
+
+ await removeAllButFirstTab();
+});
+
+async function detachTab(tab) {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+
+ await EventUtils.synthesizePlainDragAndDrop({
+ srcElement: tab,
+
+ // destElement is null because tab detaching happens due
+ // to a drag'n'drop on an invalid drop target.
+ destElement: null,
+
+ // don't move horizontally because that could cause a tab move
+ // animation, and there's code to prevent a tab detaching if
+ // the dragged tab is released while the animation is running.
+ stepX: 0,
+ stepY: 100,
+ });
+
+ return newWindowPromise;
+}
diff --git a/browser/base/content/test/performance/browser_tabopen.js b/browser/base/content/test/performance/browser_tabopen.js
new file mode 100644
index 0000000000..2457812cb7
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabopen.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when opening new tabs.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ // TODO (bug 1702653): Disable tab shadows for tests since the shadow
+ // can extend outside of the boundingClientRect. The tabRect will need
+ // to grow to include the shadow size.
+ gBrowser.tabContainer.setAttribute("noshadowfortests", "true");
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // Prepare the window to avoid flicker and reflow that's unrelated to our
+ // tab opening operation.
+ gURLBar.focus();
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+
+ let firstTabRect = gBrowser.selectedTab.getBoundingClientRect();
+ let tabPaddingStart = parseFloat(
+ getComputedStyle(gBrowser.selectedTab).paddingInlineStart
+ );
+ let minTabWidth = firstTabRect.width - 2 * tabPaddingStart;
+ let maxTabWidth = firstTabRect.width;
+ let firstTabLabelRect =
+ gBrowser.selectedTab.textLabel.getBoundingClientRect();
+ let newTabButtonRect = document
+ .getElementById("tabs-newtab-button")
+ .getBoundingClientRect();
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+
+ let inRange = (val, min, max) => min <= val && val <= max;
+
+ info(`tabStripRect=${JSON.stringify(tabStripRect)}`);
+ info(`firstTabRect=${JSON.stringify(firstTabRect)}`);
+ info(`tabPaddingStart=${JSON.stringify(tabPaddingStart)}`);
+ info(`firstTabLabelRect=${JSON.stringify(firstTabLabelRect)}`);
+ info(`newTabButtonRect=${JSON.stringify(newTabButtonRect)}`);
+ info(`textBoxRect=${JSON.stringify(textBoxRect)}`);
+
+ let inTabStrip = function (r) {
+ return (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right
+ );
+ };
+
+ const kTabCloseIconWidth = 13;
+
+ let isExpectedChange = function (r) {
+ // We expect all changes to be within the tab strip.
+ if (!inTabStrip(r)) {
+ return false;
+ }
+
+ // The first tab should get deselected at the same time as the next tab
+ // starts appearing, so we should have one rect that includes the first tab
+ // but is wider.
+ if (
+ inRange(r.w, minTabWidth, maxTabWidth * 2) &&
+ inRange(r.x1, firstTabRect.x, firstTabRect.x + tabPaddingStart)
+ ) {
+ return true;
+ }
+
+ // The second tab gets painted several times due to tabopen animation.
+ let isSecondTabRect =
+ inRange(
+ r.x1,
+ // When the animation starts the tab close icon overflows.
+ // -1 for the border on Win7
+ firstTabRect.right - kTabCloseIconWidth - 1,
+ firstTabRect.right + firstTabRect.width
+ ) &&
+ r.x2 <
+ firstTabRect.right +
+ firstTabRect.width +
+ // Sometimes the '+' is in the same rect.
+ newTabButtonRect.width;
+
+ if (isSecondTabRect) {
+ return true;
+ }
+ // The '+' icon moves with an animation. At the end of the animation
+ // the former and new positions can touch each other causing the rect
+ // to have twice the icon's width.
+ if (
+ r.h == kTabCloseIconWidth &&
+ r.w <= 2 * kTabCloseIconWidth + kMaxEmptyPixels
+ ) {
+ return true;
+ }
+
+ // We sometimes have a rect for the right most 2px of the '+' button.
+ if (r.h == 2 && r.w == 2) {
+ return true;
+ }
+
+ // Same for the 'X' icon.
+ if (r.h == 10 && r.w <= 2 * 10) {
+ return true;
+ }
+
+ // Other changes are unexpected.
+ return false;
+ };
+
+ // Add a reflow observer and open a new tab.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabAnimationEnd"
+ );
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects => rects.filter(r => !isExpectedChange(r)),
+ exceptions: [
+ {
+ name:
+ "bug 1446452 - the new tab should appear at the same time as the" +
+ " previous one gets deselected",
+ condition: r =>
+ // In tab strip
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ // Position and size of the first tab.
+ r.x1 == firstTabRect.left &&
+ inRange(
+ r.w,
+ firstTabRect.width - 1, // -1 as the border doesn't change
+ firstTabRect.width
+ ),
+ },
+ {
+ name: "the urlbar placeolder moves up and down by a few pixels",
+ // This seems to only happen on the second run in --verify
+ condition: r =>
+ r.x1 >= textBoxRect.left &&
+ r.x2 <= textBoxRect.right &&
+ r.y1 >= textBoxRect.top &&
+ r.y2 <= textBoxRect.bottom,
+ },
+ {
+ name: "bug 1477966 - the name of a deselected tab should appear immediately",
+ condition: r =>
+ AppConstants.platform == "macosx" &&
+ r.x1 >= firstTabLabelRect.x &&
+ r.x2 <= firstTabLabelRect.right &&
+ r.y1 >= firstTabLabelRect.y &&
+ r.y2 <= firstTabLabelRect.bottom,
+ },
+ ],
+ },
+ }
+ );
+
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await switchDone;
+});
diff --git a/browser/base/content/test/performance/browser_tabopen_squeeze.js b/browser/base/content/test/performance/browser_tabopen_squeeze.js
new file mode 100644
index 0000000000..f92bdc2ea4
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabopen_squeeze.js
@@ -0,0 +1,100 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when opening a new tab that will
+ * cause the existing tabs to squeeze smaller.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // Compute the number of tabs we can put into the strip without
+ // overflowing, and remove one, so that we can create
+ // TAB_COUNT_FOR_SQUEEE tabs, and then one more, which should
+ // cause the tab to squeeze to a smaller size rather than overflow.
+ const TAB_COUNT_FOR_SQUEEZE = computeMaxTabCount() - 1;
+
+ await createTabs(TAB_COUNT_FOR_SQUEEZE);
+
+ gURLBar.focus();
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabAnimationEnd"
+ );
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect plenty of changed rects within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // It would make sense for each rect to have a width smaller than
+ // a tab (ie. tabstrip.width / tabcount), but tabs are small enough
+ // that they sometimes get reported in the same rect.
+ // So we accept up to the width of n-1 tabs.
+ r.w <=
+ (gBrowser.tabs.length - 1) *
+ Math.ceil(tabStripRect.width / gBrowser.tabs.length)
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name: "the urlbar placeolder moves up and down by a few pixels",
+ condition: r =>
+ r.x1 >= textBoxRect.left &&
+ r.x2 <= textBoxRect.right &&
+ r.y1 >= textBoxRect.top &&
+ r.y2 <= textBoxRect.bottom,
+ },
+ ],
+ },
+ }
+ );
+
+ await removeAllButFirstTab();
+});
diff --git a/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
new file mode 100644
index 0000000000..1fd33ed836
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
@@ -0,0 +1,200 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_*_REFLOWS.
+ * This is a (now empty) list of known reflows.
+ * Instead of adding more reflows to the lists, you should be modifying your
+ * code to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_OVERFLOW_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+const EXPECTED_UNDERFLOW_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/**
+ * This test ensures that there are no unexpected uninterruptible reflows when
+ * opening a new tab that will cause the existing tabs to overflow and the tab
+ * strip to become scrollable. It also tests that there are no unexpected
+ * uninterruptible reflows when closing that tab, which causes the tab strip to
+ * underflow.
+ */
+add_task(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ await ensureNoPreloadedBrowser();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ const TAB_COUNT_FOR_OVERFLOW = computeMaxTabCount();
+
+ await createTabs(TAB_COUNT_FOR_OVERFLOW);
+
+ gURLBar.focus();
+ await disableFxaBadge();
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+
+ let ignoreTabstripRects = {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect plenty of changed rects within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name: "the urlbar placeolder moves up and down by a few pixels",
+ condition: r =>
+ r.x1 >= textBoxRect.left &&
+ r.x2 <= textBoxRect.right &&
+ r.y1 >= textBoxRect.top &&
+ r.y2 <= textBoxRect.bottom,
+ },
+ {
+ name: "bug 1446449 - spurious tab switch spinner",
+ condition: r =>
+ // In the content area
+ r.y1 >=
+ document.getElementById("appcontent").getBoundingClientRect().top,
+ },
+ ],
+ };
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "TabAnimationEnd"
+ );
+ await switchDone;
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.arrowScrollbox.hasAttribute(
+ "scrolledtoend"
+ );
+ });
+ },
+ { expectedReflows: EXPECTED_OVERFLOW_REFLOWS, frames: ignoreTabstripRects }
+ );
+
+ Assert.ok(
+ gBrowser.tabContainer.hasAttribute("overflow"),
+ "Tabs should now be overflowed."
+ );
+
+ // Now test that opening and closing a tab while overflowed doesn't cause
+ // us to reflow.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserOpenTab();
+ await switchDone;
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.arrowScrollbox.hasAttribute(
+ "scrolledtoend"
+ );
+ });
+ },
+ { expectedReflows: [], frames: ignoreTabstripRects }
+ );
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab, { animate: true });
+ await switchDone;
+ },
+ { expectedReflows: [], frames: ignoreTabstripRects }
+ );
+
+ // At this point, we have an overflowed tab strip, and we've got the last tab
+ // selected. This should mean that the first tab is scrolled out of view.
+ // Let's test that we don't reflow when switching to that first tab.
+ let lastTab = gBrowser.selectedTab;
+ let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+
+ // First, we'll check that the first tab is actually scrolled
+ // at least partially out of view.
+ Assert.ok(
+ arrowScrollbox.scrollPosition > 0,
+ "First tab should be partially scrolled out of view."
+ );
+
+ // Now switch to the first tab. We shouldn't flush layout at all.
+ await withPerfObserver(
+ async function () {
+ let firstTab = gBrowser.tabs[0];
+ await BrowserTestUtils.switchTab(gBrowser, firstTab);
+ await TestUtils.waitForCondition(() => {
+ return gBrowser.tabContainer.arrowScrollbox.hasAttribute(
+ "scrolledtostart"
+ );
+ });
+ },
+ { expectedReflows: [], frames: ignoreTabstripRects }
+ );
+
+ // Okay, now close the last tab. The tabstrip should stay overflowed, but removing
+ // one more after that should underflow it.
+ BrowserTestUtils.removeTab(lastTab);
+
+ Assert.ok(
+ gBrowser.tabContainer.hasAttribute("overflow"),
+ "Tabs should still be overflowed."
+ );
+
+ // Depending on the size of the window, it might take one or more tab
+ // removals to put the tab strip out of the overflow state, so we'll just
+ // keep testing removals until that occurs.
+ while (gBrowser.tabContainer.hasAttribute("overflow")) {
+ lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ if (gBrowser.selectedTab !== lastTab) {
+ await BrowserTestUtils.switchTab(gBrowser, lastTab);
+ }
+
+ // ... and make sure we don't flush layout when closing it, and exiting
+ // the overflowed state.
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ BrowserTestUtils.removeTab(lastTab, { animate: true });
+ await switchDone;
+ await TestUtils.waitForCondition(() => !lastTab.isConnected);
+ },
+ {
+ expectedReflows: EXPECTED_UNDERFLOW_REFLOWS,
+ frames: ignoreTabstripRects,
+ }
+ );
+ }
+
+ await removeAllButFirstTab();
+});
diff --git a/browser/base/content/test/performance/browser_tabswitch.js b/browser/base/content/test/performance/browser_tabswitch.js
new file mode 100644
index 0000000000..bbbbac3a21
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabswitch.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when switching between two
+ * tabs that are both fully visible.
+ */
+add_task(async function () {
+ // TODO (bug 1702653): Disable tab shadows for tests since the shadow
+ // can extend outside of the boundingClientRect. The tabRect will need
+ // to grow to include the shadow size.
+ gBrowser.tabContainer.setAttribute("noshadowfortests", "true");
+
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ // The test starts on about:blank and opens an about:blank
+ // tab which triggers opening the toolbar since
+ // ensureNoPreloadedBrowser sets AboutNewTab.newTabURL to about:blank.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "never"]],
+ });
+
+ // At the time of writing, there are no reflows on simple tab switching.
+ // Mochitest will fail if we have no assertions, so we add one here
+ // to make sure nobody adds any new ones.
+ Assert.equal(
+ EXPECTED_REFLOWS.length,
+ 0,
+ "We shouldn't have added any new expected reflows."
+ );
+
+ let origTab = gBrowser.selectedTab;
+ let firstSwitchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ let otherTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await firstSwitchDone;
+
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let firstTabRect = origTab.getBoundingClientRect();
+ let tabPaddingStart = parseFloat(
+ getComputedStyle(gBrowser.selectedTab).paddingInlineStart
+ );
+ let minTabWidth = firstTabRect.width - 2 * tabPaddingStart;
+ let maxTabWidth = firstTabRect.width;
+ let inRange = (val, min, max) => min <= val && val <= max;
+
+ await withPerfObserver(
+ async function () {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ gBrowser.selectedTab = origTab;
+ await switchDone;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect all changes to be within the tab strip.
+ (
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ r.x1 >= tabStripRect.left &&
+ r.x2 <= tabStripRect.right &&
+ // The tab selection changes between 2 adjacent tabs, so we expect
+ // both to change color at once: this should be a single rect of the
+ // width of 2 tabs.
+ inRange(
+ r.w,
+ minTabWidth - 1, // -1 for the border on Win7
+ maxTabWidth * 2
+ )
+ )
+ )
+ ),
+ exceptions: [
+ {
+ name:
+ "bug 1446454 - the border between tabs should be painted at" +
+ " the same time as the tab switch",
+ condition: r =>
+ // In tab strip
+ r.y1 >= tabStripRect.top &&
+ r.y2 <= tabStripRect.bottom &&
+ // 1px border, 1px before the end of the first tab.
+ r.w == 1 &&
+ r.x1 == firstTabRect.right - 1,
+ },
+ {
+ name: "bug 1446449 - spurious tab switch spinner",
+ condition: r =>
+ AppConstants.DEBUG &&
+ // In the content area
+ r.y1 >=
+ document.getElementById("appcontent").getBoundingClientRect()
+ .top,
+ },
+ ],
+ },
+ }
+ );
+
+ BrowserTestUtils.removeTab(otherTab);
+});
diff --git a/browser/base/content/test/performance/browser_toolbariconcolor_restyles.js b/browser/base/content/test/performance/browser_toolbariconcolor_restyles.js
new file mode 100644
index 0000000000..890c8f3c80
--- /dev/null
+++ b/browser/base/content/test/performance/browser_toolbariconcolor_restyles.js
@@ -0,0 +1,65 @@
+"use strict";
+
+/**
+ * Ensure redundant style flushes are not triggered when switching between windows
+ */
+add_task(async function test_toolbar_element_restyles_on_activation() {
+ let restyles = {
+ win1: {},
+ win2: {},
+ };
+
+ // create a window and snapshot the elementsStyled
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ await new Promise(resolve => waitForFocus(resolve, win1));
+
+ // create a 2nd window and snapshot the elementsStyled
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ await new Promise(resolve => waitForFocus(resolve, win2));
+
+ // (De)-activate both windows once before we take a measurement. The first
+ // (de-)activation may flush styles, after that the style data should be
+ // cached.
+ win1.focus();
+ win2.focus();
+
+ // Flush any pending styles before we take a measurement.
+ win1.getComputedStyle(win1.document.firstElementChild);
+ win2.getComputedStyle(win2.document.firstElementChild);
+
+ // Clear the focused element from each window so that when
+ // we raise them, the focus of the element doesn't cause an
+ // unrelated style flush.
+ Services.focus.clearFocus(win1);
+ Services.focus.clearFocus(win2);
+
+ let utils1 = SpecialPowers.getDOMWindowUtils(win1);
+ restyles.win1.initial = utils1.restyleGeneration;
+
+ let utils2 = SpecialPowers.getDOMWindowUtils(win2);
+ restyles.win2.initial = utils2.restyleGeneration;
+
+ // switch back to 1st window, and snapshot elementsStyled
+ win1.focus();
+ restyles.win1.activate = utils1.restyleGeneration;
+ restyles.win2.deactivate = utils2.restyleGeneration;
+
+ // switch back to 2nd window, and snapshot elementsStyled
+ win2.focus();
+ restyles.win2.activate = utils2.restyleGeneration;
+ restyles.win1.deactivate = utils1.restyleGeneration;
+
+ is(
+ restyles.win1.activate - restyles.win1.deactivate,
+ 0,
+ "No elements restyled when re-activating/deactivating a window"
+ );
+ is(
+ restyles.win2.activate - restyles.win2.deactivate,
+ 0,
+ "No elements restyled when re-activating/deactivating a window"
+ );
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/base/content/test/performance/browser_urlbar_keyed_search.js b/browser/base/content/test/performance/browser_urlbar_keyed_search.js
new file mode 100644
index 0000000000..a44e5d4822
--- /dev/null
+++ b/browser/base/content/test/performance/browser_urlbar_keyed_search.js
@@ -0,0 +1,27 @@
+"use strict";
+
+// This tests searching in the urlbar (a.k.a. the quantumbar).
+
+/**
+ * WHOA THERE: We should never be adding new things to
+ * EXPECTED_REFLOWS_FIRST_OPEN or EXPECTED_REFLOWS_SECOND_OPEN.
+ * Instead of adding reflows to these lists, you should be modifying your code
+ * to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+
+/* These reflows happen only the first time the panel opens. */
+const EXPECTED_REFLOWS_FIRST_OPEN = [];
+
+/* These reflows happen every time the panel opens. */
+const EXPECTED_REFLOWS_SECOND_OPEN = [];
+
+add_task(async function quantumbar() {
+ await runUrlbarTest(
+ true,
+ EXPECTED_REFLOWS_FIRST_OPEN,
+ EXPECTED_REFLOWS_SECOND_OPEN
+ );
+});
diff --git a/browser/base/content/test/performance/browser_urlbar_search.js b/browser/base/content/test/performance/browser_urlbar_search.js
new file mode 100644
index 0000000000..35961c641f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_urlbar_search.js
@@ -0,0 +1,27 @@
+"use strict";
+
+// This tests searching in the urlbar (a.k.a. the quantumbar).
+
+/**
+ * WHOA THERE: We should never be adding new things to
+ * EXPECTED_REFLOWS_FIRST_OPEN or EXPECTED_REFLOWS_SECOND_OPEN.
+ * Instead of adding reflows to these lists, you should be modifying your code
+ * to avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+
+/* These reflows happen only the first time the panel opens. */
+const EXPECTED_REFLOWS_FIRST_OPEN = [];
+
+/* These reflows happen every time the panel opens. */
+const EXPECTED_REFLOWS_SECOND_OPEN = [];
+
+add_task(async function quantumbar() {
+ await runUrlbarTest(
+ false,
+ EXPECTED_REFLOWS_FIRST_OPEN,
+ EXPECTED_REFLOWS_SECOND_OPEN
+ );
+});
diff --git a/browser/base/content/test/performance/browser_vsync_accessibility.js b/browser/base/content/test/performance/browser_vsync_accessibility.js
new file mode 100644
index 0000000000..64e3dc0b85
--- /dev/null
+++ b/browser/base/content/test/performance/browser_vsync_accessibility.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "wait for vsync to be disabled at the start of the test"
+ );
+ Assert.ok(!ChromeUtils.vsyncEnabled(), "vsync should be disabled");
+ Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await TestUtils.waitForCondition(
+ () => !ChromeUtils.vsyncEnabled(),
+ "wait for vsync to be disabled after initializing the accessibility service"
+ );
+ Assert.ok(!ChromeUtils.vsyncEnabled(), "vsync should still be disabled");
+});
diff --git a/browser/base/content/test/performance/browser_window_resize.js b/browser/base/content/test/performance/browser_window_resize.js
new file mode 100644
index 0000000000..0838ae9f8f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_window_resize.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+const gToolbar = document.getElementById("PersonalToolbar");
+
+/**
+ * Sets the visibility state on the Bookmarks Toolbar, and
+ * waits for it to transition to fully visible.
+ *
+ * @param visible (bool)
+ * Whether or not the bookmarks toolbar should be made visible.
+ * @returns Promise
+ */
+async function toggleBookmarksToolbar(visible) {
+ let transitionPromise = BrowserTestUtils.waitForEvent(
+ gToolbar,
+ "transitionend",
+ e => e.propertyName == "max-height"
+ );
+
+ setToolbarVisibility(gToolbar, visible);
+ await transitionPromise;
+}
+
+/**
+ * Resizes a browser window to a particular width and height, and
+ * waits for it to reach a "steady state" with respect to its overflowing
+ * toolbars.
+ * @param win (browser window)
+ * The window to resize.
+ * @param width (int)
+ * The width to resize the window to.
+ * @param height (int)
+ * The height to resize the window to.
+ * @returns Promise
+ */
+async function resizeWindow(win, width, height) {
+ let toolbarEvent = BrowserTestUtils.waitForEvent(
+ win,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ let resizeEvent = BrowserTestUtils.waitForEvent(win, "resize");
+ win.windowUtils.ensureDirtyRootFrame();
+ win.resizeTo(width, height);
+ await resizeEvent;
+ await toolbarEvent;
+}
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when resizing windows.
+ */
+add_task(async function () {
+ const BOOKMARKS_COUNT = 150;
+ const STARTING_WIDTH = 600;
+ const STARTING_HEIGHT = 400;
+ const SMALL_WIDTH = 150;
+ const SMALL_HEIGHT = 150;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add a bunch of bookmarks to display in the Bookmarks toolbar
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: Array(BOOKMARKS_COUNT)
+ .fill("")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .map((_, i) => ({ url: `http://test.places.${i}/` })),
+ });
+
+ let wasCollapsed = gToolbar.collapsed;
+ Assert.ok(wasCollapsed, "The toolbar is collapsed by default");
+ if (wasCollapsed) {
+ let promiseReady = BrowserTestUtils.waitForEvent(
+ gToolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ await toggleBookmarksToolbar(true);
+ await promiseReady;
+ }
+
+ registerCleanupFunction(async () => {
+ if (wasCollapsed) {
+ await toggleBookmarksToolbar(false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+
+ let win = await prepareSettledWindow();
+
+ if (
+ win.screen.availWidth < STARTING_WIDTH ||
+ win.screen.availHeight < STARTING_HEIGHT
+ ) {
+ Assert.ok(
+ false,
+ "This test is running on too small a display - " +
+ `(${STARTING_WIDTH}x${STARTING_HEIGHT} min)`
+ );
+ return;
+ }
+
+ await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+
+ await withPerfObserver(
+ async function () {
+ await resizeWindow(win, SMALL_WIDTH, SMALL_HEIGHT);
+ await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+ },
+ { expectedReflows: EXPECTED_REFLOWS, frames: { filter: () => [] } },
+ win
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/performance/browser_windowclose.js b/browser/base/content/test/performance/browser_windowclose.js
new file mode 100644
index 0000000000..26eecf9539
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowclose.js
@@ -0,0 +1,58 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing windows. When the
+ * window is closed, the test waits until the original window
+ * has activated.
+ */
+add_task(async function () {
+ // Ensure that this browser window starts focused. This seems to be
+ // necessary to avoid intermittent failures when running this test
+ // on repeat.
+ await new Promise(resolve => {
+ waitForFocus(resolve, window);
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await new Promise(resolve => {
+ waitForFocus(resolve, win);
+ });
+
+ // At the time of writing, there are no reflows on window closing.
+ // Mochitest will fail if we have no assertions, so we add one here
+ // to make sure nobody adds any new ones.
+ Assert.equal(
+ EXPECTED_REFLOWS.length,
+ 0,
+ "We shouldn't have added any new expected reflows for window close."
+ );
+
+ await withPerfObserver(
+ async function () {
+ let promiseOrigBrowserFocused = TestUtils.waitForCondition(() => {
+ return Services.focus.activeWindow == window;
+ });
+ await BrowserTestUtils.closeWindow(win);
+ await promiseOrigBrowserFocused;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ },
+ win
+ );
+});
diff --git a/browser/base/content/test/performance/browser_windowopen.js b/browser/base/content/test/performance/browser_windowopen.js
new file mode 100644
index 0000000000..ebc924c3c6
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowopen.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://firefox-source-docs.mozilla.org/performance/bestpractices.html
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+// We'll assume the changes we are seeing are due to this focus change if
+// there are at least 5 areas that changed near the top of the screen, or if
+// the toolbar background is involved on OSX, but will only ignore this once.
+function isLikelyFocusChange(rects) {
+ if (rects.length > 5 && rects.every(r => r.y2 < 100)) {
+ return true;
+ }
+ if (
+ Services.appinfo.OS == "Darwin" &&
+ rects.length == 2 &&
+ rects.every(r => r.y1 == 0 && r.h == 33)
+ ) {
+ return true;
+ }
+ return false;
+}
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows or flickering areas when opening new windows.
+ */
+add_task(async function () {
+ // Flushing all caches helps to ensure that we get consistent
+ // behaviour when opening a new window, even if windows have been
+ // opened in previous tests.
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ Services.obs.notifyObservers(null, "chrome-flush-caches");
+
+ let bookmarksToolbarRect = await getBookmarksToolbarRect();
+
+ let win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no,remote,suppressanimation",
+ "about:home"
+ );
+
+ await disableFxaBadge();
+
+ let alreadyFocused = false;
+ let inRange = (val, min, max) => min <= val && val <= max;
+ let expectations = {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter(rects, frame, previousFrame) {
+ // The first screenshot we get in OSX / Windows shows an unfocused browser
+ // window for some reason. See bug 1445161.
+ if (!alreadyFocused && isLikelyFocusChange(rects)) {
+ alreadyFocused = true;
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ return [];
+ }
+
+ return rects;
+ },
+ exceptions: [
+ {
+ name: "bug 1421463 - reload toolbar icon shouldn't flicker",
+ condition: r =>
+ inRange(r.h, 13, 14) &&
+ inRange(r.w, 14, 16) && // icon size
+ inRange(r.y1, 40, 80) && // in the toolbar
+ inRange(r.x1, 65, 100), // near the left side of the screen
+ },
+ {
+ name: "bug 1555842 - the urlbar shouldn't flicker",
+ condition: r => {
+ let inputFieldRect = win.gURLBar.inputField.getBoundingClientRect();
+
+ return (
+ (!AppConstants.DEBUG ||
+ (AppConstants.platform == "linux" && AppConstants.ASAN)) &&
+ r.x1 >= inputFieldRect.left &&
+ r.x2 <= inputFieldRect.right &&
+ r.y1 >= inputFieldRect.top &&
+ r.y2 <= inputFieldRect.bottom
+ );
+ },
+ },
+ {
+ name: "Initial bookmark icon appearing after startup",
+ condition: r =>
+ r.w == 16 &&
+ r.h == 16 && // icon size
+ inRange(
+ r.y1,
+ bookmarksToolbarRect.top,
+ bookmarksToolbarRect.top + bookmarksToolbarRect.height / 2
+ ) && // in the toolbar
+ inRange(r.x1, 11, 13), // very close to the left of the screen
+ },
+ {
+ // Note that the length and x values here are a bit weird because on
+ // some fonts, we appear to detect the two words separately.
+ name: "Initial bookmark text ('Getting Started' or 'Get Involved') appearing after startup",
+ condition: r =>
+ inRange(r.w, 25, 120) && // length of text
+ inRange(r.h, 9, 15) && // height of text
+ inRange(
+ r.y1,
+ bookmarksToolbarRect.top,
+ bookmarksToolbarRect.top + bookmarksToolbarRect.height / 2
+ ) && // in the toolbar
+ inRange(r.x1, 30, 90), // close to the left of the screen
+ },
+ ],
+ },
+ };
+
+ await withPerfObserver(
+ async function () {
+ // Avoid showing the remotecontrol UI.
+ await new Promise(resolve => {
+ win.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ delete win.Marionette;
+ win.Marionette = { running: false };
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == win
+ );
+
+ let promises = [
+ BrowserTestUtils.firstBrowserLoaded(win, false),
+ BrowserTestUtils.browserStopped(
+ win.gBrowser.selectedBrowser,
+ "about:home"
+ ),
+ ];
+
+ await Promise.all(promises);
+
+ await new Promise(resolve => {
+ // 10 is an arbitrary value here, it needs to be at least 2 to avoid
+ // races with code initializing itself using idle callbacks.
+ (function waitForIdle(count = 10) {
+ if (!count) {
+ resolve();
+ return;
+ }
+ Services.tm.idleDispatchToMainThread(() => {
+ waitForIdle(count - 1);
+ });
+ })();
+ });
+ },
+ expectations,
+ win
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/performance/file_empty.html b/browser/base/content/test/performance/file_empty.html
new file mode 100644
index 0000000000..865879c583
--- /dev/null
+++ b/browser/base/content/test/performance/file_empty.html
@@ -0,0 +1 @@
+<!-- this file intentionally left blank -->
diff --git a/browser/base/content/test/performance/head.js b/browser/base/content/test/performance/head.js
new file mode 100644
index 0000000000..bcfe4ba9be
--- /dev/null
+++ b/browser/base/content/test/performance/head.js
@@ -0,0 +1,971 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PerfTestHelpers: "resource://testing-common/PerfTestHelpers.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+});
+
+/**
+ * This function can be called if the test needs to trigger frame dirtying
+ * outside of the normal mechanism.
+ *
+ * @param win (dom window)
+ * The window in which the frame tree needs to be marked as dirty.
+ */
+function dirtyFrame(win) {
+ let dwu = win.windowUtils;
+ try {
+ dwu.ensureDirtyRootFrame();
+ } catch (e) {
+ // If this fails, we should probably make note of it, but it's not fatal.
+ info("Note: ensureDirtyRootFrame threw an exception:" + e);
+ }
+}
+
+/**
+ * Async utility function to collect the stacks of uninterruptible reflows
+ * occuring during some period of time in a window.
+ *
+ * @param testPromise (Promise)
+ * A promise that is resolved when the data collection should stop.
+ *
+ * @param win (browser window, optional)
+ * The browser window to monitor. Defaults to the current window.
+ *
+ * @return An array of reflow stacks
+ */
+async function recordReflows(testPromise, win = window) {
+ // Collect all reflow stacks, we'll process them later.
+ let reflows = [];
+
+ let observer = {
+ reflow(start, end) {
+ // Gather information about the current code path.
+ reflows.push(new Error().stack);
+
+ // Just in case, dirty the frame now that we've reflowed.
+ dirtyFrame(win);
+ },
+
+ reflowInterruptible(start, end) {
+ // Interruptible reflows are the reflows caused by the refresh
+ // driver ticking. These are fine.
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIReflowObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ let docShell = win.docShell;
+ docShell.addWeakReflowObserver(observer);
+
+ let dirtyFrameFn = event => {
+ if (event.type != "MozAfterPaint") {
+ dirtyFrame(win);
+ }
+ };
+ Services.els.addListenerForAllEvents(win, dirtyFrameFn, true);
+
+ try {
+ dirtyFrame(win);
+ await testPromise;
+ } finally {
+ Services.els.removeListenerForAllEvents(win, dirtyFrameFn, true);
+ docShell.removeWeakReflowObserver(observer);
+ }
+
+ return reflows;
+}
+
+/**
+ * Utility function to report unexpected reflows.
+ *
+ * @param reflows (Array)
+ * An array of reflow stacks returned by recordReflows.
+ *
+ * @param expectedReflows (Array, optional)
+ * An Array of Objects representing reflows.
+ *
+ * Example:
+ *
+ * [
+ * {
+ * // This reflow is caused by lorem ipsum.
+ * // Sometimes, due to unpredictable timings, the reflow may be hit
+ * // less times.
+ * stack: [
+ * "select@chrome://global/content/bindings/textbox.xml",
+ * "focusAndSelectUrlBar@chrome://browser/content/browser.js",
+ * "openLinkIn@chrome://browser/content/utilityOverlay.js",
+ * "openUILinkIn@chrome://browser/content/utilityOverlay.js",
+ * "BrowserOpenTab@chrome://browser/content/browser.js",
+ * ],
+ * // We expect this particular reflow to happen up to 2 times.
+ * maxCount: 2,
+ * },
+ *
+ * {
+ * // This reflow is caused by lorem ipsum. We expect this reflow
+ * // to only happen once, so we can omit the "maxCount" property.
+ * stack: [
+ * "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml",
+ * "_fillTrailingGap@chrome://browser/content/tabbrowser.xml",
+ * "_handleNewTab@chrome://browser/content/tabbrowser.xml",
+ * "onxbltransitionend@chrome://browser/content/tabbrowser.xml",
+ * ],
+ * }
+ * ]
+ *
+ * Note that line numbers are not included in the stacks.
+ *
+ * Order of the reflows doesn't matter. Expected reflows that aren't seen
+ * will cause an assertion failure. When this argument is not passed,
+ * it defaults to the empty Array, meaning no reflows are expected.
+ */
+function reportUnexpectedReflows(reflows, expectedReflows = []) {
+ let knownReflows = expectedReflows.map(r => {
+ return {
+ stack: r.stack,
+ path: r.stack.join("|"),
+ count: 0,
+ maxCount: r.maxCount || 1,
+ actualStacks: new Map(),
+ };
+ });
+ let unexpectedReflows = new Map();
+
+ if (knownReflows.some(r => r.path.includes("*"))) {
+ Assert.ok(
+ false,
+ "Do not include async frames in the stack, as " +
+ "that feature is not available on all trees."
+ );
+ }
+
+ for (let stack of reflows) {
+ let path = stack
+ .split("\n")
+ .slice(1) // the first frame which is our test code.
+ .map(line => line.replace(/:\d+:\d+$/, "")) // strip line numbers.
+ .join("|");
+
+ // Stack trace is empty. Reflow was triggered by native code, which
+ // we ignore.
+ if (path === "") {
+ continue;
+ }
+
+ // Functions from EventUtils.js calculate coordinates and
+ // dimensions, causing us to reflow. That's the test
+ // harness and we don't care about that, so we'll filter that out.
+ if (
+ /^(synthesize|send|createDragEventObject).*?@chrome:\/\/mochikit.*?EventUtils\.js/.test(
+ path
+ )
+ ) {
+ continue;
+ }
+
+ let index = knownReflows.findIndex(reflow => path.startsWith(reflow.path));
+ if (index != -1) {
+ let reflow = knownReflows[index];
+ ++reflow.count;
+ reflow.actualStacks.set(stack, (reflow.actualStacks.get(stack) || 0) + 1);
+ } else {
+ unexpectedReflows.set(stack, (unexpectedReflows.get(stack) || 0) + 1);
+ }
+ }
+
+ let formatStack = stack =>
+ stack
+ .split("\n")
+ .slice(1)
+ .map(frame => " " + frame)
+ .join("\n");
+ for (let reflow of knownReflows) {
+ let firstFrame = reflow.stack[0];
+ if (!reflow.count) {
+ Assert.ok(
+ false,
+ `Unused expected reflow at ${firstFrame}:\nStack:\n` +
+ reflow.stack.map(frame => " " + frame).join("\n") +
+ "\n" +
+ "This is probably a good thing - just remove it from the list of reflows."
+ );
+ } else {
+ if (reflow.count > reflow.maxCount) {
+ Assert.ok(
+ false,
+ `reflow at ${firstFrame} was encountered ${reflow.count} times,\n` +
+ `it was expected to happen up to ${reflow.maxCount} times.`
+ );
+ } else {
+ todo(
+ false,
+ `known reflow at ${firstFrame} was encountered ${reflow.count} times`
+ );
+ }
+ for (let [stack, count] of reflow.actualStacks) {
+ info(
+ "Full stack" +
+ (count > 1 ? ` (hit ${count} times)` : "") +
+ ":\n" +
+ formatStack(stack)
+ );
+ }
+ }
+ }
+
+ for (let [stack, count] of unexpectedReflows) {
+ let location = stack.split("\n")[1].replace(/:\d+:\d+$/, "");
+ Assert.ok(
+ false,
+ `unexpected reflow at ${location} hit ${count} times\n` +
+ "Stack:\n" +
+ formatStack(stack)
+ );
+ }
+ Assert.ok(
+ !unexpectedReflows.size,
+ unexpectedReflows.size + " unexpected reflows"
+ );
+}
+
+async function ensureNoPreloadedBrowser(win = window) {
+ // If we've got a preloaded browser, get rid of it so that it
+ // doesn't interfere with the test if it's loading. We have to
+ // do this before we disable preloading or changing the new tab
+ // URL, otherwise _getPreloadedBrowser will return null, despite
+ // the preloaded browser existing.
+ NewTabPagePreloading.removePreloadedBrowser(win);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtab.preload", false]],
+ });
+
+ AboutNewTab.newTabURL = "about:blank";
+
+ registerCleanupFunction(() => {
+ AboutNewTab.resetNewTabURL();
+ });
+}
+
+// Onboarding puts a badge on the fxa toolbar button a while after startup
+// which confuses tests that look at repaints in the toolbar. Use this
+// function to cancel the badge update.
+function disableFxaBadge() {
+ let { ToolbarBadgeHub } = ChromeUtils.import(
+ "resource://activity-stream/lib/ToolbarBadgeHub.jsm"
+ );
+ ToolbarBadgeHub.removeAllNotifications();
+
+ // Also prevent a new timer from being set
+ return SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.toolbar.accessed", true]],
+ });
+}
+
+function rectInBoundingClientRect(r, bcr) {
+ return (
+ bcr.x <= r.x1 &&
+ bcr.y <= r.y1 &&
+ bcr.x + bcr.width >= r.x2 &&
+ bcr.y + bcr.height >= r.y2
+ );
+}
+
+async function getBookmarksToolbarRect() {
+ // Temporarily open the bookmarks toolbar to measure its rect
+ let bookmarksToolbar = gNavToolbox.querySelector("#PersonalToolbar");
+ let wasVisible = !bookmarksToolbar.collapsed;
+ if (!wasVisible) {
+ setToolbarVisibility(bookmarksToolbar, true, false, false);
+ await TestUtils.waitForCondition(
+ () => bookmarksToolbar.getBoundingClientRect().height > 0,
+ "wait for non-zero bookmarks toolbar height"
+ );
+ }
+ let bookmarksToolbarRect = bookmarksToolbar.getBoundingClientRect();
+ if (!wasVisible) {
+ setToolbarVisibility(bookmarksToolbar, false, false, false);
+ await TestUtils.waitForCondition(
+ () => bookmarksToolbar.getBoundingClientRect().height == 0,
+ "wait for zero bookmarks toolbar height"
+ );
+ }
+ return bookmarksToolbarRect;
+}
+
+async function prepareSettledWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureNoPreloadedBrowser(win);
+ return win;
+}
+
+/**
+ * Calculate and return how many additional tabs can be fit into the
+ * tabstrip without causing it to overflow.
+ *
+ * @return int
+ * The maximum additional tabs that can be fit into the
+ * tabstrip without causing it to overflow.
+ */
+function computeMaxTabCount() {
+ let currentTabCount = gBrowser.tabs.length;
+ let newTabButton = gBrowser.tabContainer.newTabButton;
+ let newTabRect = newTabButton.getBoundingClientRect();
+ let tabStripRect =
+ gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let availableTabStripWidth = tabStripRect.width - newTabRect.width;
+
+ let tabMinWidth = parseInt(
+ getComputedStyle(gBrowser.selectedTab, null).minWidth,
+ 10
+ );
+
+ let maxTabCount =
+ Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount;
+ Assert.ok(
+ maxTabCount > 0,
+ "Tabstrip needs to be wide enough to accomodate at least 1 more tab " +
+ "without overflowing."
+ );
+ return maxTabCount;
+}
+
+/**
+ * Helper function that opens up some number of about:blank tabs, and wait
+ * until they're all fully open.
+ *
+ * @param howMany (int)
+ * How many about:blank tabs to open.
+ */
+async function createTabs(howMany) {
+ let uris = [];
+ while (howMany--) {
+ uris.push("about:blank");
+ }
+
+ gBrowser.loadTabs(uris, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await TestUtils.waitForCondition(() => {
+ return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
+ });
+}
+
+/**
+ * Removes all of the tabs except the originally selected
+ * tab, and waits until all of the DOM nodes have been
+ * completely removed from the tab strip.
+ */
+async function removeAllButFirstTab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.warnOnCloseOtherTabs", false]],
+ });
+ gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+ await TestUtils.waitForCondition(() => gBrowser.tabs.length == 1);
+ await SpecialPowers.popPrefEnv();
+}
+
+/**
+ * Adds some entries to the Places database so that we can
+ * do semi-realistic look-ups in the URL bar.
+ *
+ * @param searchStr (string)
+ * Optional text to add to the search history items.
+ */
+async function addDummyHistoryEntries(searchStr = "") {
+ await PlacesUtils.history.clear();
+ const NUM_VISITS = 10;
+ let visits = [];
+
+ for (let i = 0; i < NUM_VISITS; ++i) {
+ visits.push({
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ uri: `http://example.com/urlbar-reflows-${i}`,
+ title: `Reflow test for URL bar entry #${i} - ${searchStr}`,
+ });
+ }
+
+ await PlacesTestUtils.addVisits(visits);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+}
+
+/**
+ * Async utility function to capture a screenshot of each painted frame.
+ *
+ * @param testPromise (Promise)
+ * A promise that is resolved when the data collection should stop.
+ *
+ * @param win (browser window, optional)
+ * The browser window to monitor. Defaults to the current window.
+ *
+ * @return An array of screenshots
+ */
+async function recordFrames(testPromise, win = window) {
+ let canvas = win.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.mozOpaque = true;
+ let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
+
+ let frames = [];
+
+ let afterPaintListener = event => {
+ let width, height;
+ canvas.width = width = win.innerWidth;
+ canvas.height = height = win.innerHeight;
+ ctx.drawWindow(
+ win,
+ 0,
+ 0,
+ width,
+ height,
+ "white",
+ ctx.DRAWWINDOW_DO_NOT_FLUSH |
+ ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS
+ );
+ let data = Cu.cloneInto(ctx.getImageData(0, 0, width, height).data, {});
+ if (frames.length) {
+ // Compare this frame with the previous one to avoid storing duplicate
+ // frames and running out of memory.
+ let previous = frames[frames.length - 1];
+ if (previous.width == width && previous.height == height) {
+ let equals = true;
+ for (let i = 0; i < data.length; ++i) {
+ if (data[i] != previous.data[i]) {
+ equals = false;
+ break;
+ }
+ }
+ if (equals) {
+ return;
+ }
+ }
+ }
+ frames.push({ data, width, height });
+ };
+ win.addEventListener("MozAfterPaint", afterPaintListener);
+
+ // If the test is using an existing window, capture a frame immediately.
+ if (win.document.readyState == "complete") {
+ afterPaintListener();
+ }
+
+ try {
+ await testPromise;
+ } finally {
+ win.removeEventListener("MozAfterPaint", afterPaintListener);
+ }
+
+ return frames;
+}
+
+// How many identical pixels to accept between 2 rects when deciding to merge
+// them.
+const kMaxEmptyPixels = 3;
+function compareFrames(frame, previousFrame) {
+ // Accessing the Math global is expensive as the test executes in a
+ // non-syntactic scope. Accessing it as a lexical variable is enough
+ // to make the code JIT well.
+ const M = Math;
+
+ function expandRect(x, y, rect) {
+ if (rect.x2 < x) {
+ rect.x2 = x;
+ } else if (rect.x1 > x) {
+ rect.x1 = x;
+ }
+ if (rect.y2 < y) {
+ rect.y2 = y;
+ }
+ }
+
+ function isInRect(x, y, rect) {
+ return (
+ (rect.y2 == y || rect.y2 == y - 1) && rect.x1 - 1 <= x && x <= rect.x2 + 1
+ );
+ }
+
+ if (
+ frame.height != previousFrame.height ||
+ frame.width != previousFrame.width
+ ) {
+ // If the frames have different sizes, assume the whole window has
+ // been repainted when the window was resized.
+ return [{ x1: 0, x2: frame.width, y1: 0, y2: frame.height }];
+ }
+
+ let l = frame.data.length;
+ let different = [];
+ let rects = [];
+ for (let i = 0; i < l; i += 4) {
+ let x = (i / 4) % frame.width;
+ let y = M.floor(i / 4 / frame.width);
+ for (let j = 0; j < 4; ++j) {
+ let index = i + j;
+
+ if (frame.data[index] != previousFrame.data[index]) {
+ let found = false;
+ for (let rect of rects) {
+ if (isInRect(x, y, rect)) {
+ expandRect(x, y, rect);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ rects.unshift({ x1: x, x2: x, y1: y, y2: y });
+ }
+
+ different.push(i);
+ break;
+ }
+ }
+ }
+ rects.reverse();
+
+ // The following code block merges rects that are close to each other
+ // (less than kMaxEmptyPixels away).
+ // This is needed to avoid having a rect for each letter when a label moves.
+ let areRectsContiguous = function (r1, r2) {
+ return (
+ r1.y2 >= r2.y1 - 1 - kMaxEmptyPixels &&
+ r2.x1 - 1 - kMaxEmptyPixels <= r1.x2 &&
+ r2.x2 >= r1.x1 - 1 - kMaxEmptyPixels
+ );
+ };
+ let hasMergedRects;
+ do {
+ hasMergedRects = false;
+ for (let r = rects.length - 1; r > 0; --r) {
+ let rr = rects[r];
+ for (let s = r - 1; s >= 0; --s) {
+ let rs = rects[s];
+ if (areRectsContiguous(rs, rr)) {
+ rs.x1 = Math.min(rs.x1, rr.x1);
+ rs.y1 = Math.min(rs.y1, rr.y1);
+ rs.x2 = Math.max(rs.x2, rr.x2);
+ rs.y2 = Math.max(rs.y2, rr.y2);
+ rects.splice(r, 1);
+ hasMergedRects = true;
+ break;
+ }
+ }
+ }
+ } while (hasMergedRects);
+
+ // For convenience, pre-compute the width and height of each rect.
+ rects.forEach(r => {
+ r.w = r.x2 - r.x1 + 1;
+ r.h = r.y2 - r.y1 + 1;
+ });
+
+ return rects;
+}
+
+function dumpFrame({ data, width, height }) {
+ let canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.mozOpaque = true;
+ canvas.width = width;
+ canvas.height = height;
+
+ canvas
+ .getContext("2d", { alpha: false, willReadFrequently: true })
+ .putImageData(new ImageData(data, width, height), 0, 0);
+
+ info(canvas.toDataURL());
+}
+
+/**
+ * Utility function to report unexpected changed areas on screen.
+ *
+ * @param frames (Array)
+ * An array of frames captured by recordFrames.
+ *
+ * @param expectations (Object)
+ * An Object indicating which changes on screen are expected.
+ * If can contain the following optional fields:
+ * - filter: a function used to exclude changed rects that are expected.
+ * It takes the following parameters:
+ * - rects: an array of changed rects
+ * - frame: the current frame
+ * - previousFrame: the previous frame
+ * It returns an array of rects. This array is typically a copy of
+ * the rects parameter, from which identified expected changes have
+ * been excluded.
+ * - exceptions: an array of objects describing known flicker bugs.
+ * Example:
+ * exceptions: [
+ * {name: "bug 1nnnnnn - the foo icon shouldn't flicker",
+ * condition: r => r.w == 14 && r.y1 == 0 && ... }
+ * },
+ * {name: "bug ...
+ * ]
+ */
+function reportUnexpectedFlicker(frames, expectations) {
+ info("comparing " + frames.length + " frames");
+
+ let unexpectedRects = 0;
+ for (let i = 1; i < frames.length; ++i) {
+ let frame = frames[i],
+ previousFrame = frames[i - 1];
+ let rects = compareFrames(frame, previousFrame);
+
+ if (expectations.filter) {
+ rects = expectations.filter(rects, frame, previousFrame);
+ }
+
+ rects = rects.filter(rect => {
+ let rectText = `${rect.toSource()}, window width: ${frame.width}`;
+ for (let e of expectations.exceptions || []) {
+ if (e.condition(rect)) {
+ todo(false, e.name + ", " + rectText);
+ return false;
+ }
+ }
+
+ ok(false, "unexpected changed rect: " + rectText);
+ return true;
+ });
+
+ if (!rects.length) {
+ continue;
+ }
+
+ // Before dumping a frame with unexpected differences for the first time,
+ // ensure at least one previous frame has been logged so that it's possible
+ // to see the differences when examining the log.
+ if (!unexpectedRects) {
+ dumpFrame(previousFrame);
+ }
+ unexpectedRects += rects.length;
+ dumpFrame(frame);
+ }
+ is(unexpectedRects, 0, "should have 0 unknown flickering areas");
+}
+
+/**
+ * This is the main function that performance tests in this folder will call.
+ *
+ * The general idea is that individual tests provide a test function (testFn)
+ * that will perform some user interactions we care about (eg. open a tab), and
+ * this withPerfObserver function takes care of setting up and removing the
+ * observers and listener we need to detect common performance issues.
+ *
+ * Once testFn is done, withPerfObserver will analyse the collected data and
+ * report anything unexpected.
+ *
+ * @param testFn (async function)
+ * An async function that exercises some part of the browser UI.
+ *
+ * @param exceptions (object, optional)
+ * An Array of Objects representing expectations and known issues.
+ * It can contain the following fields:
+ * - expectedReflows: an array of expected reflow stacks.
+ * (see the comment above reportUnexpectedReflows for an example)
+ * - frames: an object setting expectations for what will change
+ * on screen during the test, and the known flicker bugs.
+ * (see the comment above reportUnexpectedFlicker for an example)
+ */
+async function withPerfObserver(testFn, exceptions = {}, win = window) {
+ let resolveFn, rejectFn;
+ let promiseTestDone = new Promise((resolve, reject) => {
+ resolveFn = resolve;
+ rejectFn = reject;
+ });
+
+ let promiseReflows = recordReflows(promiseTestDone, win);
+ let promiseFrames = recordFrames(promiseTestDone, win);
+
+ testFn().then(resolveFn, rejectFn);
+ await promiseTestDone;
+
+ let reflows = await promiseReflows;
+ reportUnexpectedReflows(reflows, exceptions.expectedReflows);
+
+ let frames = await promiseFrames;
+ reportUnexpectedFlicker(frames, exceptions.frames);
+}
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when typing into the URL bar
+ * with the default values in Places.
+ *
+ * @param {bool} keyed
+ * Pass true to synthesize typing the search string one key at a time.
+ * @param {array} expectedReflowsFirstOpen
+ * The array of expected reflow stacks when the panel is first opened.
+ * @param {array} [expectedReflowsSecondOpen]
+ * The array of expected reflow stacks when the panel is subsequently
+ * opened, if you're testing opening the panel twice.
+ */
+async function runUrlbarTest(
+ keyed,
+ expectedReflowsFirstOpen,
+ expectedReflowsSecondOpen = null
+) {
+ const SEARCH_TERM = keyed ? "" : "urlbar-reflows-" + Date.now();
+ await addDummyHistoryEntries(SEARCH_TERM);
+
+ let win = await prepareSettledWindow();
+
+ let URLBar = win.gURLBar;
+
+ URLBar.focus();
+ URLBar.value = SEARCH_TERM;
+ let testFn = async function () {
+ let popup = URLBar.view;
+ let oldOnQueryResults = popup.onQueryResults.bind(popup);
+ let oldOnQueryFinished = popup.onQueryFinished.bind(popup);
+
+ // We need to invalidate the frame tree outside of the normal
+ // mechanism since invalidations and result additions to the
+ // URL bar occur without firing JS events (which is how we
+ // normally know to dirty the frame tree).
+ popup.onQueryResults = context => {
+ dirtyFrame(win);
+ oldOnQueryResults(context);
+ };
+
+ popup.onQueryFinished = context => {
+ dirtyFrame(win);
+ oldOnQueryFinished(context);
+ };
+
+ let waitExtra = async () => {
+ // There are several setTimeout(fn, 0); calls inside autocomplete.xml
+ // that we need to wait for. Since those have higher priority than
+ // idle callbacks, we can be sure they will have run once this
+ // idle callback is called. The timeout seems to be required in
+ // automation - presumably because the machines can be pretty busy
+ // especially if it's GC'ing from previous tests.
+ await new Promise(resolve =>
+ win.requestIdleCallback(resolve, { timeout: 1000 })
+ );
+ };
+
+ if (keyed) {
+ // Only keying in 6 characters because the number of reflows triggered
+ // is so high that we risk timing out the test if we key in any more.
+ let searchTerm = "ows-10";
+ for (let i = 0; i < searchTerm.length; ++i) {
+ let char = searchTerm[i];
+ EventUtils.synthesizeKey(char, {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ await waitExtra();
+ }
+ } else {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus: SimpleTest.waitForFocus,
+ value: URLBar.value,
+ });
+ await waitExtra();
+ }
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ };
+
+ let urlbarRect = URLBar.textbox.getBoundingClientRect();
+ const SHADOW_SIZE = 17;
+ let expectedRects = {
+ filter: rects => {
+ // We put text into the urlbar so expect its textbox to change.
+ // We expect many changes in the results view.
+ // So we just allow changes anywhere in the urlbar. We don't check the
+ // bottom of the rect because the result view height varies depending on
+ // the results.
+ // We use floor/ceil because the Urlbar dimensions aren't always
+ // integers.
+ return rects.filter(
+ r =>
+ !(
+ r.x1 >= Math.floor(urlbarRect.left) - SHADOW_SIZE &&
+ r.x2 <= Math.ceil(urlbarRect.right) + SHADOW_SIZE &&
+ r.y1 >= Math.floor(urlbarRect.top) - SHADOW_SIZE
+ )
+ );
+ },
+ };
+
+ info("First opening");
+ await withPerfObserver(
+ testFn,
+ { expectedReflows: expectedReflowsFirstOpen, frames: expectedRects },
+ win
+ );
+
+ if (expectedReflowsSecondOpen) {
+ info("Second opening");
+ await withPerfObserver(
+ testFn,
+ { expectedReflows: expectedReflowsSecondOpen, frames: expectedRects },
+ win
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+}
+
+/**
+ * Helper method for checking which scripts are loaded on content process
+ * startup, used by `browser_startup_content.js` and
+ * `browser_startup_content_subframe.js`.
+ *
+ * Parameters to this function are passed in an object literal to avoid
+ * confusion about parameter order.
+ *
+ * @param loadedInfo (Object)
+ * Mapping from script type to a set of scripts which have been loaded
+ * of that type.
+ *
+ * @param known (Object)
+ * Mapping from script type to a set of scripts which must have been
+ * loaded of that type.
+ *
+ * @param intermittent (Object)
+ * Mapping from script type to a set of scripts which may have been
+ * loaded of that type. There must be a script type map for every type
+ * in `known`.
+ *
+ * @param forbidden (Object)
+ * Mapping from script type to a set of scripts which must not have been
+ * loaded of that type.
+ *
+ * @param dumpAllStacks (bool)
+ * If true, dump the stacks for all loaded modules. Makes the output
+ * noisy.
+ */
+async function checkLoadedScripts({
+ loadedInfo,
+ known,
+ intermittent,
+ forbidden,
+ dumpAllStacks,
+}) {
+ let loadedList = {};
+
+ async function checkAllExist(scriptType, list, listType) {
+ if (scriptType == "services") {
+ for (let contract of list) {
+ ok(
+ contract in Cc,
+ `${listType} entry ${contract} for content process startup must exist`
+ );
+ }
+ } else {
+ let results = await PerfTestHelpers.throttledMapPromises(
+ list,
+ async uri => ({
+ uri,
+ exists: await PerfTestHelpers.checkURIExists(uri),
+ })
+ );
+
+ for (let { uri, exists } of results) {
+ ok(
+ exists,
+ `${listType} entry ${uri} for content process startup must exist`
+ );
+ }
+ }
+ }
+
+ for (let scriptType in known) {
+ loadedList[scriptType] = Object.keys(loadedInfo[scriptType]).filter(c => {
+ if (!known[scriptType].has(c)) {
+ return true;
+ }
+ known[scriptType].delete(c);
+ return false;
+ });
+
+ loadedList[scriptType] = loadedList[scriptType].filter(c => {
+ return !intermittent[scriptType].has(c);
+ });
+
+ if (loadedList[scriptType].length) {
+ console.log("Unexpected scripts:", loadedList[scriptType]);
+ }
+ is(
+ loadedList[scriptType].length,
+ 0,
+ `should have no unexpected ${scriptType} loaded on content process startup`
+ );
+
+ for (let script of loadedList[scriptType]) {
+ record(
+ false,
+ `Unexpected ${scriptType} loaded during content process startup: ${script}`,
+ undefined,
+ loadedInfo[scriptType][script]
+ );
+ }
+
+ await checkAllExist(scriptType, intermittent[scriptType], "intermittent");
+
+ is(
+ known[scriptType].size,
+ 0,
+ `all known ${scriptType} scripts should have been loaded`
+ );
+
+ for (let script of known[scriptType]) {
+ ok(
+ false,
+ `${scriptType} is expected to load for content process startup but wasn't: ${script}`
+ );
+ }
+
+ if (dumpAllStacks) {
+ info(`Stacks for all loaded ${scriptType}:`);
+ for (let file in loadedInfo[scriptType]) {
+ if (loadedInfo[scriptType][file]) {
+ info(
+ `${file}\n------------------------------------\n` +
+ loadedInfo[scriptType][file] +
+ "\n"
+ );
+ }
+ }
+ }
+ }
+
+ for (let scriptType in forbidden) {
+ for (let script of forbidden[scriptType]) {
+ let loaded = script in loadedInfo[scriptType];
+ if (loaded) {
+ record(
+ false,
+ `Forbidden ${scriptType} loaded during content process startup: ${script}`,
+ undefined,
+ loadedInfo[scriptType][script]
+ );
+ }
+ }
+
+ await checkAllExist(scriptType, forbidden[scriptType], "forbidden");
+ }
+}
diff --git a/browser/base/content/test/performance/hidpi/browser.ini b/browser/base/content/test/performance/hidpi/browser.ini
new file mode 100644
index 0000000000..5375700ee8
--- /dev/null
+++ b/browser/base/content/test/performance/hidpi/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+prefs =
+ browser.startup.recordImages=true
+ layout.css.devPixelsPerPx='2'
+
+[../browser_startup_images.js]
+skip-if = !debug || (os == 'win' && (os_version == '6.1'))
diff --git a/browser/base/content/test/performance/io/browser.ini b/browser/base/content/test/performance/io/browser.ini
new file mode 100644
index 0000000000..7f4b66365e
--- /dev/null
+++ b/browser/base/content/test/performance/io/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+# Currently disabled on debug due to debug-only failures, see bug 1549723.
+# Disabled on Linux asan due to bug 1549729.
+# Disabled on Windows asan due to intermittent startup hangs, bug 1629824.
+skip-if =
+ debug
+ tsan
+ asan
+# to avoid overhead when running the browser normally, StartupRecorder.sys.mjs will
+# do almost nothing unless browser.startup.record is true.
+# gfx.canvas.willReadFrequently.enable is just an optimization, but needs to be
+# set during early startup to have an impact as a canvas will be used by
+# StartupRecorder.sys.mjs
+prefs =
+ browser.startup.record=true
+ gfx.canvas.willReadFrequently.enable=true
+ # The Screenshots extension is disabled by default in Mochitests. We re-enable
+ # it here, since it's a more realistic configuration.
+ extensions.screenshots.disabled=false
+environment =
+ GNOME_ACCESSIBILITY=0
+ MOZ_PROFILER_STARTUP=1
+ MOZ_PROFILER_STARTUP_PERFORMANCE_TEST=1
+ MOZ_PROFILER_STARTUP_FEATURES=js,mainthreadio
+ MOZ_PROFILER_STARTUP_ENTRIES=10000000
+[../browser_startup_content_mainthreadio.js]
+[../browser_startup_mainthreadio.js]
+skip-if =
+ apple_silicon # bug 1707724
+ socketprocess_networking
+ os == 'win' && bits == 32
+ os == 'win' && msix # Bug 1833639
+[../browser_startup_syncIPC.js]
diff --git a/browser/base/content/test/performance/lowdpi/browser.ini b/browser/base/content/test/performance/lowdpi/browser.ini
new file mode 100644
index 0000000000..5dc4efc5ac
--- /dev/null
+++ b/browser/base/content/test/performance/lowdpi/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs =
+ browser.startup.recordImages=true
+ layout.css.devPixelsPerPx='1'
+
+[../browser_startup_images.js]
+skip-if = !debug
+
diff --git a/browser/base/content/test/performance/moz.build b/browser/base/content/test/performance/moz.build
new file mode 100644
index 0000000000..eb5df95729
--- /dev/null
+++ b/browser/base/content/test/performance/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser.ini",
+ "hidpi/browser.ini",
+ "io/browser.ini",
+ "lowdpi/browser.ini",
+]
+
+TESTING_JS_MODULES += [
+ "PerfTestHelpers.sys.mjs",
+]
diff --git a/browser/base/content/test/performance/triage.json b/browser/base/content/test/performance/triage.json
new file mode 100644
index 0000000000..a5f367c745
--- /dev/null
+++ b/browser/base/content/test/performance/triage.json
@@ -0,0 +1,62 @@
+{
+ "triagers": {
+ "Gijs": {
+ "bzmail": "gijskruitbosch+bugs@gmail.com"
+ },
+ "Mike Conley": {
+ "bzmail": "mconley@mozilla.com"
+ },
+ "Florian Quèze": {
+ "bzmail": "florian@mozilla.com"
+ },
+ "Doug Thayer": {
+ "bzmail": "dothayer@mozilla.com"
+ }
+ },
+ "duty-start-dates": {
+ "2023-03-23": "Gijs Kruitbosch",
+ "2023-03-30": "Mike Conley",
+ "2023-04-06": "Florian Quèze",
+ "2023-04-13": "Doug Thayer",
+ "2023-04-20": "Gijs Kruitbosch",
+ "2023-04-27": "Mike Conley",
+ "2023-05-04": "Doug Thayer",
+ "2023-05-11": "Gijs Kruitbosch",
+ "2023-05-18": "Mike Conley",
+ "2023-05-25": "Doug Thayer",
+ "2023-06-01": "Gijs Kruitbosch",
+ "2023-06-08": "Mike Conley",
+ "2023-06-15": "Doug Thayer",
+ "2023-06-22": "Gijs Kruitbosch",
+ "2023-06-29": "Mike Conley",
+ "2023-07-06": "Doug Thayer",
+ "2023-07-13": "Gijs Kruitbosch",
+ "2023-07-20": "Mike Conley",
+ "2023-07-27": "Florian Quèze",
+ "2023-08-03": "Doug Thayer",
+ "2023-08-10": "Gijs Kruitbosch",
+ "2023-08-17": "Mike Conley",
+ "2023-08-24": "Florian Quèze",
+ "2023-08-31": "Doug Thayer",
+ "2023-09-07": "Gijs Kruitbosch",
+ "2023-09-14": "Mike Conley",
+ "2023-09-21": "Florian Quèze",
+ "2023-09-28": "Doug Thayer",
+ "2023-10-05": "Gijs Kruitbosch",
+ "2023-10-12": "Mike Conley",
+ "2023-10-19": "Florian Quèze",
+ "2023-10-26": "Doug Thayer",
+ "2023-11-02": "Gijs Kruitbosch",
+ "2023-11-09": "Mike Conley",
+ "2023-11-16": "Florian Quèze",
+ "2023-11-23": "Doug Thayer",
+ "2023-11-30": "Gijs Kruitbosch",
+ "2023-12-07": "Mike Conley",
+ "2023-12-14": "Florian Quèze",
+ "2023-12-21": "Doug Thayer",
+ "2023-12-28": "Gijs Kruitbosch",
+ "2024-01-04": "Mike Conley",
+ "2024-01-11": "Florian Quèze",
+ "2024-01-18": "Doug Thayer"
+ }
+}
diff --git a/browser/base/content/test/perftest.ini b/browser/base/content/test/perftest.ini
new file mode 100644
index 0000000000..d4351967e4
--- /dev/null
+++ b/browser/base/content/test/perftest.ini
@@ -0,0 +1 @@
+[perftest_browser_xhtml_dom.js]
diff --git a/browser/base/content/test/perftest_browser_xhtml_dom.js b/browser/base/content/test/perftest_browser_xhtml_dom.js
new file mode 100644
index 0000000000..f3cc00ec49
--- /dev/null
+++ b/browser/base/content/test/perftest_browser_xhtml_dom.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-env node */
+"use strict";
+
+/* global module */
+async function test(context, commands) {
+ await context.selenium.driver.setContext("chrome");
+ let elementData = await context.selenium.driver.executeAsyncScript(
+ function () {
+ let callback = arguments[arguments.length - 1];
+ (async function () {
+ let lightDOM = document.querySelectorAll("*");
+ let elementsWithoutIDs = {};
+ let idElements = [];
+ let lightDOMDetails = { idElements, elementsWithoutIDs };
+ lightDOM.forEach(n => {
+ if (n.id) {
+ idElements.push(n.id);
+ } else {
+ if (!elementsWithoutIDs.hasOwnProperty(n.localName)) {
+ elementsWithoutIDs[n.localName] = 0;
+ }
+ elementsWithoutIDs[n.localName]++;
+ }
+ });
+ let lightDOMCount = lightDOM.length;
+
+ // Recursively explore shadow DOM:
+ function getShadowElements(root) {
+ let allElems = Array.from(root.querySelectorAll("*"));
+ let shadowRoots = allElems.map(n => n.openOrClosedShadowRoot);
+ for (let innerRoot of shadowRoots) {
+ if (innerRoot) {
+ allElems.push(getShadowElements(innerRoot));
+ }
+ }
+ return allElems;
+ }
+ let totalDOMCount = Array.from(lightDOM, node => {
+ if (node.openOrClosedShadowRoot) {
+ return [node].concat(
+ getShadowElements(node.openOrClosedShadowRoot)
+ );
+ }
+ return node;
+ }).flat().length;
+ let panelMenuCount = document.querySelectorAll(
+ "panel,menupopup,popup,popupnotification"
+ ).length;
+ return {
+ panelMenuCount,
+ lightDOMCount,
+ totalDOMCount,
+ lightDOMDetails,
+ };
+ })().then(callback);
+ }
+ );
+ let { lightDOMDetails } = elementData;
+ delete elementData.lightDOMDetails;
+ lightDOMDetails.idElements.sort();
+ for (let id of lightDOMDetails.idElements) {
+ console.log(id);
+ }
+ console.log("Elements without ids:");
+ for (let [localName, count] of Object.entries(
+ lightDOMDetails.elementsWithoutIDs
+ )) {
+ console.log(count.toString().padStart(4) + " " + localName);
+ }
+ console.log(elementData);
+ await context.selenium.driver.setContext("content");
+ await commands.measure.start("data:text/html,BrowserDOM");
+ commands.measure.addObject(elementData);
+}
+
+module.exports = {
+ test,
+ owner: "Browser Front-end team",
+ name: "Dom-size",
+ description: "Measures the size of the DOM",
+ supportedBrowsers: ["Desktop"],
+ supportedPlatforms: ["Windows", "Linux", "macOS"],
+};
diff --git a/browser/base/content/test/permissions/browser.ini b/browser/base/content/test/permissions/browser.ini
new file mode 100644
index 0000000000..216c0efb05
--- /dev/null
+++ b/browser/base/content/test/permissions/browser.ini
@@ -0,0 +1,41 @@
+[DEFAULT]
+support-files=
+ head.js
+ permissions.html
+ temporary_permissions_subframe.html
+ temporary_permissions_frame.html
+[browser_autoplay_blocked.js]
+support-files =
+ browser_autoplay_blocked.html
+ browser_autoplay_blocked_slow.sjs
+ browser_autoplay_js.html
+ browser_autoplay_muted.html
+ ../general/audio.ogg
+skip-if = true # Bug 1538602
+[browser_canvas_fingerprinting_resistance.js]
+skip-if =
+ debug
+ os == "linux" && asan # Bug 1522069
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_canvas_rfp_exclusion.js]
+[browser_permission_delegate_geo.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_permissions.js]
+[browser_permissions_delegate_vibrate.js]
+support-files=
+ empty.html
+[browser_permissions_handling_user_input.js]
+support-files=
+ dummy.js
+[browser_permissions_postPrompt.js]
+support-files=
+ dummy.js
+[browser_reservedkey.js]
+[browser_site_scoped_permissions.js]
+[browser_temporary_permissions.js]
+support-files =
+ ../webrtc/get_user_media.html
+[browser_temporary_permissions_expiry.js]
+[browser_temporary_permissions_navigation.js]
+[browser_temporary_permissions_tabs.js]
diff --git a/browser/base/content/test/permissions/browser_autoplay_blocked.html b/browser/base/content/test/permissions/browser_autoplay_blocked.html
new file mode 100644
index 0000000000..8c3b058890
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <audio autoplay="autoplay" >
+ <source src="audio.ogg" />
+ </audio>
+ </body>
+</html>
diff --git a/browser/base/content/test/permissions/browser_autoplay_blocked.js b/browser/base/content/test/permissions/browser_autoplay_blocked.js
new file mode 100644
index 0000000000..04b0316345
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked.js
@@ -0,0 +1,357 @@
+/*
+ * Test that a blocked request to autoplay media is shown to the user
+ */
+
+const AUTOPLAY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "browser_autoplay_blocked.html";
+
+const AUTOPLAY_JS_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "browser_autoplay_js.html";
+
+const SLOW_AUTOPLAY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "browser_autoplay_blocked_slow.sjs";
+
+const MUTED_AUTOPLAY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "browser_autoplay_muted.html";
+
+const EMPTY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+const AUTOPLAY_PREF = "media.autoplay.default";
+const AUTOPLAY_PERM = "autoplay-media";
+
+function autoplayBlockedIcon() {
+ return document.querySelector(
+ "#blocked-permissions-container " +
+ ".blocked-permission-icon.autoplay-media-icon"
+ );
+}
+
+function permissionListBlockedIcons() {
+ return document.querySelectorAll(
+ "image.permission-popup-permission-icon.blocked-permission-icon"
+ );
+}
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function blockedIconShown() {
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(autoplayBlockedIcon());
+ }, "Blocked icon is shown");
+}
+
+async function blockedIconHidden() {
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_hidden(autoplayBlockedIcon());
+ }, "Blocked icon is hidden");
+}
+
+function testPermListHasEntries(expectEntries) {
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let listEntryCount = permissionsList.querySelectorAll(
+ ".permission-popup-permission-item"
+ ).length;
+ if (expectEntries) {
+ ok(listEntryCount, "List of permissions is not empty");
+ return;
+ }
+ ok(!listEntryCount, "List of permissions is empty");
+}
+
+add_setup(async function () {
+ registerCleanupFunction(() => {
+ Services.perms.removeAll();
+ Services.prefs.clearUserPref(AUTOPLAY_PREF);
+ });
+});
+
+add_task(async function testMainViewVisible() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.ALLOWED);
+
+ await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function () {
+ ok(
+ BrowserTestUtils.is_hidden(autoplayBlockedIcon()),
+ "Blocked icon not shown"
+ );
+
+ await openPermissionPopup();
+ testPermListHasEntries(false);
+ await closePermissionPopup();
+ });
+
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function (browser) {
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+
+ await blockedIconShown();
+
+ await openPermissionPopup();
+ testPermListHasEntries(true);
+
+ let labelText = SitePermissions.getPermissionLabel(AUTOPLAY_PERM);
+ let labels = permissionsList.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+ is(labels[0].textContent, labelText, "Correct value");
+
+ let menulist = document.getElementById("permission-popup-menulist");
+ Assert.equal(menulist.label, "Block Audio");
+
+ await EventUtils.synthesizeMouseAtCenter(menulist, { type: "mousedown" });
+ await TestUtils.waitForCondition(() => {
+ return (
+ menulist.getElementsByTagName("menuitem")[0].label ===
+ "Allow Audio and Video"
+ );
+ });
+
+ let menuitem = menulist.getElementsByTagName("menuitem")[0];
+ Assert.equal(menuitem.getAttribute("label"), "Allow Audio and Video");
+
+ menuitem.click();
+ menulist.menupopup.hidePopup();
+ await closePermissionPopup();
+
+ let uri = Services.io.newURI(AUTOPLAY_PAGE);
+ let state = PermissionTestUtils.getPermissionObject(
+ uri,
+ AUTOPLAY_PERM
+ ).capability;
+ Assert.equal(state, Services.perms.ALLOW_ACTION);
+ });
+
+ Services.perms.removeAll();
+});
+
+add_task(async function testGloballyBlockedOnNewWindow() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ AUTOPLAY_PAGE
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ AUTOPLAY_PAGE
+ );
+ await blockedIconShown();
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(
+ principal,
+ AUTOPLAY_PERM,
+ tab.linkedBrowser
+ ),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ }
+ );
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(tab);
+ let win = await promiseWin;
+ tab = win.gBrowser.selectedTab;
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(
+ principal,
+ AUTOPLAY_PERM,
+ tab.linkedBrowser
+ ),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ }
+ );
+
+ SitePermissions.removeFromPrincipal(
+ principal,
+ AUTOPLAY_PERM,
+ tab.linkedBrowser
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function testBFCache() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ await BrowserTestUtils.withNewTab("about:home", async function (browser) {
+ BrowserTestUtils.loadURIString(browser, AUTOPLAY_PAGE);
+ await blockedIconShown();
+
+ gBrowser.goBack();
+ await blockedIconHidden();
+
+ // Not sure why using `gBrowser.goForward()` doesn't trigger document's
+ // visibility changes in some debug build on try server, which makes us not
+ // to receive the blocked event.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.history.forward();
+ });
+ await blockedIconShown();
+ });
+
+ Services.perms.removeAll();
+});
+
+add_task(async function testBlockedIconFromCORSIframe() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ await BrowserTestUtils.withNewTab(EMPTY_PAGE, async browser => {
+ const blockedIconShownPromise = blockedIconShown();
+ const CORS_AUTOPLAY_PAGE = AUTOPLAY_PAGE.replace(
+ "example.com",
+ "example.org"
+ );
+ info(`Load CORS autoplay on an iframe`);
+ await SpecialPowers.spawn(browser, [CORS_AUTOPLAY_PAGE], async url => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ info("Wait until iframe finishes loading");
+ await new Promise(r => (iframe.onload = r));
+ });
+ await blockedIconShownPromise;
+ ok(true, "Blocked icon shown for the CORS autoplay iframe");
+ });
+
+ Services.perms.removeAll();
+});
+
+add_task(async function testChangingBlockingSettingDuringNavigation() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ await BrowserTestUtils.withNewTab("about:home", async function (browser) {
+ await blockedIconHidden();
+ BrowserTestUtils.loadURIString(browser, AUTOPLAY_PAGE);
+ await blockedIconShown();
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.ALLOWED);
+
+ gBrowser.goBack();
+ await blockedIconHidden();
+
+ gBrowser.goForward();
+
+ // Sleep here to prevent false positives, the icon gets shown with an
+ // async `GloballyAutoplayBlocked` event. The sleep gives it a little
+ // time for it to show otherwise there is a chance it passes before it
+ // would have shown.
+ await sleep(100);
+ ok(
+ BrowserTestUtils.is_hidden(autoplayBlockedIcon()),
+ "Blocked icon is hidden"
+ );
+ });
+
+ Services.perms.removeAll();
+});
+
+add_task(async function testSlowLoadingPage() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SLOW_AUTOPLAY_PAGE
+ );
+ await blockedIconShown();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ // Wait until the blocked icon is hidden by switching tabs
+ await blockedIconHidden();
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await blockedIconShown();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ Services.perms.removeAll();
+});
+
+add_task(async function testBlockedAll() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED_ALL);
+
+ await BrowserTestUtils.withNewTab("about:home", async function (browser) {
+ await blockedIconHidden();
+ BrowserTestUtils.loadURIString(browser, MUTED_AUTOPLAY_PAGE);
+ await blockedIconShown();
+
+ await openPermissionPopup();
+
+ Assert.equal(
+ permissionListBlockedIcons().length,
+ 1,
+ "Blocked icon is shown"
+ );
+
+ let menulist = document.getElementById("permission-popup-menulist");
+ await EventUtils.synthesizeMouseAtCenter(menulist, { type: "mousedown" });
+ await TestUtils.waitForCondition(() => {
+ return (
+ menulist.getElementsByTagName("menuitem")[1].label === "Block Audio"
+ );
+ });
+
+ let menuitem = menulist.getElementsByTagName("menuitem")[0];
+ menuitem.click();
+ menulist.menupopup.hidePopup();
+ await closePermissionPopup();
+ gBrowser.reload();
+ await blockedIconHidden();
+ });
+ Services.perms.removeAll();
+});
+
+add_task(async function testMultiplePlayNotificationsFromJS() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ await BrowserTestUtils.withNewTab("about:home", async function (browser) {
+ let count = 0;
+ browser.addEventListener("GloballyAutoplayBlocked", function () {
+ is(++count, 1, "Shouldn't get more than one autoplay blocked event");
+ });
+
+ await blockedIconHidden();
+
+ BrowserTestUtils.loadURIString(browser, AUTOPLAY_JS_PAGE);
+
+ await blockedIconShown();
+
+ // Sleep here a bit to ensure that multiple events don't arrive.
+ await sleep(100);
+
+ is(count, 1, "Shouldn't have got more events");
+ });
+
+ Services.perms.removeAll();
+});
diff --git a/browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs b/browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs
new file mode 100644
index 0000000000..12929760f7
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const DELAY_MS = 200;
+
+const AUTOPLAY_HTML = `<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <audio autoplay="autoplay" >
+ <source src="audio.ogg" />
+ </audio>
+ <script>
+ document.location.href = '#foo';
+ </script>
+ </body>
+</html>`;
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+ resp.setHeader("Cache-Control", "no-cache", false);
+ resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ resp.write(AUTOPLAY_HTML);
+ timer.init(
+ () => {
+ resp.write("");
+ resp.finish();
+ },
+ DELAY_MS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/browser/base/content/test/permissions/browser_autoplay_js.html b/browser/base/content/test/permissions/browser_autoplay_js.html
new file mode 100644
index 0000000000..9782487ee9
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_js.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<meta charset="utf8">
+<audio>
+ <source src="audio.ogg" />
+</audio>
+<script>
+onload = function() {
+ let audio = document.querySelector("audio");
+ for (let i = 0; i < 100; ++i) {
+ audio.play();
+ }
+};
+</script>
diff --git a/browser/base/content/test/permissions/browser_autoplay_muted.html b/browser/base/content/test/permissions/browser_autoplay_muted.html
new file mode 100644
index 0000000000..4f9d1ca846
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_muted.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <audio autoplay="autoplay" muted>
+ <source src="audio.ogg" />
+ </audio>
+ </body>
+</html>
diff --git a/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js
new file mode 100644
index 0000000000..dbb2d1ea32
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js
@@ -0,0 +1,383 @@
+/**
+ * When "privacy.resistFingerprinting" is set to true, user permission is
+ * required for canvas data extraction.
+ * This tests whether the site permission prompt for canvas data extraction
+ * works properly.
+ * When "privacy.resistFingerprinting.randomDataOnCanvasExtract" is true,
+ * canvas data extraction results in random data, and when it is false, canvas
+ * data extraction results in all-white data.
+ */
+"use strict";
+
+const kUrl = "https://example.com/";
+const kPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(kUrl),
+ {}
+);
+const kPermission = "canvas";
+
+function initTab() {
+ let contentWindow = content.wrappedJSObject;
+
+ let drawCanvas = (fillStyle, id) => {
+ let contentDocument = contentWindow.document;
+ let width = 64,
+ height = 64;
+ let canvas = contentDocument.createElement("canvas");
+ if (id) {
+ canvas.setAttribute("id", id);
+ }
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+ contentDocument.body.appendChild(canvas);
+
+ let context = canvas.getContext("2d");
+ context.fillStyle = fillStyle;
+ context.fillRect(0, 0, width, height);
+
+ if (id) {
+ let button = contentDocument.createElement("button");
+ button.addEventListener("click", function () {
+ canvas.toDataURL();
+ });
+ button.setAttribute("id", "clickme");
+ button.innerHTML = "Click Me!";
+ contentDocument.body.appendChild(button);
+ }
+
+ return canvas;
+ };
+
+ let placeholder = drawCanvas("white");
+ contentWindow.kPlaceholderData = placeholder.toDataURL();
+ let canvas = drawCanvas("cyan", "canvas-id-canvas");
+ contentWindow.kPlacedData = canvas.toDataURL();
+ is(
+ canvas.toDataURL(),
+ contentWindow.kPlacedData,
+ "privacy.resistFingerprinting = false, canvas data == placed data"
+ );
+ isnot(
+ canvas.toDataURL(),
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = false, canvas data != placeholder data"
+ );
+}
+
+function enableResistFingerprinting(
+ randomDataOnCanvasExtract,
+ autoDeclineNoInput
+) {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", true],
+ [
+ "privacy.resistFingerprinting.randomDataOnCanvasExtract",
+ randomDataOnCanvasExtract,
+ ],
+ [
+ "privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts",
+ autoDeclineNoInput,
+ ],
+ ],
+ });
+}
+
+function promisePopupShown() {
+ return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+}
+
+function promisePopupHidden() {
+ return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
+}
+
+function extractCanvasData(randomDataOnCanvasExtract, grantPermission) {
+ let contentWindow = content.wrappedJSObject;
+ let canvas = contentWindow.document.getElementById("canvas-id-canvas");
+ let canvasData = canvas.toDataURL();
+ if (grantPermission) {
+ is(
+ canvasData,
+ contentWindow.kPlacedData,
+ "privacy.resistFingerprinting = true, permission granted, canvas data == placed data"
+ );
+ if (!randomDataOnCanvasExtract) {
+ isnot(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission granted, canvas data != placeholderdata"
+ );
+ }
+ } else if (grantPermission === false) {
+ isnot(
+ canvasData,
+ contentWindow.kPlacedData,
+ "privacy.resistFingerprinting = true, permission denied, canvas data != placed data"
+ );
+ if (!randomDataOnCanvasExtract) {
+ is(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission denied, canvas data == placeholderdata"
+ );
+ } else {
+ isnot(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, permission denied, canvas data != placeholderdata"
+ );
+ }
+ } else {
+ isnot(
+ canvasData,
+ contentWindow.kPlacedData,
+ "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data"
+ );
+ if (!randomDataOnCanvasExtract) {
+ is(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, requesting permission, canvas data == placeholderdata"
+ );
+ } else {
+ isnot(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, requesting permission, canvas data != placeholderdata"
+ );
+ }
+ }
+}
+
+function triggerCommand(button) {
+ let notifications = PopupNotifications.panel.children;
+ let notification = notifications[0];
+ EventUtils.synthesizeMouseAtCenter(notification[button], {});
+}
+
+function triggerMainCommand() {
+ triggerCommand("button");
+}
+
+function triggerSecondaryCommand() {
+ triggerCommand("secondaryButton");
+}
+
+function testPermission() {
+ return Services.perms.testPermissionFromPrincipal(kPrincipal, kPermission);
+}
+
+async function withNewTabNoInput(
+ randomDataOnCanvasExtract,
+ grantPermission,
+ browser
+) {
+ await SpecialPowers.spawn(browser, [], initTab);
+ await enableResistFingerprinting(randomDataOnCanvasExtract, false);
+ let popupShown = promisePopupShown();
+ await SpecialPowers.spawn(
+ browser,
+ [randomDataOnCanvasExtract],
+ extractCanvasData
+ );
+ await popupShown;
+ let popupHidden = promisePopupHidden();
+ if (grantPermission) {
+ triggerMainCommand();
+ await popupHidden;
+ is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted");
+ } else {
+ triggerSecondaryCommand();
+ await popupHidden;
+ is(testPermission(), Services.perms.DENY_ACTION, "permission denied");
+ }
+ await SpecialPowers.spawn(
+ browser,
+ [randomDataOnCanvasExtract, grantPermission],
+ extractCanvasData
+ );
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doTestNoInput(randomDataOnCanvasExtract, grantPermission) {
+ await BrowserTestUtils.withNewTab(
+ kUrl,
+ withNewTabNoInput.bind(null, randomDataOnCanvasExtract, grantPermission)
+ );
+ Services.perms.removeFromPrincipal(kPrincipal, kPermission);
+}
+
+// With auto-declining disabled (not the default)
+// Tests clicking "Don't Allow" button of the permission prompt.
+add_task(doTestNoInput.bind(null, true, false));
+add_task(doTestNoInput.bind(null, false, false));
+
+// Tests clicking "Allow" button of the permission prompt.
+add_task(doTestNoInput.bind(null, true, true));
+add_task(doTestNoInput.bind(null, false, true));
+
+async function withNewTabAutoBlockNoInput(randomDataOnCanvasExtract, browser) {
+ await SpecialPowers.spawn(browser, [], initTab);
+ await enableResistFingerprinting(randomDataOnCanvasExtract, true);
+
+ let noShowHandler = () => {
+ ok(false, "The popup notification should not show in this case.");
+ };
+ PopupNotifications.panel.addEventListener("popupshown", noShowHandler, {
+ once: true,
+ });
+
+ let promisePopupObserver = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+
+ // Try to extract canvas data without user inputs.
+ await SpecialPowers.spawn(
+ browser,
+ [randomDataOnCanvasExtract],
+ extractCanvasData
+ );
+
+ await promisePopupObserver;
+ info("There should be no popup shown on the panel.");
+
+ // Check that the icon of canvas permission is shown.
+ let canvasNotification = PopupNotifications.getNotification(
+ "canvas-permissions-prompt",
+ browser
+ );
+
+ is(
+ canvasNotification.anchorElement.getAttribute("showing"),
+ "true",
+ "The canvas permission icon is correctly shown."
+ );
+ PopupNotifications.panel.removeEventListener("popupshown", noShowHandler);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doTestAutoBlockNoInput(randomDataOnCanvasExtract) {
+ await BrowserTestUtils.withNewTab(
+ kUrl,
+ withNewTabAutoBlockNoInput.bind(null, randomDataOnCanvasExtract)
+ );
+}
+
+add_task(doTestAutoBlockNoInput.bind(null, true));
+add_task(doTestAutoBlockNoInput.bind(null, false));
+
+function extractCanvasDataUserInput(
+ randomDataOnCanvasExtract,
+ grantPermission
+) {
+ let contentWindow = content.wrappedJSObject;
+ let canvas = contentWindow.document.getElementById("canvas-id-canvas");
+ let canvasData = canvas.toDataURL();
+ if (grantPermission) {
+ is(
+ canvasData,
+ contentWindow.kPlacedData,
+ "privacy.resistFingerprinting = true, permission granted, canvas data == placed data"
+ );
+ if (!randomDataOnCanvasExtract) {
+ isnot(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission granted, canvas data != placeholderdata"
+ );
+ }
+ } else if (grantPermission === false) {
+ isnot(
+ canvasData,
+ contentWindow.kPlacedData,
+ "privacy.resistFingerprinting = true, permission denied, canvas data != placed data"
+ );
+ if (!randomDataOnCanvasExtract) {
+ is(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission denied, canvas data == placeholderdata"
+ );
+ } else {
+ isnot(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, permission denied, canvas data != placeholderdata"
+ );
+ }
+ } else {
+ isnot(
+ canvasData,
+ contentWindow.kPlacedData,
+ "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data"
+ );
+ if (!randomDataOnCanvasExtract) {
+ is(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, requesting permission, canvas data == placeholderdata"
+ );
+ } else {
+ isnot(
+ canvasData,
+ contentWindow.kPlaceholderData,
+ "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, requesting permission, canvas data != placeholderdata"
+ );
+ }
+ }
+}
+
+async function withNewTabInput(
+ randomDataOnCanvasExtract,
+ grantPermission,
+ browser
+) {
+ await SpecialPowers.spawn(browser, [], initTab);
+ await enableResistFingerprinting(randomDataOnCanvasExtract, true);
+ let popupShown = promisePopupShown();
+ await SpecialPowers.spawn(browser, [], function (host) {
+ E10SUtils.wrapHandlingUserInput(content, true, function () {
+ var button = content.document.getElementById("clickme");
+ button.click();
+ });
+ });
+ await popupShown;
+ let popupHidden = promisePopupHidden();
+ if (grantPermission) {
+ triggerMainCommand();
+ await popupHidden;
+ is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted");
+ } else {
+ triggerSecondaryCommand();
+ await popupHidden;
+ is(testPermission(), Services.perms.DENY_ACTION, "permission denied");
+ }
+ await SpecialPowers.spawn(
+ browser,
+ [randomDataOnCanvasExtract, grantPermission],
+ extractCanvasDataUserInput
+ );
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doTestInput(
+ randomDataOnCanvasExtract,
+ grantPermission,
+ autoDeclineNoInput
+) {
+ await BrowserTestUtils.withNewTab(
+ kUrl,
+ withNewTabInput.bind(null, randomDataOnCanvasExtract, grantPermission)
+ );
+ Services.perms.removeFromPrincipal(kPrincipal, kPermission);
+}
+
+// With auto-declining enabled (the default)
+// Tests clicking "Don't Allow" button of the permission prompt.
+add_task(doTestInput.bind(null, true, false));
+add_task(doTestInput.bind(null, false, false));
+
+// Tests clicking "Allow" button of the permission prompt.
+add_task(doTestInput.bind(null, true, true));
+add_task(doTestInput.bind(null, false, true));
diff --git a/browser/base/content/test/permissions/browser_canvas_rfp_exclusion.js b/browser/base/content/test/permissions/browser_canvas_rfp_exclusion.js
new file mode 100644
index 0000000000..61c9c5bf84
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_canvas_rfp_exclusion.js
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Adapted from browser_canvas_fingerprinting_resistance.js
+ */
+"use strict";
+
+const kUrl = "https://example.com/";
+var gPlacedData = false;
+
+function initTab(performReadbackTest) {
+ let contentWindow = content.wrappedJSObject;
+
+ let drawCanvas = (fillStyle, id) => {
+ let contentDocument = contentWindow.document;
+ let width = 64,
+ height = 64;
+ let canvas = contentDocument.createElement("canvas");
+ if (id) {
+ canvas.setAttribute("id", id);
+ }
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+ contentDocument.body.appendChild(canvas);
+
+ let context = canvas.getContext("2d");
+ context.fillStyle = fillStyle;
+ context.fillRect(0, 0, width, height);
+ return canvas;
+ };
+
+ let canvas = drawCanvas("cyan", "canvas-id-canvas");
+
+ let placedData = canvas.toDataURL();
+ if (performReadbackTest) {
+ is(
+ canvas.toDataURL(),
+ placedData,
+ "Reading the placed data twice didn't match"
+ );
+ return placedData;
+ }
+ return undefined;
+}
+
+function disableResistFingerprinting() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", false],
+ ["privacy.resistFingerprinting.pbmode", false],
+ ],
+ });
+}
+
+function enableResistFingerprinting(RfpNonPbmExclusion, RfpDomainExclusion) {
+ if (RfpNonPbmExclusion && RfpDomainExclusion) {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.pbmode", true],
+ ["privacy.resistFingerprinting.exemptedDomains", "example.com"],
+ ],
+ });
+ } else if (RfpNonPbmExclusion) {
+ return SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting.pbmode", true]],
+ });
+ } else if (RfpDomainExclusion) {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", true],
+ ["privacy.resistFingerprinting.exemptedDomains", "example.com"],
+ ],
+ });
+ }
+ return SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+}
+
+function extractCanvasData(
+ placedData,
+ isPbm,
+ RfpNonPbmExclusion,
+ RfpDomainExclusion
+) {
+ let contentWindow = content.wrappedJSObject;
+ let canvas = contentWindow.document.getElementById("canvas-id-canvas");
+ let canvasData = canvas.toDataURL();
+
+ if (RfpDomainExclusion) {
+ is(
+ canvasData,
+ placedData,
+ `A: RFP, domain exempted, canvas data == placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})`
+ );
+ } else if (!isPbm && RfpNonPbmExclusion) {
+ is(
+ canvasData,
+ placedData,
+ `B: RFP, nonPBM exempted, not in PBM, canvas data == placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})`
+ );
+ } else if (isPbm && RfpNonPbmExclusion) {
+ isnot(
+ canvasData,
+ placedData,
+ `C: RFP, nonPBM exempted, in PBM, canvas data != placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})`
+ );
+ } else {
+ isnot(
+ canvasData,
+ placedData,
+ `D: RFP, domain not exempted, nonPBM not exempted, canvas data != placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})`
+ );
+ }
+}
+
+async function populatePlacedData() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await disableResistFingerprinting();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: kUrl,
+ },
+ async function () {
+ let browser = win.gBrowser.selectedBrowser;
+ gPlacedData = await SpecialPowers.spawn(
+ browser,
+ [/* performReadbackTest= */ true],
+ initTab
+ );
+ }
+ );
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+}
+
+async function rfpExclusionTestOnCanvas(
+ win,
+ placedData,
+ isPbm,
+ RfpNonPbmExclusion,
+ RfpDomainExclusion
+) {
+ let browser = win.gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(
+ browser,
+ [/* performReadbackTest= */ false],
+ initTab
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [placedData, isPbm, RfpNonPbmExclusion, RfpDomainExclusion],
+ extractCanvasData
+ );
+}
+
+async function testCanvasRfpExclusion(
+ isPbm,
+ RfpNonPbmExclusion,
+ RfpDomainExclusion
+) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: isPbm,
+ });
+ await enableResistFingerprinting(RfpNonPbmExclusion, RfpDomainExclusion);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: kUrl,
+ },
+ rfpExclusionTestOnCanvas.bind(
+ null,
+ win,
+ gPlacedData,
+ isPbm,
+ RfpNonPbmExclusion,
+ RfpDomainExclusion
+ )
+ );
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(populatePlacedData.bind(null));
+add_task(testCanvasRfpExclusion.bind(null, false, false, false));
+add_task(testCanvasRfpExclusion.bind(null, false, false, true));
+add_task(testCanvasRfpExclusion.bind(null, false, true, false));
+add_task(testCanvasRfpExclusion.bind(null, false, true, true));
+add_task(testCanvasRfpExclusion.bind(null, true, false, false));
+add_task(testCanvasRfpExclusion.bind(null, true, false, true));
+add_task(testCanvasRfpExclusion.bind(null, true, true, false));
+add_task(testCanvasRfpExclusion.bind(null, true, true, true));
diff --git a/browser/base/content/test/permissions/browser_permission_delegate_geo.js b/browser/base/content/test/permissions/browser_permission_delegate_geo.js
new file mode 100644
index 0000000000..45e78d4519
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permission_delegate_geo.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ORIGIN = "https://example.com";
+const CROSS_SUBFRAME_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "temporary_permissions_subframe.html";
+
+const CROSS_FRAME_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "temporary_permissions_frame.html";
+
+const PromptResult = {
+ ALLOW: "allow",
+ DENY: "deny",
+ PROMPT: "prompt",
+};
+
+var Perms = Services.perms;
+var uri = NetUtil.newURI(ORIGIN);
+var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {});
+
+async function checkNotificationBothOrigins(
+ firstPartyOrigin,
+ thirdPartyOrigin
+) {
+ // Notification is shown, check label and deny to clean
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ // Check the label of the notificaiton should be the first party
+ is(
+ PopupNotifications.getNotification("geolocation").options.name,
+ firstPartyOrigin,
+ "Use first party's origin"
+ );
+
+ // Check the second name of the notificaiton should be the third party
+ is(
+ PopupNotifications.getNotification("geolocation").options.secondName,
+ thirdPartyOrigin,
+ "Use third party's origin"
+ );
+
+ // Check remember checkbox is hidden
+ let checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(checkbox.hidden, "checkbox is not visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
+ await popuphidden;
+}
+
+async function checkGeolocation(browser, frameId, expect) {
+ let isPrompt = expect == PromptResult.PROMPT;
+ let waitForPrompt;
+ if (isPrompt) {
+ waitForPrompt = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ frameId, expect, isPrompt }],
+ async args => {
+ let frame = content.document.getElementById(args.frameId);
+
+ let waitForNoPrompt = new Promise(resolve => {
+ function onMessage(event) {
+ // Check the result right here because there's no notification
+ Assert.equal(
+ event.data,
+ args.expect,
+ "Correct expectation for third party"
+ );
+ content.window.removeEventListener("message", onMessage);
+ resolve();
+ }
+
+ if (!args.isPrompt) {
+ content.window.addEventListener("message", onMessage);
+ }
+ });
+
+ await content.SpecialPowers.spawn(frame, [], async () => {
+ const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+ );
+
+ E10SUtils.wrapHandlingUserInput(this.content, true, function () {
+ let frameDoc = this.content.document;
+ frameDoc.getElementById("geo").click();
+ });
+ });
+
+ if (!args.isPrompt) {
+ await waitForNoPrompt;
+ }
+ }
+ );
+
+ if (isPrompt) {
+ await waitForPrompt;
+ }
+}
+
+add_setup(async function () {
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ["permissions.delegation.enabled", true],
+ // This is the amount of time before the repeating
+ // NetworkGeolocationProvider timer is stopped.
+ // It needs to be less than 5000ms, or the timer will be
+ // reported as left behind by the test.
+ ["geo.timeout", 4000],
+ ],
+ },
+ r
+ );
+ });
+});
+
+// Test that temp blocked permissions in first party affect the third party
+// iframe.
+add_task(async function testUseTempPermissionsFirstParty() {
+ await BrowserTestUtils.withNewTab(
+ CROSS_SUBFRAME_PAGE,
+ async function (browser) {
+ SitePermissions.setForPrincipal(
+ principal,
+ "geo",
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ await checkGeolocation(browser, "frame", PromptResult.DENY);
+
+ SitePermissions.removeFromPrincipal(principal, "geo", browser);
+ }
+ );
+});
+
+// Test that persistent permissions in first party affect the third party
+// iframe.
+add_task(async function testUsePersistentPermissionsFirstParty() {
+ await BrowserTestUtils.withNewTab(
+ CROSS_SUBFRAME_PAGE,
+ async function (browser) {
+ async function checkPermission(aPermission, aExpect) {
+ PermissionTestUtils.add(uri, "geo", aPermission);
+ await checkGeolocation(browser, "frame", aExpect);
+
+ if (aExpect == PromptResult.PROMPT) {
+ // Notification is shown, check label and deny to clean
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ // Check the label of the notificaiton should be the first party
+ is(
+ PopupNotifications.getNotification("geolocation").options.name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
+
+ await popuphidden;
+ SitePermissions.removeFromPrincipal(null, "geo", browser);
+ }
+
+ PermissionTestUtils.remove(uri, "geo");
+ }
+
+ await checkPermission(Perms.PROMPT_ACTION, PromptResult.PROMPT);
+ await checkPermission(Perms.DENY_ACTION, PromptResult.DENY);
+ await checkPermission(Perms.ALLOW_ACTION, PromptResult.ALLOW);
+ }
+ );
+});
+
+// Test that we do not prompt for maybe unsafe permission delegation if the
+// origin of the page is the original src origin.
+add_task(async function testPromptInMaybeUnsafePermissionDelegation() {
+ await BrowserTestUtils.withNewTab(
+ CROSS_SUBFRAME_PAGE,
+ async function (browser) {
+ // Persistent allow top level origin
+ PermissionTestUtils.add(uri, "geo", Perms.ALLOW_ACTION);
+
+ await checkGeolocation(browser, "frameAllowsAll", PromptResult.ALLOW);
+
+ SitePermissions.removeFromPrincipal(null, "geo", browser);
+ PermissionTestUtils.remove(uri, "geo");
+ }
+ );
+});
+
+// Test that we should prompt if we are in unsafe permission delegation and
+// change location to origin which is not explicitly trusted. The prompt popup
+// should include both first and third party origin.
+add_task(async function testPromptChangeLocationUnsafePermissionDelegation() {
+ await BrowserTestUtils.withNewTab(
+ CROSS_SUBFRAME_PAGE,
+ async function (browser) {
+ // Persistent allow top level origin
+ PermissionTestUtils.add(uri, "geo", Perms.ALLOW_ACTION);
+
+ let iframe = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.getElementById("frameAllowsAll")
+ .browsingContext;
+ });
+
+ let otherURI =
+ "https://test1.example.com/browser/browser/base/content/test/permissions/permissions.html";
+ let loaded = BrowserTestUtils.browserLoaded(browser, true, otherURI);
+ await SpecialPowers.spawn(iframe, [otherURI], async function (_otherURI) {
+ content.location = _otherURI;
+ });
+ await loaded;
+
+ await checkGeolocation(browser, "frameAllowsAll", PromptResult.PROMPT);
+ await checkNotificationBothOrigins(uri.host, "test1.example.com");
+
+ SitePermissions.removeFromPrincipal(null, "geo", browser);
+ PermissionTestUtils.remove(uri, "geo");
+ }
+ );
+});
+
+// If we are in unsafe permission delegation and the origin is explicitly
+// trusted in ancestor chain. Do not need prompt
+add_task(async function testExplicitlyAllowedInChain() {
+ await BrowserTestUtils.withNewTab(CROSS_FRAME_PAGE, async function (browser) {
+ // Persistent allow top level origin
+ PermissionTestUtils.add(uri, "geo", Perms.ALLOW_ACTION);
+
+ let iframeAncestor = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.getElementById("frameAncestor").browsingContext;
+ });
+
+ let iframe = await SpecialPowers.spawn(iframeAncestor, [], () => {
+ return content.document.getElementById("frameAllowsAll").browsingContext;
+ });
+
+ // Change location to check that we actually look at the ancestor chain
+ // instead of just considering the "same origin as src" rule.
+ let otherURI =
+ "https://test2.example.com/browser/browser/base/content/test/permissions/permissions.html";
+ let loaded = BrowserTestUtils.browserLoaded(browser, true, otherURI);
+ await SpecialPowers.spawn(iframe, [otherURI], async function (_otherURI) {
+ content.location = _otherURI;
+ });
+ await loaded;
+
+ await checkGeolocation(
+ iframeAncestor,
+ "frameAllowsAll",
+ PromptResult.ALLOW
+ );
+
+ PermissionTestUtils.remove(uri, "geo");
+ });
+});
diff --git a/browser/base/content/test/permissions/browser_permissions.js b/browser/base/content/test/permissions/browser_permissions.js
new file mode 100644
index 0000000000..0843ed9119
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions.js
@@ -0,0 +1,569 @@
+/*
+ * Test the Permissions section in the Control Center.
+ */
+
+const PERMISSIONS_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "permissions.html";
+
+function testPermListHasEntries(expectEntries) {
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let listEntryCount = permissionsList.querySelectorAll(
+ ".permission-popup-permission-item"
+ ).length;
+ if (expectEntries) {
+ ok(listEntryCount, "List of permissions is not empty");
+ return;
+ }
+ ok(!listEntryCount, "List of permissions is empty");
+}
+
+add_task(async function testMainViewVisible() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function () {
+ await openPermissionPopup();
+
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ testPermListHasEntries(false);
+
+ await closePermissionPopup();
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+
+ let labelText = SitePermissions.getPermissionLabel("camera");
+ let labels = permissionsList.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+ is(labels[0].innerHTML, labelText, "Correct value");
+
+ let img = permissionsList.querySelector(
+ "image.permission-popup-permission-icon"
+ );
+ ok(img, "There is an image for the permissions");
+ ok(img.classList.contains("camera-icon"), "proper class is in image class");
+
+ await closePermissionPopup();
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(false);
+
+ await closePermissionPopup();
+ });
+});
+
+add_task(async function testIdentityIcon() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function () {
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.ALLOW_ACTION
+ );
+
+ ok(
+ gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-box signals granted permissions"
+ );
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+
+ ok(
+ !gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-box doesn't signal granted permissions"
+ );
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "not-a-site-permission",
+ Services.perms.ALLOW_ACTION
+ );
+
+ ok(
+ !gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-box doesn't signal granted permissions"
+ );
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ ok(
+ gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-box signals granted permissions"
+ );
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "cookie");
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_DENY
+ );
+
+ ok(
+ gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-box signals granted permissions"
+ );
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_DEFAULT
+ );
+
+ ok(
+ !gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-box doesn't signal granted permissions"
+ );
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+ PermissionTestUtils.remove(gBrowser.currentURI, "not-a-site-permission");
+ PermissionTestUtils.remove(gBrowser.currentURI, "cookie");
+ });
+});
+
+add_task(async function testCancelPermission() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function () {
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.DENY_ACTION
+ );
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+
+ permissionsList
+ .querySelector(".permission-popup-permission-remove-button")
+ .click();
+
+ is(
+ permissionsList.querySelectorAll(".permission-popup-permission-label")
+ .length,
+ 1,
+ "First permission should be removed"
+ );
+
+ permissionsList
+ .querySelector(".permission-popup-permission-remove-button")
+ .click();
+
+ is(
+ permissionsList.querySelectorAll(".permission-popup-permission-label")
+ .length,
+ 0,
+ "Second permission should be removed"
+ );
+
+ await closePermissionPopup();
+ });
+});
+
+add_task(async function testPermissionHints() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) {
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let reloadHint = document.getElementById(
+ "permission-popup-permission-reload-hint"
+ );
+
+ await openPermissionPopup();
+
+ ok(BrowserTestUtils.is_hidden(reloadHint), "Reload hint is hidden");
+
+ await closePermissionPopup();
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.DENY_ACTION
+ );
+
+ await openPermissionPopup();
+
+ ok(BrowserTestUtils.is_hidden(reloadHint), "Reload hint is hidden");
+
+ let cancelButtons = permissionsList.querySelectorAll(
+ ".permission-popup-permission-remove-button"
+ );
+ PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+
+ cancelButtons[0].click();
+ ok(!BrowserTestUtils.is_hidden(reloadHint), "Reload hint is visible");
+
+ cancelButtons[1].click();
+ ok(!BrowserTestUtils.is_hidden(reloadHint), "Reload hint is visible");
+
+ await closePermissionPopup();
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURIString(browser, PERMISSIONS_PAGE);
+ await loaded;
+ await openPermissionPopup();
+
+ ok(
+ BrowserTestUtils.is_hidden(reloadHint),
+ "Reload hint is hidden after reloading"
+ );
+
+ await closePermissionPopup();
+ });
+});
+
+add_task(async function testPermissionIcons() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function () {
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.DENY_ACTION
+ );
+
+ let geoIcon = gPermissionPanel._identityPermissionBox.querySelector(
+ ".blocked-permission-icon[data-permission-id='geo']"
+ );
+ ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
+
+ let cameraIcon = gPermissionPanel._identityPermissionBox.querySelector(
+ ".blocked-permission-icon[data-permission-id='camera']"
+ );
+ ok(
+ !cameraIcon.hasAttribute("showing"),
+ "allowed permission icon is not shown"
+ );
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+
+ ok(
+ !geoIcon.hasAttribute("showing"),
+ "blocked permission icon is not shown after reset"
+ );
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+ });
+});
+
+add_task(async function testPermissionShortcuts() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) {
+ browser.focus();
+
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["permissions.default.shortcuts", 0]] },
+ r
+ );
+ });
+
+ async function tryKey(desc, expectedValue) {
+ await EventUtils.synthesizeAndWaitKey("c", { accelKey: true });
+ let result = await SpecialPowers.spawn(browser, [], function () {
+ return {
+ keydowns: content.wrappedJSObject.gKeyDowns,
+ keypresses: content.wrappedJSObject.gKeyPresses,
+ };
+ });
+ is(
+ result.keydowns,
+ expectedValue,
+ "keydown event was fired or not fired as expected, " + desc
+ );
+ is(
+ result.keypresses,
+ 0,
+ "keypress event shouldn't be fired for shortcut key, " + desc
+ );
+ }
+
+ await tryKey("pressed with default permissions", 1);
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "shortcuts",
+ Services.perms.DENY_ACTION
+ );
+ await tryKey("pressed when site blocked", 1);
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "shortcuts",
+ PermissionTestUtils.ALLOW
+ );
+ await tryKey("pressed when site allowed", 2);
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "shortcuts");
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["permissions.default.shortcuts", 2]] },
+ r
+ );
+ });
+
+ await tryKey("pressed when globally blocked", 2);
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "shortcuts",
+ Services.perms.ALLOW_ACTION
+ );
+ await tryKey("pressed when globally blocked but site allowed", 3);
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "shortcuts",
+ Services.perms.DENY_ACTION
+ );
+ await tryKey("pressed when globally blocked and site blocked", 3);
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "shortcuts");
+ });
+});
+
+// Test the control center UI when policy permissions are set.
+add_task(async function testPolicyPermission() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+ });
+
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "popup",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_POLICY
+ );
+
+ await openPermissionPopup();
+
+ // Check if the icon, nameLabel and stateLabel are visible.
+ let img, labelText, labels;
+
+ img = permissionsList.querySelector(
+ "image.permission-popup-permission-icon"
+ );
+ ok(img, "There is an image for the popup permission");
+ ok(img.classList.contains("popup-icon"), "proper class is in image class");
+
+ labelText = SitePermissions.getPermissionLabel("popup");
+ labels = permissionsList.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+ is(labels[0].innerHTML, labelText, "Correct name label value");
+
+ labelText = SitePermissions.getCurrentStateLabel(
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_POLICY
+ );
+ labels = permissionsList.querySelectorAll(
+ ".permission-popup-permission-state-label"
+ );
+ is(labels[0].innerHTML, labelText, "Correct state label value");
+
+ // Check if the menulist and the remove button are hidden.
+ // The menulist is specific to the "popup" permission.
+ let menulist = document.getElementById("permission-popup-menulist");
+ ok(menulist == null, "The popup permission menulist is not visible");
+
+ let removeButton = permissionsList.querySelector(
+ ".permission-popup-permission-remove-button"
+ );
+ ok(removeButton == null, "The permission remove button is not visible");
+
+ Services.perms.removeAll();
+ await closePermissionPopup();
+ });
+});
+
+add_task(async function testHiddenAfterRefresh() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) {
+ ok(
+ BrowserTestUtils.is_hidden(gPermissionPanel._permissionPopup),
+ "Popup is hidden"
+ );
+
+ await openPermissionPopup();
+
+ ok(
+ !BrowserTestUtils.is_hidden(gPermissionPanel._permissionPopup),
+ "Popup is shown"
+ );
+
+ let reloaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ PERMISSIONS_PAGE
+ );
+ EventUtils.synthesizeKey("VK_F5", {}, browser.ownerGlobal);
+ await reloaded;
+
+ ok(
+ BrowserTestUtils.is_hidden(gPermissionPanel._permissionPopup),
+ "Popup is hidden"
+ );
+ });
+});
+
+add_task(async function test3rdPartyStoragePermission() {
+ // 3rdPartyStorage permissions are listed under an anchor container - test
+ // that this works correctly, i.e. the permission items are added to the
+ // anchor when relevant, and other permission items are added to the default
+ // anchor, and adding/removing permissions preserves this behavior correctly.
+
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) {
+ await openPermissionPopup();
+
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let storagePermissionAnchor = permissionsList.querySelector(
+ `.permission-popup-permission-list-anchor[anchorfor="3rdPartyStorage"]`
+ );
+
+ testPermListHasEntries(false);
+
+ ok(
+ BrowserTestUtils.is_hidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ await closePermissionPopup();
+
+ let storagePermissionID = "3rdPartyStorage^example2.com";
+ PermissionTestUtils.add(
+ browser.currentURI,
+ storagePermissionID,
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+ ok(
+ BrowserTestUtils.is_visible(storagePermissionAnchor.firstElementChild),
+ "Anchor header is visible"
+ );
+
+ let labelText = SitePermissions.getPermissionLabel(storagePermissionID);
+ let labels = storagePermissionAnchor.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in 3rdPartyStorage anchor");
+ is(
+ labels[0].getAttribute("value"),
+ labelText,
+ "Permission label has the correct value"
+ );
+
+ await closePermissionPopup();
+
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "camera",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+ ok(
+ BrowserTestUtils.is_visible(storagePermissionAnchor.firstElementChild),
+ "Anchor header is visible"
+ );
+
+ labels = permissionsList.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 2, "Two permissions visible in main view");
+ labels = storagePermissionAnchor.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in 3rdPartyStorage anchor");
+
+ storagePermissionAnchor
+ .querySelector(".permission-popup-permission-remove-button")
+ .click();
+ is(
+ storagePermissionAnchor.querySelectorAll(
+ ".permission-popup-permission-label"
+ ).length,
+ 0,
+ "Permission item should be removed"
+ );
+ is(
+ PermissionTestUtils.testPermission(
+ browser.currentURI,
+ storagePermissionID
+ ),
+ SitePermissions.UNKNOWN,
+ "Permission removed from permission manager"
+ );
+
+ await closePermissionPopup();
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+ ok(
+ BrowserTestUtils.is_hidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ labels = permissionsList.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+
+ await closePermissionPopup();
+
+ PermissionTestUtils.remove(browser.currentURI, "camera");
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(false);
+ ok(
+ BrowserTestUtils.is_hidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ await closePermissionPopup();
+ });
+});
diff --git a/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js b/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js
new file mode 100644
index 0000000000..53be5cc175
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js
@@ -0,0 +1,46 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_PAGE =
+ "https://example.com/browser/browser/base/content/test/permissions/empty.html";
+
+add_task(async function testNoPermissionPrompt() {
+ info("Creating tab");
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) {
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.vibrator.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ },
+ r
+ );
+ });
+
+ await ContentTask.spawn(browser, null, async function () {
+ let frame = content.document.createElement("iframe");
+ // Cross origin src
+ frame.src =
+ "https://example.org/browser/browser/base/content/test/permissions/empty.html";
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ await content.SpecialPowers.spawn(frame, [], async function () {
+ // Request a permission.
+ let result = this.content.navigator.vibrate([100, 100]);
+ Assert.equal(result, false, "navigator.vibrate has been denied");
+ });
+ content.document.body.removeChild(frame);
+ });
+ });
+});
diff --git a/browser/base/content/test/permissions/browser_permissions_handling_user_input.js b/browser/base/content/test/permissions/browser_permissions_handling_user_input.js
new file mode 100644
index 0000000000..94b69c4998
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_handling_user_input.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ORIGIN = "https://example.com";
+const PERMISSIONS_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "permissions.html";
+
+function assertShown(task) {
+ return BrowserTestUtils.withNewTab(
+ PERMISSIONS_PAGE,
+ async function (browser) {
+ let popupshown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ await SpecialPowers.spawn(browser, [], task);
+
+ await popupshown;
+
+ ok(true, "Notification permission prompt was shown");
+ }
+ );
+}
+
+function assertNotShown(task) {
+ return BrowserTestUtils.withNewTab(
+ PERMISSIONS_PAGE,
+ async function (browser) {
+ let popupshown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ await SpecialPowers.spawn(browser, [], task);
+
+ let sawPrompt = await Promise.race([
+ popupshown.then(() => true),
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ new Promise(c => setTimeout(() => c(false), 1000)),
+ ]);
+
+ is(sawPrompt, false, "Notification permission prompt was not shown");
+ }
+ );
+}
+
+// Tests that notification permissions are automatically denied without user interaction.
+add_task(async function testNotificationPermission() {
+ Services.prefs.setBoolPref(
+ "dom.webnotifications.requireuserinteraction",
+ true
+ );
+
+ // First test that when user interaction is required, requests
+ // with user interaction will show the permission prompt.
+
+ await assertShown(function () {
+ content.document.notifyUserGestureActivation();
+ content.document.getElementById("desktop-notification").click();
+ });
+
+ await assertShown(function () {
+ content.document.notifyUserGestureActivation();
+ content.document.getElementById("push").click();
+ });
+
+ // Now test that requests without user interaction will fail.
+
+ await assertNotShown(function () {
+ content.postMessage("push", "*");
+ });
+
+ await assertNotShown(async function () {
+ let response = await content.Notification.requestPermission();
+ is(response, "default", "The request was automatically denied");
+ });
+
+ Services.prefs.setBoolPref(
+ "dom.webnotifications.requireuserinteraction",
+ false
+ );
+
+ // Finally test that those requests will show a prompt again
+ // if the pref has been set to false.
+
+ await assertShown(function () {
+ content.postMessage("push", "*");
+ });
+
+ await assertShown(function () {
+ content.Notification.requestPermission();
+ });
+
+ Services.prefs.clearUserPref("dom.webnotifications.requireuserinteraction");
+});
diff --git a/browser/base/content/test/permissions/browser_permissions_postPrompt.js b/browser/base/content/test/permissions/browser_permissions_postPrompt.js
new file mode 100644
index 0000000000..8434f1fbb3
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_postPrompt.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ORIGIN = "https://example.com";
+const PERMISSIONS_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "permissions.html";
+
+function testPostPrompt(task) {
+ let uri = Services.io.newURI(PERMISSIONS_PAGE);
+ return BrowserTestUtils.withNewTab(
+ PERMISSIONS_PAGE,
+ async function (browser) {
+ let icon = document.getElementById("web-notifications-notification-icon");
+ ok(
+ !BrowserTestUtils.is_visible(icon),
+ "notifications icon is not visible at first"
+ );
+
+ await SpecialPowers.spawn(browser, [], task);
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(icon),
+ "notifications icon is visible"
+ );
+ ok(
+ !PopupNotifications.panel.hasAttribute("panelopen"),
+ "only the icon is showing, the panel is not open"
+ );
+
+ let popupshown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ icon.click();
+ await popupshown;
+
+ ok(true, "Notification permission prompt was shown");
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ EventUtils.synthesizeMouseAtCenter(notification.button, {});
+
+ is(
+ PermissionTestUtils.testPermission(uri, "desktop-notification"),
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ "User can override the default deny by using the prompt"
+ );
+
+ PermissionTestUtils.remove(uri, "desktop-notification");
+ }
+ );
+}
+
+add_task(async function testNotificationPermission() {
+ Services.prefs.setBoolPref(
+ "dom.webnotifications.requireuserinteraction",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "permissions.desktop-notification.postPrompt.enabled",
+ true
+ );
+
+ Services.prefs.setIntPref(
+ "permissions.default.desktop-notification",
+ Ci.nsIPermissionManager.DENY_ACTION
+ );
+
+ // First test that all requests (even with user interaction) will cause a post-prompt
+ // if the global default is "deny".
+
+ await testPostPrompt(function () {
+ E10SUtils.wrapHandlingUserInput(content, true, function () {
+ content.document.getElementById("desktop-notification").click();
+ });
+ });
+
+ await testPostPrompt(function () {
+ E10SUtils.wrapHandlingUserInput(content, true, function () {
+ content.document.getElementById("push").click();
+ });
+ });
+
+ Services.prefs.clearUserPref("permissions.default.desktop-notification");
+
+ // Now test that requests without user interaction will post-prompt when the
+ // user interaction requirement is set.
+
+ await testPostPrompt(function () {
+ content.postMessage("push", "*");
+ });
+
+ await testPostPrompt(async function () {
+ let response = await content.Notification.requestPermission();
+ is(response, "default", "The request was automatically denied");
+ });
+
+ Services.prefs.clearUserPref("dom.webnotifications.requireuserinteraction");
+ Services.prefs.clearUserPref(
+ "permissions.desktop-notification.postPrompt.enabled"
+ );
+});
diff --git a/browser/base/content/test/permissions/browser_reservedkey.js b/browser/base/content/test/permissions/browser_reservedkey.js
new file mode 100644
index 0000000000..c8eb0ab6c6
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_reservedkey.js
@@ -0,0 +1,312 @@
+add_task(async function test_reserved_shortcuts() {
+ let keyset = document.createXULElement("keyset");
+ let key1 = document.createXULElement("key");
+ key1.setAttribute("id", "kt_reserved");
+ key1.setAttribute("modifiers", "shift");
+ key1.setAttribute("key", "O");
+ key1.setAttribute("reserved", "true");
+ key1.setAttribute("count", "0");
+ key1.addEventListener("command", () => {
+ let attribute = key1.getAttribute("count");
+ key1.setAttribute("count", Number(attribute) + 1);
+ });
+
+ let key2 = document.createXULElement("key");
+ key2.setAttribute("id", "kt_notreserved");
+ key2.setAttribute("modifiers", "shift");
+ key2.setAttribute("key", "P");
+ key2.setAttribute("reserved", "false");
+ key2.setAttribute("count", "0");
+ key2.addEventListener("command", () => {
+ let attribute = key2.getAttribute("count");
+ key2.setAttribute("count", Number(attribute) + 1);
+ });
+
+ let key3 = document.createXULElement("key");
+ key3.setAttribute("id", "kt_reserveddefault");
+ key3.setAttribute("modifiers", "shift");
+ key3.setAttribute("key", "Q");
+ key3.setAttribute("count", "0");
+ key3.addEventListener("command", () => {
+ let attribute = key3.getAttribute("count");
+ key3.setAttribute("count", Number(attribute) + 1);
+ });
+
+ keyset.appendChild(key1);
+ keyset.appendChild(key2);
+ keyset.appendChild(key3);
+ let container = document.createXULElement("box");
+ container.appendChild(keyset);
+ document.documentElement.appendChild(container);
+
+ const pageUrl =
+ "data:text/html,<body onload='document.body.firstElementChild.focus();'><div onkeydown='event.preventDefault();' tabindex=0>Test</div></body>";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ EventUtils.sendString("OPQ");
+
+ is(
+ document.getElementById("kt_reserved").getAttribute("count"),
+ "1",
+ "reserved='true' with preference off"
+ );
+ is(
+ document.getElementById("kt_notreserved").getAttribute("count"),
+ "0",
+ "reserved='false' with preference off"
+ );
+ is(
+ document.getElementById("kt_reserveddefault").getAttribute("count"),
+ "0",
+ "default reserved with preference off"
+ );
+
+ // Now try with reserved shortcut key handling enabled.
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["permissions.default.shortcuts", 2]] },
+ resolve
+ );
+ });
+
+ EventUtils.sendString("OPQ");
+
+ is(
+ document.getElementById("kt_reserved").getAttribute("count"),
+ "2",
+ "reserved='true' with preference on"
+ );
+ is(
+ document.getElementById("kt_notreserved").getAttribute("count"),
+ "0",
+ "reserved='false' with preference on"
+ );
+ is(
+ document.getElementById("kt_reserveddefault").getAttribute("count"),
+ "1",
+ "default reserved with preference on"
+ );
+
+ document.documentElement.removeChild(container);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test checks that Alt+<key> and F10 cannot be blocked when the preference is set.
+if (!navigator.platform.includes("Mac")) {
+ add_task(async function test_accesskeys_menus() {
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["permissions.default.shortcuts", 2]] },
+ resolve
+ );
+ });
+
+ const uri =
+ 'data:text/html,<body onkeydown=\'if (event.key == "H" || event.key == "F10") event.preventDefault();\'>';
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri);
+
+ // Pressing Alt+H should open the Help menu.
+ let helpPopup = document.getElementById("menu_HelpPopup");
+ let popupShown = BrowserTestUtils.waitForEvent(helpPopup, "popupshown");
+ EventUtils.synthesizeKey("KEY_Alt", { type: "keydown" });
+ EventUtils.synthesizeKey("h", { altKey: true });
+ EventUtils.synthesizeKey("KEY_Alt", { type: "keyup" });
+ await popupShown;
+
+ ok(true, "Help menu opened");
+
+ let popupHidden = BrowserTestUtils.waitForEvent(helpPopup, "popuphidden");
+ helpPopup.hidePopup();
+ await popupHidden;
+
+ // Pressing F10 should focus the menubar. On Linux, the file menu should open, but on Windows,
+ // pressing Down will open the file menu.
+ let menubar = document.getElementById("main-menubar");
+ let menubarActive = BrowserTestUtils.waitForEvent(
+ menubar,
+ "DOMMenuBarActive"
+ );
+ EventUtils.synthesizeKey("KEY_F10");
+ await menubarActive;
+
+ let filePopup = document.getElementById("menu_FilePopup");
+ popupShown = BrowserTestUtils.waitForEvent(filePopup, "popupshown");
+ if (navigator.platform.includes("Win")) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await popupShown;
+
+ ok(true, "File menu opened");
+
+ popupHidden = BrowserTestUtils.waitForEvent(filePopup, "popuphidden");
+ filePopup.hidePopup();
+ await popupHidden;
+
+ BrowserTestUtils.removeTab(tab1);
+ });
+}
+
+// There is a <key> element for Backspace and delete with reserved="false",
+// so make sure that it is not treated as a blocked shortcut key.
+add_task(async function test_backspace_delete() {
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["permissions.default.shortcuts", 2]] },
+ resolve
+ );
+ });
+
+ // The input field is autofocused. If this test fails, backspace can go back
+ // in history so cancel the beforeunload event and adjust the field to make the test fail.
+ const uri =
+ 'data:text/html,<body onbeforeunload=\'document.getElementById("field").value = "failed";\'>' +
+ "<input id='field' value='something'></body>";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.document.getElementById("field").focus();
+
+ // Add a promise that resolves when the backspace key gets received
+ // so we can ensure the key gets received before checking the result.
+ content.keysPromise = new Promise(resolve => {
+ content.addEventListener("keyup", event => {
+ if (event.code == "Backspace") {
+ resolve(content.document.getElementById("field").value);
+ }
+ });
+ });
+ });
+
+ // Move the caret so backspace will delete the first character.
+ EventUtils.synthesizeKey("KEY_ArrowRight", {});
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+
+ let fieldValue = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ return content.keysPromise;
+ }
+ );
+ is(fieldValue, "omething", "backspace not prevented");
+
+ // now do the same thing for the delete key:
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.document.getElementById("field").focus();
+
+ // Add a promise that resolves when the backspace key gets received
+ // so we can ensure the key gets received before checking the result.
+ content.keysPromise = new Promise(resolve => {
+ content.addEventListener("keyup", event => {
+ if (event.code == "Delete") {
+ resolve(content.document.getElementById("field").value);
+ }
+ });
+ });
+ });
+
+ // Move the caret so backspace will delete the first character.
+ EventUtils.synthesizeKey("KEY_Delete", {});
+
+ fieldValue = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ return content.keysPromise;
+ }
+ );
+ is(fieldValue, "mething", "delete not prevented");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// TODO: Make this to run on Windows too to have automated tests also there.
+if (
+ navigator.platform.includes("Mac") ||
+ navigator.platform.includes("Linux")
+) {
+ add_task(
+ async function test_reserved_shortcuts_conflict_with_user_settings() {
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["test.events.async.enabled", true]] },
+ resolve
+ );
+ });
+
+ const keyset = document.createXULElement("keyset");
+ const key = document.createXULElement("key");
+ key.setAttribute("id", "conflict_with_known_native_key_binding");
+ if (navigator.platform.includes("Mac")) {
+ // Select to end of the paragraph
+ key.setAttribute("modifiers", "ctrl,shift");
+ key.setAttribute("key", "E");
+ } else {
+ // Select All
+ key.setAttribute("modifiers", "ctrl");
+ key.setAttribute("key", "a");
+ }
+ key.setAttribute("reserved", "true");
+ key.setAttribute("count", "0");
+ key.addEventListener("command", () => {
+ const attribute = key.getAttribute("count");
+ key.setAttribute("count", Number(attribute) + 1);
+ });
+
+ keyset.appendChild(key);
+ const container = document.createXULElement("box");
+ container.appendChild(keyset);
+ document.documentElement.appendChild(container);
+
+ const pageUrl =
+ "data:text/html,<body onload='document.body.firstChild.focus(); getSelection().collapse(document.body.firstChild, 0)'><div contenteditable>Test</div></body>";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ pageUrl
+ );
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [key.getAttribute("key")],
+ async function (aExpectedKeyValue) {
+ content.promiseTestResult = new Promise(resolve => {
+ content.addEventListener("keyup", event => {
+ if (event.key.toLowerCase() == aExpectedKeyValue.toLowerCase()) {
+ resolve(content.getSelection().getRangeAt(0).toString());
+ }
+ });
+ });
+ }
+ );
+
+ EventUtils.synthesizeKey(key.getAttribute("key"), {
+ ctrlKey: key.getAttribute("modifiers").includes("ctrl"),
+ shiftKey: key.getAttribute("modifiers").includes("shift"),
+ });
+
+ const selectedText = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ return content.promiseTestResult;
+ }
+ );
+ is(
+ selectedText,
+ "Test",
+ "The shortcut key should select all text in the editor"
+ );
+
+ is(
+ key.getAttribute("count"),
+ "0",
+ "The reserved shortcut key should be consumed by the focused editor instead"
+ );
+
+ document.documentElement.removeChild(container);
+
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+}
diff --git a/browser/base/content/test/permissions/browser_site_scoped_permissions.js b/browser/base/content/test/permissions/browser_site_scoped_permissions.js
new file mode 100644
index 0000000000..560b1fff4c
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_site_scoped_permissions.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EMPTY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+const SUBDOMAIN_EMPTY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://www.example.com"
+ ) + "empty.html";
+
+add_task(async function testSiteScopedPermissionSubdomainAffectsBaseDomain() {
+ let subdomainOrigin = "https://www.example.com";
+ let subdomainPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ subdomainOrigin
+ );
+ let id = "3rdPartyStorage^https://example.org";
+
+ await BrowserTestUtils.withNewTab(EMPTY_PAGE, async function (browser) {
+ Services.perms.addFromPrincipal(
+ subdomainPrincipal,
+ id,
+ SitePermissions.ALLOW
+ );
+
+ await openPermissionPopup();
+
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let listEntryCount = permissionsList.querySelectorAll(
+ ".permission-popup-permission-item"
+ ).length;
+ is(
+ listEntryCount,
+ 1,
+ "Permission exists on base domain when set on subdomain"
+ );
+
+ closePermissionPopup();
+
+ Services.perms.removeFromPrincipal(subdomainPrincipal, id);
+
+ await openPermissionPopup();
+
+ listEntryCount = permissionsList.querySelectorAll(
+ ".permission-popup-permission-item-3rdPartyStorage"
+ ).length;
+ is(
+ listEntryCount,
+ 0,
+ "Permission removed on base domain when removed on subdomain"
+ );
+
+ await closePermissionPopup();
+ });
+});
+
+add_task(async function testSiteScopedPermissionBaseDomainAffectsSubdomain() {
+ let origin = "https://example.com";
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ let id = "3rdPartyStorage^https://example.org";
+
+ await BrowserTestUtils.withNewTab(
+ SUBDOMAIN_EMPTY_PAGE,
+ async function (browser) {
+ Services.perms.addFromPrincipal(principal, id, SitePermissions.ALLOW);
+ await openPermissionPopup();
+
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let listEntryCount = permissionsList.querySelectorAll(
+ ".permission-popup-permission-item"
+ ).length;
+ is(
+ listEntryCount,
+ 1,
+ "Permission exists on base domain when set on subdomain"
+ );
+
+ closePermissionPopup();
+
+ Services.perms.removeFromPrincipal(principal, id);
+
+ await openPermissionPopup();
+
+ listEntryCount = permissionsList.querySelectorAll(
+ ".permission-popup-permission-item-3rdPartyStorage"
+ ).length;
+ is(
+ listEntryCount,
+ 0,
+ "Permission removed on base domain when removed on subdomain"
+ );
+
+ await closePermissionPopup();
+ }
+ );
+});
diff --git a/browser/base/content/test/permissions/browser_temporary_permissions.js b/browser/base/content/test/permissions/browser_temporary_permissions.js
new file mode 100644
index 0000000000..83f7e49d56
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ORIGIN = "https://example.com";
+const PERMISSIONS_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "permissions.html";
+const SUBFRAME_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "temporary_permissions_subframe.html";
+
+// Test that setting temp permissions triggers a change in the identity block.
+add_task(async function testTempPermissionChangeEvents() {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN);
+ let id = "geo";
+
+ await BrowserTestUtils.withNewTab(ORIGIN, function (browser) {
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ let geoIcon = document.querySelector(
+ ".blocked-permission-icon[data-permission-id=geo]"
+ );
+
+ Assert.notEqual(
+ geoIcon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ SitePermissions.removeFromPrincipal(principal, id, browser);
+
+ Assert.equal(
+ geoIcon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should not be visible"
+ );
+ });
+});
+
+// Test that temp blocked permissions requested by subframes (with a different URI) affect the whole page.
+add_task(async function testTempPermissionSubframes() {
+ let uri = NetUtil.newURI(ORIGIN);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let id = "geo";
+
+ await BrowserTestUtils.withNewTab(SUBFRAME_PAGE, async function (browser) {
+ let popupshown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ },
+ r
+ );
+ });
+
+ // Request a permission.
+ await SpecialPowers.spawn(browser, [uri.host], async function (host0) {
+ let frame = content.document.getElementById("frame");
+
+ await content.SpecialPowers.spawn(frame, [host0], async function (host) {
+ const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+ );
+
+ E10SUtils.wrapHandlingUserInput(this.content, true, function () {
+ let frameDoc = this.content.document;
+
+ // Make sure that the origin of our test page is different.
+ Assert.notEqual(frameDoc.location.host, host);
+
+ frameDoc.getElementById("geo").click();
+ });
+ });
+ });
+
+ await popupshown;
+
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
+
+ await popuphidden;
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+ });
+});
diff --git a/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js b/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
new file mode 100644
index 0000000000..e323f769cd
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
@@ -0,0 +1,208 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+const ORIGIN = "https://example.com";
+const PERMISSIONS_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "permissions.html";
+
+// Ignore promise rejection caused by clicking Deny button.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/The request is not allowed/);
+
+const EXPIRE_TIME_MS = 100;
+const TIMEOUT_MS = 500;
+
+const EXPIRE_TIME_CUSTOM_MS = 1000;
+const TIMEOUT_CUSTOM_MS = 1500;
+
+const kVREnabled = SpecialPowers.getBoolPref("dom.vr.enabled");
+
+// Test that temporary permissions can be re-requested after they expired
+// and that the identity block is updated accordingly.
+add_task(async function testTempPermissionRequestAfterExpiry() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.temporary_permission_expire_time_ms", EXPIRE_TIME_MS],
+ ["media.navigator.permission.fake", true],
+ ["dom.vr.always_support_vr", true],
+ ],
+ });
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN);
+ let ids = ["geo", "camera"];
+
+ if (kVREnabled) {
+ ids.push("xr");
+ }
+
+ for (let id of ids) {
+ await BrowserTestUtils.withNewTab(
+ PERMISSIONS_PAGE,
+ async function (browser) {
+ let blockedIcon = gPermissionPanel._identityPermissionBox.querySelector(
+ `.blocked-permission-icon[data-permission-id='${id}']`
+ );
+
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(principal, id, browser),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }
+ );
+
+ ok(
+ blockedIcon.hasAttribute("showing"),
+ "blocked permission icon is shown"
+ );
+
+ await new Promise(c => setTimeout(c, TIMEOUT_MS));
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(principal, id, browser),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ }
+ );
+
+ let popupshown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Request a permission;
+ await BrowserTestUtils.synthesizeMouseAtCenter(`#${id}`, {}, browser);
+
+ await popupshown;
+
+ ok(
+ !blockedIcon.hasAttribute("showing"),
+ "blocked permission icon is not shown"
+ );
+
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
+
+ await popuphidden;
+
+ SitePermissions.removeFromPrincipal(principal, id, browser);
+ }
+ );
+ }
+});
+
+/**
+ * Test whether the identity UI shows the permission granted state.
+ * @param {boolean} state - true = Shows permission granted, false otherwise.
+ */
+async function testIdentityPermissionGrantedState(state) {
+ let hasAttribute;
+ let msg = `Identity permission box ${
+ state ? "shows" : "does not show"
+ } granted permissions.`;
+ await TestUtils.waitForCondition(() => {
+ hasAttribute =
+ gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions");
+ return hasAttribute == state;
+ }, msg);
+ is(hasAttribute, state, msg);
+}
+
+// Test that temporary permissions can have custom expiry time and the identity
+// block is updated correctly on expiry.
+add_task(async function testTempPermissionCustomExpiry() {
+ const TEST_ID = "geo";
+ // Set a default expiry time which is lower than the custom one we'll set.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.temporary_permission_expire_time_ms", EXPIRE_TIME_MS]],
+ });
+
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async browser => {
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, TEST_ID, browser),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "Permission not set initially"
+ );
+
+ await testIdentityPermissionGrantedState(false);
+
+ // Set permission with custom expiry time.
+ SitePermissions.setForPrincipal(
+ null,
+ "geo",
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser,
+ EXPIRE_TIME_CUSTOM_MS
+ );
+
+ await testIdentityPermissionGrantedState(true);
+
+ // We've set the permission, start the timer promise.
+ let timeout = new Promise(resolve =>
+ setTimeout(resolve, TIMEOUT_CUSTOM_MS)
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, TEST_ID, browser),
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "We should see the temporary permission we just set."
+ );
+
+ // Wait for half of the expiry time.
+ await new Promise(resolve =>
+ setTimeout(resolve, EXPIRE_TIME_CUSTOM_MS / 2)
+ );
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, TEST_ID, browser),
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "Temporary permission should not have expired yet."
+ );
+
+ // Wait until permission expiry.
+ await timeout;
+
+ // Identity permission section should have updated by now. It should do this
+ // without relying on side-effects of the SitePermissions getter.
+ await testIdentityPermissionGrantedState(false);
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, TEST_ID, browser),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "Permission should have expired"
+ );
+ });
+});
diff --git a/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js
new file mode 100644
index 0000000000..7da79b1810
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that temporary permissions are removed on user initiated reload only.
+add_task(async function testTempPermissionOnReload() {
+ let origin = "https://example.com/";
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ let id = "geo";
+
+ await BrowserTestUtils.withNewTab(origin, async function (browser) {
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ let reloaded = BrowserTestUtils.browserLoaded(browser, false, origin);
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ // Reload through the page (should not remove the temp permission).
+ await SpecialPowers.spawn(browser, [], () =>
+ content.document.location.reload()
+ );
+
+ await reloaded;
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ reloaded = BrowserTestUtils.browserLoaded(browser, false, origin);
+
+ // Reload as a user (should remove the temp permission).
+ BrowserReload();
+
+ await reloaded;
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // Set the permission again.
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ // Open the tab context menu.
+ let contextMenu = document.getElementById("tabContextMenu");
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+
+ let reloadMenuItem = document.getElementById("context_reloadTab");
+
+ reloaded = BrowserTestUtils.browserLoaded(browser, false, origin);
+
+ // Reload as a user through the context menu (should remove the temp permission).
+ contextMenu.activateItem(reloadMenuItem);
+
+ await reloaded;
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // Set the permission again.
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ // Reload as user via return key in urlbar (should remove the temp permission)
+ let urlBarInput = document.getElementById("urlbar-input");
+ await EventUtils.synthesizeMouseAtCenter(urlBarInput, {});
+
+ reloaded = BrowserTestUtils.browserLoaded(browser, false, origin);
+
+ EventUtils.synthesizeAndWaitKey("VK_RETURN", {});
+
+ await reloaded;
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ SitePermissions.removeFromPrincipal(principal, id, browser);
+ });
+});
+
+// Test that temporary permissions are not removed when reloading all tabs.
+add_task(async function testTempPermissionOnReloadAllTabs() {
+ let origin = "https://example.com/";
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ let id = "geo";
+
+ await BrowserTestUtils.withNewTab(origin, async function (browser) {
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ // Select all tabs before opening the context menu.
+ gBrowser.selectAllTabs();
+
+ // Open the tab context menu.
+ let contextMenu = document.getElementById("tabContextMenu");
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+
+ let reloadMenuItem = document.getElementById("context_reloadSelectedTabs");
+
+ let reloaded = Promise.all(
+ gBrowser.visibleTabs.map(tab =>
+ BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab))
+ )
+ );
+ contextMenu.activateItem(reloadMenuItem);
+ await reloaded;
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ SitePermissions.removeFromPrincipal(principal, id, browser);
+ });
+});
+
+// Test that temporary permissions are persisted through navigation in a tab.
+add_task(async function testTempPermissionOnNavigation() {
+ let origin = "https://example.com/";
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ let id = "geo";
+
+ await BrowserTestUtils.withNewTab(origin, async function (browser) {
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "https://example.org/"
+ );
+
+ // Navigate to another domain.
+ await SpecialPowers.spawn(
+ browser,
+ [],
+ () => (content.document.location = "https://example.org/")
+ );
+
+ await loaded;
+
+ // The temporary permissions for the current URI should be reset.
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(browser.contentPrincipal, id, browser),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ }
+ );
+
+ loaded = BrowserTestUtils.browserLoaded(browser, false, origin);
+
+ // Navigate to the original domain.
+ await SpecialPowers.spawn(
+ browser,
+ [],
+ () => (content.document.location = "https://example.com/")
+ );
+
+ await loaded;
+
+ // The temporary permissions for the original URI should still exist.
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(browser.contentPrincipal, id, browser),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }
+ );
+
+ SitePermissions.removeFromPrincipal(browser.contentPrincipal, id, browser);
+ });
+});
diff --git a/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js b/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js
new file mode 100644
index 0000000000..a4347f9671
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that temp permissions are persisted through moving tabs to new windows.
+add_task(async function testTempPermissionOnTabMove() {
+ let origin = "https://example.com/";
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ let id = "geo";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin);
+
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ tab.linkedBrowser
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }
+ );
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(tab);
+ let win = await promiseWin;
+ tab = win.gBrowser.selectedTab;
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }
+ );
+
+ SitePermissions.removeFromPrincipal(principal, id, tab.linkedBrowser);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Test that temp permissions don't affect other tabs of the same URI.
+add_task(async function testTempPermissionMultipleTabs() {
+ let origin = "https://example.com/";
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ let id = "geo";
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin);
+
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ tab2.linkedBrowser
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(principal, id, tab2.linkedBrowser),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(principal, id, tab1.linkedBrowser),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ }
+ );
+
+ let geoIcon = document.querySelector(
+ ".blocked-permission-icon[data-permission-id=geo]"
+ );
+
+ Assert.notEqual(
+ geoIcon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ Assert.equal(
+ geoIcon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should not be visible"
+ );
+
+ SitePermissions.removeFromPrincipal(principal, id, tab2.linkedBrowser);
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Test that temp permissions are cleared when closing tabs.
+add_task(async function testTempPermissionOnTabClose() {
+ let origin = "https://example.com/";
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ let id = "geo";
+
+ ok(
+ !SitePermissions._temporaryPermissions._stateByBrowser.size,
+ "Temporary permission map should be empty initially."
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin);
+
+ SitePermissions.setForPrincipal(
+ principal,
+ id,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ tab.linkedBrowser
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ }
+ );
+
+ ok(
+ SitePermissions._temporaryPermissions._stateByBrowser.has(
+ tab.linkedBrowser
+ ),
+ "Temporary permission map should have an entry for the browser."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ ok(
+ !SitePermissions._temporaryPermissions._stateByBrowser.size,
+ "Temporary permission map should be empty after closing the tab."
+ );
+});
diff --git a/browser/base/content/test/permissions/dummy.js b/browser/base/content/test/permissions/dummy.js
new file mode 100644
index 0000000000..c45ec0a714
--- /dev/null
+++ b/browser/base/content/test/permissions/dummy.js
@@ -0,0 +1 @@
+// Just a dummy file for testing.
diff --git a/browser/base/content/test/permissions/empty.html b/browser/base/content/test/permissions/empty.html
new file mode 100644
index 0000000000..1ad28bb1f7
--- /dev/null
+++ b/browser/base/content/test/permissions/empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Empty file</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/permissions/head.js b/browser/base/content/test/permissions/head.js
new file mode 100644
index 0000000000..847386b7e2
--- /dev/null
+++ b/browser/base/content/test/permissions/head.js
@@ -0,0 +1,28 @@
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+SpecialPowers.addTaskImport(
+ "E10SUtils",
+ "resource://gre/modules/E10SUtils.sys.mjs"
+);
+
+function openPermissionPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gPermissionPanel._permissionPopup
+ );
+ gPermissionPanel._identityPermissionBox.click();
+ return promise;
+}
+
+function closePermissionPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gPermissionPanel._permissionPopup,
+ "popuphidden"
+ );
+ gPermissionPanel._permissionPopup.hidePopup();
+ return promise;
+}
diff --git a/browser/base/content/test/permissions/permissions.html b/browser/base/content/test/permissions/permissions.html
new file mode 100644
index 0000000000..97286914e7
--- /dev/null
+++ b/browser/base/content/test/permissions/permissions.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+<script>
+var gKeyDowns = 0;
+var gKeyPresses = 0;
+
+navigator.serviceWorker.register("dummy.js");
+
+function requestPush() {
+ return navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
+ serviceWorkerRegistration.pushManager.subscribe();
+ });
+}
+
+function requestGeo() {
+ return navigator.geolocation.getCurrentPosition(() => {
+ parent.postMessage("allow", "*");
+ }, error => {
+ // PERMISSION_DENIED = 1
+ parent.postMessage(error.code == 1 ? "deny" : "allow", "*");
+ });
+}
+
+
+window.onmessage = function(event) {
+ switch (event.data) {
+ case "push":
+ requestPush();
+ break;
+ }
+};
+
+</script>
+ <body onkeydown="gKeyDowns++;" onkeypress="gKeyPresses++">
+ <!-- This page could eventually request permissions from content
+ and make sure that chrome responds appropriately -->
+ <button id="geo" onclick="requestGeo()">Geolocation</button>
+ <button id="xr" onclick="navigator.getVRDisplays()">XR</button>
+ <button id="desktop-notification" onclick="Notification.requestPermission()">Notifications</button>
+ <button id="push" onclick="requestPush()">Push Notifications</button>
+ <button id="camera" onclick="navigator.mediaDevices.getUserMedia({video: true, fake: true})">Camera</button>
+ </body>
+</html>
diff --git a/browser/base/content/test/permissions/temporary_permissions_frame.html b/browser/base/content/test/permissions/temporary_permissions_frame.html
new file mode 100644
index 0000000000..25aede980f
--- /dev/null
+++ b/browser/base/content/test/permissions/temporary_permissions_frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Permissions Subframe Test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <iframe id="frameAncestor"
+ src="https://test1.example.com/browser/browser/base/content/test/permissions/temporary_permissions_subframe.html"
+ allow="geolocation https://test1.example.com https://test2.example.com"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/permissions/temporary_permissions_subframe.html b/browser/base/content/test/permissions/temporary_permissions_subframe.html
new file mode 100644
index 0000000000..4ff13f2e91
--- /dev/null
+++ b/browser/base/content/test/permissions/temporary_permissions_subframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Temporary Permissions Subframe Test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <iframe id="frame" src="https://example.org/browser/browser/base/content/test/permissions/permissions.html" allow="geolocation"></iframe>
+ <iframe id="frameAllowsAll" src="https://example.org/browser/browser/base/content/test/permissions/permissions.html" allow="geolocation *"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/browser.ini b/browser/base/content/test/plugins/browser.ini
new file mode 100644
index 0000000000..c0e065c5d0
--- /dev/null
+++ b/browser/base/content/test/plugins/browser.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+support-files =
+ empty_file.html
+ head.js
+ plugin_bug797677.html
+ plugin_test.html
+
+[browser_bug797677.js]
+[browser_enable_DRM_prompt.js]
+skip-if = (os == 'win' && processor == 'aarch64') # bug 1533164
+[browser_globalplugin_crashinfobar.js]
+skip-if = !crashreporter
+[browser_private_browsing_eme_persistent_state.js]
+
diff --git a/browser/base/content/test/plugins/browser_bug797677.js b/browser/base/content/test/plugins/browser_bug797677.js
new file mode 100644
index 0000000000..acc728d77e
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug797677.js
@@ -0,0 +1,45 @@
+var gTestRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://127.0.0.1:8888/"
+);
+var gTestBrowser = null;
+var gConsoleErrors = 0;
+
+add_task(async function () {
+ registerCleanupFunction(function () {
+ Services.console.unregisterListener(errorListener);
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ let errorListener = {
+ observe(aMessage) {
+ if (aMessage.message.includes("NS_ERROR_FAILURE")) {
+ gConsoleErrors++;
+ }
+ },
+ };
+ Services.console.registerListener(errorListener);
+
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ gTestRoot + "plugin_bug797677.html"
+ );
+
+ let pluginInfo = await promiseForPluginInfo("plugin");
+ is(
+ pluginInfo.displayedType,
+ Ci.nsIObjectLoadingContent.TYPE_NULL,
+ "plugin should not have been found."
+ );
+
+ await SpecialPowers.spawn(gTestBrowser, [], function () {
+ let plugin = content.document.getElementById("plugin");
+ ok(plugin, "plugin should be in the page");
+ });
+ is(gConsoleErrors, 0, "should have no console errors");
+});
diff --git a/browser/base/content/test/plugins/browser_enable_DRM_prompt.js b/browser/base/content/test/plugins/browser_enable_DRM_prompt.js
new file mode 100644
index 0000000000..e77455ddd0
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_enable_DRM_prompt.js
@@ -0,0 +1,232 @@
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty_file.html";
+
+/*
+ * Register cleanup function to reset prefs after other tasks have run.
+ */
+
+add_task(async function () {
+ // Note: SpecialPowers.pushPrefEnv has problems with the "Enable DRM"
+ // button on the notification box toggling the prefs. So manually
+ // set/unset the prefs the UI we're testing toggles.
+ let emeWasEnabled = Services.prefs.getBoolPref("media.eme.enabled", false);
+ let cdmWasEnabled = Services.prefs.getBoolPref(
+ "media.gmp-widevinecdm.enabled",
+ false
+ );
+
+ // Restore the preferences to their pre-test state on test finish.
+ registerCleanupFunction(function () {
+ // Unlock incase lock test threw and didn't unlock.
+ Services.prefs.unlockPref("media.eme.enabled");
+ Services.prefs.setBoolPref("media.eme.enabled", emeWasEnabled);
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", cdmWasEnabled);
+ });
+});
+
+/*
+ * Bug 1366167 - Tests that the "Enable DRM" prompt shows if EME is requested while EME is disabled.
+ */
+
+add_task(async function test_drm_prompt_shows_for_toplevel() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
+ // Turn off EME and Widevine CDM.
+ Services.prefs.setBoolPref("media.eme.enabled", false);
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", false);
+
+ // Have content request access to Widevine, UI should drop down to
+ // prompt user to enable DRM.
+ let result = await SpecialPowers.spawn(browser, [], async function () {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ },
+ ];
+ await content.navigator.requestMediaKeySystemAccess(
+ "com.widevine.alpha",
+ config
+ );
+ } catch (ex) {
+ return { rejected: true };
+ }
+ return { rejected: false };
+ });
+ is(
+ result.rejected,
+ true,
+ "EME request should be denied because EME disabled."
+ );
+
+ // Verify the UI prompt showed.
+ let box = gBrowser.getNotificationBox(browser);
+ let notification = box.currentNotification;
+
+ ok(notification, "Notification should be visible");
+ is(
+ notification.getAttribute("value"),
+ "drmContentDisabled",
+ "Should be showing the right notification"
+ );
+
+ // Verify the "Enable DRM" button is there.
+ let buttons = notification.buttonContainer.querySelectorAll(
+ ".notification-button"
+ );
+ is(buttons.length, 1, "Should have one button.");
+
+ // Prepare a Promise that should resolve when the "Enable DRM" button's
+ // page reload completes.
+ let refreshPromise = BrowserTestUtils.browserLoaded(browser);
+ buttons[0].click();
+
+ // Wait for the reload to complete.
+ await refreshPromise;
+
+ // Verify clicking the "Enable DRM" button enabled DRM.
+ let enabled = Services.prefs.getBoolPref("media.eme.enabled", true);
+ is(
+ enabled,
+ true,
+ "EME should be enabled after click on 'Enable DRM' button"
+ );
+ });
+});
+
+add_task(async function test_eme_locked() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
+ // Turn off EME and Widevine CDM.
+ Services.prefs.setBoolPref("media.eme.enabled", false);
+ Services.prefs.lockPref("media.eme.enabled");
+
+ // Have content request access to Widevine, UI should drop down to
+ // prompt user to enable DRM.
+ let result = await SpecialPowers.spawn(browser, [], async function () {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ },
+ ];
+ await content.navigator.requestMediaKeySystemAccess(
+ "com.widevine.alpha",
+ config
+ );
+ } catch (ex) {
+ return { rejected: true };
+ }
+ return { rejected: false };
+ });
+ is(
+ result.rejected,
+ true,
+ "EME request should be denied because EME disabled."
+ );
+
+ // Verify the UI prompt did not show.
+ let box = gBrowser.getNotificationBox(browser);
+ let notification = box.currentNotification;
+
+ is(
+ notification,
+ null,
+ "Notification should not be displayed since pref is locked"
+ );
+
+ // Unlock the pref for any tests that follow.
+ Services.prefs.unlockPref("media.eme.enabled");
+ });
+});
+
+/*
+ * Bug 1642465 - Ensure cross origin frames requesting access prompt in the same way as same origin.
+ */
+
+add_task(async function test_drm_prompt_shows_for_cross_origin_iframe() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
+ // Turn off EME and Widevine CDM.
+ Services.prefs.setBoolPref("media.eme.enabled", false);
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", false);
+
+ // Have content request access to Widevine, UI should drop down to
+ // prompt user to enable DRM.
+ const CROSS_ORIGIN_URL = TEST_URL.replace("example.com", "example.org");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [CROSS_ORIGIN_URL],
+ async function (crossOriginUrl) {
+ let frame = content.document.createElement("iframe");
+ frame.src = crossOriginUrl;
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ return content.SpecialPowers.spawn(frame, [], async function () {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [
+ { contentType: 'video/webm; codecs="vp9"' },
+ ],
+ },
+ ];
+ await content.navigator.requestMediaKeySystemAccess(
+ "com.widevine.alpha",
+ config
+ );
+ } catch (ex) {
+ return { rejected: true };
+ }
+ return { rejected: false };
+ });
+ }
+ );
+ is(
+ result.rejected,
+ true,
+ "EME request should be denied because EME disabled."
+ );
+
+ // Verify the UI prompt showed.
+ let box = gBrowser.getNotificationBox(browser);
+ let notification = box.currentNotification;
+
+ ok(notification, "Notification should be visible");
+ is(
+ notification.getAttribute("value"),
+ "drmContentDisabled",
+ "Should be showing the right notification"
+ );
+
+ // Verify the "Enable DRM" button is there.
+ let buttons = notification.buttonContainer.querySelectorAll(
+ ".notification-button"
+ );
+ is(buttons.length, 1, "Should have one button.");
+
+ // Prepare a Promise that should resolve when the "Enable DRM" button's
+ // page reload completes.
+ let refreshPromise = BrowserTestUtils.browserLoaded(browser);
+ buttons[0].click();
+
+ // Wait for the reload to complete.
+ await refreshPromise;
+
+ // Verify clicking the "Enable DRM" button enabled DRM.
+ let enabled = Services.prefs.getBoolPref("media.eme.enabled", true);
+ is(
+ enabled,
+ true,
+ "EME should be enabled after click on 'Enable DRM' button"
+ );
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
new file mode 100644
index 0000000000..483c2b4032
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
@@ -0,0 +1,63 @@
+"use strict";
+
+let { PluginManager } = ChromeUtils.importESModule(
+ "resource:///actors/PluginParent.sys.mjs"
+);
+
+/**
+ * Test that the notification bar for crashed GMPs works.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function (browser) {
+ // Ensure the parent has heard before the client.
+ // In practice, this is always true for GMP crashes (but not for NPAPI ones!)
+ let props = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag2
+ );
+ props.setPropertyAsUint32("pluginID", 1);
+ props.setPropertyAsACString("pluginName", "GlobalTestPlugin");
+ props.setPropertyAsACString("pluginDumpID", "1234");
+ Services.obs.notifyObservers(props, "gmp-plugin-crash");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const GMP_CRASH_EVENT = {
+ pluginID: 1,
+ pluginName: "GlobalTestPlugin",
+ submittedCrashReport: false,
+ bubbles: true,
+ cancelable: true,
+ gmpPlugin: true,
+ };
+
+ let crashEvent = new content.PluginCrashedEvent(
+ "PluginCrashed",
+ GMP_CRASH_EVENT
+ );
+ content.dispatchEvent(crashEvent);
+ });
+
+ let notification = await waitForNotificationBar(
+ "plugin-crashed",
+ browser
+ );
+
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ ok(notification, "Infobar was shown.");
+ is(
+ notification.priority,
+ notificationBox.PRIORITY_WARNING_MEDIUM,
+ "Correct priority."
+ );
+ is(
+ notification.messageText.textContent,
+ "The GlobalTestPlugin plugin has crashed.",
+ "Correct message."
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/plugins/browser_private_browsing_eme_persistent_state.js b/browser/base/content/test/plugins/browser_private_browsing_eme_persistent_state.js
new file mode 100644
index 0000000000..fba4bb552c
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_private_browsing_eme_persistent_state.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This test ensures that navigator.requestMediaKeySystemAccess() requests
+ * to run EME with persistent state are rejected in private browsing windows.
+ * Bug 1334111.
+ */
+
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty_file.html";
+
+async function isEmePersistentStateSupported(mode) {
+ let win = await BrowserTestUtils.openNewBrowserWindow(mode);
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL);
+ let persistentStateSupported = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ persistentState: "required",
+ },
+ ];
+ await content.navigator.requestMediaKeySystemAccess(
+ "org.w3.clearkey",
+ config
+ );
+ } catch (ex) {
+ return false;
+ }
+ return true;
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+
+ return persistentStateSupported;
+}
+
+add_task(async function test() {
+ is(
+ await isEmePersistentStateSupported({ private: true }),
+ false,
+ "EME persistentState should *NOT* be supported in private browsing window."
+ );
+ is(
+ await isEmePersistentStateSupported({ private: false }),
+ true,
+ "EME persistentState *SHOULD* be supported in non private browsing window."
+ );
+});
diff --git a/browser/base/content/test/plugins/empty_file.html b/browser/base/content/test/plugins/empty_file.html
new file mode 100644
index 0000000000..af8440ac16
--- /dev/null
+++ b/browser/base/content/test/plugins/empty_file.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ This page is intentionally left blank.
+ </body>
+</html>
diff --git a/browser/base/content/test/plugins/head.js b/browser/base/content/test/plugins/head.js
new file mode 100644
index 0000000000..5f1939080e
--- /dev/null
+++ b/browser/base/content/test/plugins/head.js
@@ -0,0 +1,205 @@
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+});
+
+// Various tests in this directory may define gTestBrowser, to use as the
+// default browser under test in some of the functions below.
+/* global gTestBrowser:true */
+
+/**
+ * Waits a specified number of miliseconds.
+ *
+ * Usage:
+ * let wait = yield waitForMs(2000);
+ * ok(wait, "2 seconds should now have elapsed");
+ *
+ * @param aMs the number of miliseconds to wait for
+ * @returns a Promise that resolves to true after the time has elapsed
+ */
+function waitForMs(aMs) {
+ return new Promise(resolve => {
+ setTimeout(done, aMs);
+ function done() {
+ resolve(true);
+ }
+ });
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url) {
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+function waitForCondition(condition, nextTest, errorMsg, aTries, aWait) {
+ let tries = 0;
+ let maxTries = aTries || 100; // 100 tries
+ let maxWait = aWait || 100; // 100 msec x 100 tries = ten seconds
+ let interval = setInterval(function () {
+ if (tries >= maxTries) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ let conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, maxWait);
+ let moveOn = function () {
+ clearInterval(interval);
+ nextTest();
+ };
+}
+
+// Waits for a conditional function defined by the caller to return true.
+function promiseForCondition(aConditionFn, aMessage, aTries, aWait) {
+ return new Promise(resolve => {
+ waitForCondition(
+ aConditionFn,
+ resolve,
+ aMessage || "Condition didn't pass.",
+ aTries,
+ aWait
+ );
+ });
+}
+
+// Returns a promise for nsIObjectLoadingContent props data.
+function promiseForPluginInfo(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return SpecialPowers.spawn(browser, [aId], async function (contentId) {
+ let plugin = content.document.getElementById(contentId);
+ if (!(plugin instanceof Ci.nsIObjectLoadingContent)) {
+ throw new Error("no plugin found");
+ }
+ return {
+ activated: plugin.activated,
+ hasRunningPlugin: plugin.hasRunningPlugin,
+ displayedType: plugin.displayedType,
+ };
+ });
+}
+
+/**
+ * Allows setting focus on a window, and waiting for that window to achieve
+ * focus.
+ *
+ * @param aWindow
+ * The window to focus and wait for.
+ *
+ * @return {Promise}
+ * @resolves When the window is focused.
+ * @rejects Never.
+ */
+function promiseWaitForFocus(aWindow) {
+ return new Promise(resolve => {
+ waitForFocus(resolve, aWindow);
+ });
+}
+
+/**
+ * Returns a Promise that resolves when a notification bar
+ * for a browser is shown. Alternatively, for old-style callers,
+ * can automatically call a callback before it resolves.
+ *
+ * @param notificationID
+ * The ID of the notification to look for.
+ * @param browser
+ * The browser to check for the notification bar.
+ * @param callback (optional)
+ * A function to be called just before the Promise resolves.
+ *
+ * @return Promise
+ */
+function waitForNotificationBar(notificationID, browser, callback) {
+ return new Promise((resolve, reject) => {
+ let notification;
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ waitForCondition(
+ () =>
+ (notification =
+ notificationBox.getNotificationWithValue(notificationID)),
+ () => {
+ ok(
+ notification,
+ `Successfully got the ${notificationID} notification bar`
+ );
+ if (callback) {
+ callback(notification);
+ }
+ resolve(notification);
+ },
+ `Waited too long for the ${notificationID} notification bar`
+ );
+ });
+}
+
+function promiseForNotificationBar(notificationID, browser) {
+ return new Promise(resolve => {
+ waitForNotificationBar(notificationID, browser, resolve);
+ });
+}
+
+/**
+ * Reshow a notification and call a callback when it is reshown.
+ * @param notification
+ * The notification to reshow
+ * @param callback
+ * A function to be called when the notification has been reshown
+ */
+function waitForNotificationShown(notification, callback) {
+ if (PopupNotifications.panel.state == "open") {
+ executeSoon(callback);
+ return;
+ }
+ PopupNotifications.panel.addEventListener(
+ "popupshown",
+ function (e) {
+ callback();
+ },
+ { once: true }
+ );
+ notification.reshow();
+}
+
+function promiseForNotificationShown(notification) {
+ return new Promise(resolve => {
+ waitForNotificationShown(notification, resolve);
+ });
+}
diff --git a/browser/base/content/test/plugins/plugin_bug797677.html b/browser/base/content/test/plugins/plugin_bug797677.html
new file mode 100644
index 0000000000..1545f36475
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_bug797677.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body><embed id="plugin" type="9000"></embed></body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_test.html b/browser/base/content/test/plugins/plugin_test.html
new file mode 100644
index 0000000000..3d4f43e6a5
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 300px; height: 300px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/popupNotifications/browser.ini b/browser/base/content/test/popupNotifications/browser.ini
new file mode 100644
index 0000000000..a5a8ab4eb9
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -0,0 +1,38 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_displayURI.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_2.js]
+https_first_disabled = true
+skip-if = (os == "linux" && (debug || asan)) || (os == "linux" && bits == 64 && os_version == "18.04") # bug 1251135
+[browser_popupNotification_3.js]
+https_first_disabled = true
+skip-if = (os == "linux" && (debug || asan)) || verify
+[browser_popupNotification_4.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_5.js]
+skip-if = true # bug 1332646
+[browser_popupNotification_accesskey.js]
+skip-if = (os == "linux" && (debug || asan)) || os == "mac"
+[browser_popupNotification_checkbox.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_hide_after_identity_panel.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_hide_after_protections_panel.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_keyboard.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_learnmore.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_no_anchors.js]
+https_first_disabled = true
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_security_delay.js]
+[browser_popupNotification_selection_required.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_reshow_in_background.js]
+skip-if = (os == "linux" && (debug || asan))
diff --git a/browser/base/content/test/popupNotifications/browser_displayURI.js b/browser/base/content/test/popupNotifications/browser_displayURI.js
new file mode 100644
index 0000000000..c9e677cd45
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_displayURI.js
@@ -0,0 +1,159 @@
+/*
+ * Make sure that the correct origin is shown for permission prompts.
+ */
+
+async function check(contentTask, options = {}) {
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com/",
+ async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ ok(
+ body.innerHTML.includes("example.com"),
+ "Check that at least the eTLD+1 is present in the markup"
+ );
+ }
+ );
+
+ let channel = NetUtil.newChannel({
+ uri: getRootDirectory(gTestPath),
+ loadUsingSystemPrincipal: true,
+ });
+ channel = channel.QueryInterface(Ci.nsIFileChannel);
+
+ await BrowserTestUtils.withNewTab(
+ channel.file.path,
+ async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ if (
+ notification.id == "geolocation-notification" ||
+ notification.id == "xr-notification"
+ ) {
+ ok(
+ body.innerHTML.includes("local file"),
+ `file:// URIs should be displayed as local file.`
+ );
+ } else {
+ ok(
+ body.innerHTML.includes("Unknown origin"),
+ "file:// URIs should be displayed as unknown origin."
+ );
+ }
+ }
+ );
+
+ if (!options.skipOnExtension) {
+ // Test the scenario also on the extension page if not explicitly unsupported
+ // (e.g. an extension page can't be navigated on a blob URL).
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test Extension Name",
+ },
+ background() {
+ let { browser } = this;
+ browser.test.sendMessage(
+ "extension-tab-url",
+ browser.runtime.getURL("extension-tab-page.html")
+ );
+ },
+ files: {
+ "extension-tab-page.html": `<!DOCTYPE html><html><body></body></html>`,
+ },
+ });
+
+ await extension.startup();
+ let extensionURI = await extension.awaitMessage("extension-tab-url");
+
+ await BrowserTestUtils.withNewTab(extensionURI, async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ ok(
+ body.innerHTML.includes("Test Extension Name"),
+ "Check the the extension name is present in the markup"
+ );
+ });
+
+ await extension.unload();
+ }
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.navigator.permission.fake", true],
+ ["media.navigator.permission.force", true],
+ ["dom.vr.always_support_vr", true],
+ ],
+ });
+});
+
+add_task(async function test_displayURI_geo() {
+ await check(async function () {
+ content.navigator.geolocation.getCurrentPosition(() => {});
+ });
+});
+
+const kVREnabled = SpecialPowers.getBoolPref("dom.vr.enabled");
+if (kVREnabled) {
+ add_task(async function test_displayURI_xr() {
+ await check(async function () {
+ content.navigator.getVRDisplays();
+ });
+ });
+}
+
+add_task(async function test_displayURI_camera() {
+ await check(async function () {
+ content.navigator.mediaDevices.getUserMedia({ video: true, fake: true });
+ });
+});
+
+add_task(async function test_displayURI_geo_blob() {
+ await check(
+ async function () {
+ let text =
+ "<script>navigator.geolocation.getCurrentPosition(() => {})</script>";
+ let blob = new Blob([text], { type: "text/html" });
+ let url = content.URL.createObjectURL(blob);
+ content.location.href = url;
+ },
+ { skipOnExtension: true }
+ );
+});
+
+if (kVREnabled) {
+ add_task(async function test_displayURI_xr_blob() {
+ await check(
+ async function () {
+ let text = "<script>navigator.getVRDisplays()</script>";
+ let blob = new Blob([text], { type: "text/html" });
+ let url = content.URL.createObjectURL(blob);
+ content.location.href = url;
+ },
+ { skipOnExtension: true }
+ );
+ });
+}
+
+add_task(async function test_displayURI_camera_blob() {
+ await check(
+ async function () {
+ let text =
+ "<script>navigator.mediaDevices.getUserMedia({video: true, fake: true})</script>";
+ let blob = new Blob([text], { type: "text/html" });
+ let url = content.URL.createObjectURL(blob);
+ content.location.href = url;
+ },
+ { skipOnExtension: true }
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification.js b/browser/base/content/test/popupNotifications/browser_popupNotification.js
new file mode 100644
index 0000000000..235aa90b5f
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification.js
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 are shared between test #4 to #5
+var wrongBrowserNotificationObject = new BasicNotification("wrongBrowser");
+var wrongBrowserNotification;
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(this.notifyObj.mainActionClicked, "mainAction was clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ is(
+ this.notifyObj.mainActionSource,
+ "button",
+ "main action should have been triggered by button."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ undefined,
+ "shouldn't have a secondary action source."
+ );
+ },
+ },
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ is(
+ this.notifyObj.mainActionSource,
+ undefined,
+ "shouldn't have a main action source."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ "button",
+ "secondary action should have been triggered by button."
+ );
+ },
+ },
+ {
+ id: "Test#2b",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions.push({
+ label: "Extra Secondary Action",
+ accessKey: "E",
+ callback: () => (this.extraSecondaryActionClicked = true),
+ });
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 1);
+ },
+ onHidden(popup) {
+ ok(
+ this.extraSecondaryActionClicked,
+ "extra secondary action was clicked"
+ );
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ {
+ id: "Test#2c",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions.push(
+ {
+ label: "Extra Secondary Action",
+ accessKey: "E",
+ callback: () => ok(false, "unexpected callback invocation"),
+ },
+ {
+ label: "Other Extra Secondary Action",
+ accessKey: "O",
+ callback: () => (this.extraSecondaryActionClicked = true),
+ }
+ );
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 2);
+ },
+ onHidden(popup) {
+ ok(
+ this.extraSecondaryActionClicked,
+ "extra secondary action was clicked"
+ );
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ {
+ id: "Test#3",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // test opening a notification for a background browser
+ // Note: test 4 to 6 share a tab.
+ {
+ id: "Test#4",
+ async run() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ isnot(gBrowser.selectedTab, tab, "new tab isn't selected");
+ wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(tab);
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-backgroundShow"
+ );
+ wrongBrowserNotification = showNotification(
+ wrongBrowserNotificationObject
+ );
+ await promiseTopic;
+ is(PopupNotifications.isPanelOpen, false, "panel isn't open");
+ ok(
+ !wrongBrowserNotificationObject.mainActionClicked,
+ "main action wasn't clicked"
+ );
+ ok(
+ !wrongBrowserNotificationObject.secondaryActionClicked,
+ "secondary action wasn't clicked"
+ );
+ ok(
+ !wrongBrowserNotificationObject.dismissalCallbackTriggered,
+ "dismissal callback wasn't called"
+ );
+ goNext();
+ },
+ },
+ // now select that browser and test to see that the notification appeared
+ {
+ id: "Test#5",
+ run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ },
+ onShown(popup) {
+ checkPopup(popup, wrongBrowserNotificationObject);
+ is(
+ PopupNotifications.isPanelOpen,
+ true,
+ "isPanelOpen getter doesn't lie"
+ );
+
+ // switch back to the old browser
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ onHidden(popup) {
+ // actually remove the notification to prevent it from reappearing
+ ok(
+ wrongBrowserNotificationObject.dismissalCallbackTriggered,
+ "dismissal callback triggered due to tab switch"
+ );
+ wrongBrowserNotification.remove();
+ ok(
+ wrongBrowserNotificationObject.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+ wrongBrowserNotification = null;
+ },
+ },
+ // test that the removed notification isn't shown on browser re-select
+ {
+ id: "Test#6",
+ async run() {
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ await promiseTopic;
+ is(PopupNotifications.isPanelOpen, false, "panel isn't open");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ goNext();
+ },
+ },
+ // Test that two notifications with the same ID result in a single displayed
+ // notification.
+ {
+ id: "Test#7",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ // Show the same notification twice
+ this.notification1 = showNotification(this.notifyObj);
+ this.notification2 = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ this.notification2.remove();
+ },
+ onHidden(popup) {
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test that two notifications with different IDs are displayed
+ {
+ id: "Test#8",
+ run() {
+ this.testNotif1 = new BasicNotification(this.id);
+ this.testNotif1.message += " 1";
+ showNotification(this.testNotif1);
+ this.testNotif2 = new BasicNotification(this.id);
+ this.testNotif2.message += " 2";
+ this.testNotif2.id += "-2";
+ showNotification(this.testNotif2);
+ },
+ onShown(popup) {
+ is(popup.children.length, 2, "two notifications are shown");
+ // Trigger the main command for the first notification, and the secondary
+ // for the second. Need to do mainCommand first since the secondaryCommand
+ // triggering is async.
+ triggerMainCommand(popup);
+ is(popup.children.length, 1, "only one notification left");
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(this.testNotif1.mainActionClicked, "main action #1 was clicked");
+ ok(
+ !this.testNotif1.secondaryActionClicked,
+ "secondary action #1 wasn't clicked"
+ );
+ ok(
+ !this.testNotif1.dismissalCallbackTriggered,
+ "dismissal callback #1 wasn't called"
+ );
+
+ ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
+ ok(
+ this.testNotif2.secondaryActionClicked,
+ "secondary action #2 was clicked"
+ );
+ ok(
+ !this.testNotif2.dismissalCallbackTriggered,
+ "dismissal callback #2 wasn't called"
+ );
+ },
+ },
+ // Test notification without mainAction or secondaryActions, it should fall back
+ // to a default button that dismisses the notification in place of the main action.
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction = null;
+ this.notifyObj.secondaryActions = null;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ let notification = popup.children[0];
+ ok(
+ notification.hasAttribute("buttonhighlight"),
+ "default action is highlighted"
+ );
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test notification without mainAction but with secondaryActions, it should fall back
+ // to a default button that dismisses the notification in place of the main action
+ // and ignore the passed secondaryActions.
+ {
+ id: "Test#10",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction = null;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ let notification = popup.children[0];
+ is(
+ notification.getAttribute("secondarybuttonhidden"),
+ "true",
+ "secondary button is hidden"
+ );
+ ok(
+ notification.hasAttribute("buttonhighlight"),
+ "default action is highlighted"
+ );
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test two notifications with different anchors
+ {
+ id: "Test#11",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.firstNotification = showNotification(this.notifyObj);
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "-2";
+ this.notifyObj2.anchorID = "addons-notification-icon";
+ // Second showNotification() overrides the first
+ this.secondNotification = showNotification(this.notifyObj2);
+ },
+ onShown(popup) {
+ // This also checks that only one element is shown.
+ checkPopup(popup, this.notifyObj2);
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ // Remove the notifications
+ this.firstNotification.remove();
+ this.secondNotification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ ok(
+ this.notifyObj2.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_2.js b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
new file mode 100644
index 0000000000..8738a3b605
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
@@ -0,0 +1,315 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Test optional params
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = undefined;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test that icons appear
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.id = "geolocation";
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ let icon = document.getElementById("geo-notification-icon");
+ isnot(
+ icon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should be visible after dismissal"
+ );
+ this.notification.remove();
+ is(
+ icon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should not be visible after removal"
+ );
+ },
+ },
+
+ // Test that persistence allows the notification to persist across reloads
+ {
+ id: "Test#3",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistence: 2,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Next load will remove the notification
+ this.complete = true;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should only have hidden the notification after 3 page loads"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that a timeout allows the notification to persist across reloads
+ {
+ id: "Test#4",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ // Set a timeout of 10 minutes that should never be hit
+ this.notifyObj.addOptions({
+ timeout: Date.now() + 600000,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Next load will hide the notification
+ this.notification.options.timeout = Date.now() - 1;
+ this.complete = true;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should only have hidden the notification after the timeout was passed"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that setting persistWhileVisible allows a visible notification to
+ // persist across location changes
+ {
+ id: "Test#5",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistWhileVisible: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Notification should persist across location changes
+ this.complete = true;
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should only have hidden the notification after it was dismissed"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+
+ // Test that nested icon nodes correctly activate popups
+ {
+ id: "Test#6",
+ run() {
+ // Add a temporary box as the anchor with a button
+ this.box = document.createXULElement("box");
+ PopupNotifications.iconBox.appendChild(this.box);
+
+ let button = document.createXULElement("button");
+ button.setAttribute("label", "Please click me!");
+ this.box.appendChild(button);
+
+ // The notification should open up on the box
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = this.box.id = "nested-box";
+ this.notifyObj.addOptions({ dismissed: true });
+ this.notification = showNotification(this.notifyObj);
+
+ // This test places a normal button in the notification area, which has
+ // standard GTK styling and dimensions. Due to the clip-path, this button
+ // gets clipped off, which makes it necessary to synthesize the mouse click
+ // a little bit downward. To be safe, I adjusted the x-offset with the same
+ // amount.
+ EventUtils.synthesizeMouse(button, 4, 4, {});
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ this.box.remove();
+ },
+ },
+ // Test that popupnotifications without popups have anchor icons shown
+ {
+ id: "Test#7",
+ async run() {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.anchorID = "geo-notification-icon";
+ notifyObj.addOptions({ neverShow: true });
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ showNotification(notifyObj);
+ await promiseTopic;
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+ goNext();
+ },
+ },
+ // Test that autoplay media icon is shown
+ {
+ id: "Test#8",
+ async run() {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.anchorID = "autoplay-media-notification-icon";
+ notifyObj.addOptions({ neverShow: true });
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ showNotification(notifyObj);
+ await promiseTopic;
+ isnot(
+ document
+ .getElementById("autoplay-media-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "autoplay media icon should be visible"
+ );
+ goNext();
+ },
+ },
+ // Test notification close button
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ EventUtils.synthesizeMouseAtCenter(notification.closebutton, {});
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ ok(
+ !this.notifyObj.secondaryActionClicked,
+ "secondary action not clicked"
+ );
+ },
+ },
+ // Test notification when chrome is hidden
+ {
+ id: "Test#11",
+ run() {
+ window.locationbar.visible = false;
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ is(
+ popup.anchorNode.className,
+ "tabbrowser-tab",
+ "notification anchored to tab"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ window.locationbar.visible = true;
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_3.js b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
new file mode 100644
index 0000000000..e0954e39ca
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
@@ -0,0 +1,377 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Test notification is removed when dismissed if removeOnDismissal is true
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ removeOnDismissal: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test multiple notification icons are shown
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notification2 = showNotification(this.notifyObj2);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj2);
+
+ // check notifyObj1 anchor icon is showing
+ isnot(
+ document
+ .getElementById("default-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "default anchor should be visible"
+ );
+ // check notifyObj2 anchor icon is showing
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification1.remove();
+ ok(
+ this.notifyObj1.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+
+ this.notification2.remove();
+ ok(
+ this.notifyObj2.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+ },
+ },
+ // Test that multiple notification icons are removed when switching tabs
+ {
+ id: "Test#3",
+ async run() {
+ // show the notification on old tab.
+ this.notifyObjOld = new BasicNotification(this.id);
+ this.notifyObjOld.anchorID = "default-notification-icon";
+ this.notificationOld = showNotification(this.notifyObjOld);
+
+ // switch tab
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ // show the notification on new tab.
+ this.notifyObjNew = new BasicNotification(this.id);
+ this.notifyObjNew.anchorID = "geo-notification-icon";
+ this.notificationNew = showNotification(this.notifyObjNew);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObjNew);
+
+ // check notifyObjOld anchor icon is removed
+ is(
+ document
+ .getElementById("default-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "default anchor shouldn't be visible"
+ );
+ // check notifyObjNew anchor icon is showing
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notificationNew.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ gBrowser.selectedTab = this.oldSelectedTab;
+ this.notificationOld.remove();
+ },
+ },
+ // test security delay - too early
+ {
+ id: "Test#4",
+ async run() {
+ // Set the security delay to 100s
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.notification_enable_delay", 100000]],
+ });
+
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+
+ // Wait to see if the main command worked
+ executeSoon(function delayedDismissal() {
+ dismissNotification(popup);
+ });
+ },
+ onHidden(popup) {
+ ok(
+ !this.notifyObj.mainActionClicked,
+ "mainAction was not clicked because it was too soon"
+ );
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was triggered"
+ );
+ },
+ },
+ // test security delay - after delay
+ {
+ id: "Test#5",
+ async run() {
+ // Set the security delay to 10ms
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.notification_enable_delay", 10]],
+ });
+
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ // Wait until after the delay to trigger the main action
+ setTimeout(function delayedDismissal() {
+ triggerMainCommand(popup);
+ }, 500);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.mainActionClicked,
+ "mainAction was clicked after the delay"
+ );
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was not triggered"
+ );
+ },
+ },
+ // reload removes notification
+ {
+ id: "Test#6",
+ async run() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(true, "Notification removed in background tab after reloading");
+ goNext();
+ }
+ };
+ showNotification(notifyObj);
+ executeSoon(function () {
+ gBrowser.selectedBrowser.reload();
+ });
+ },
+ },
+ // location change in background tab removes notification
+ {
+ id: "Test#7",
+ async run() {
+ let oldSelectedTab = gBrowser.selectedTab;
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ gBrowser.selectedTab = oldSelectedTab;
+ let browser = gBrowser.getBrowserForTab(newTab);
+
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.browser = browser;
+ notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(true, "Notification removed in background tab after reloading");
+ executeSoon(function () {
+ gBrowser.removeTab(newTab);
+ goNext();
+ });
+ }
+ };
+ showNotification(notifyObj);
+ executeSoon(function () {
+ browser.reload();
+ });
+ },
+ },
+ // Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
+ {
+ id: "Test#8",
+ async run() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let originalTab = gBrowser.selectedTab;
+ let bgTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ let anchor = document.createXULElement("box");
+ anchor.id = "test26-anchor";
+ anchor.className = "notification-anchor-icon";
+ PopupNotifications.iconBox.appendChild(anchor);
+
+ gBrowser.selectedTab = originalTab;
+
+ let fgNotifyObj = new BasicNotification(this.id);
+ fgNotifyObj.anchorID = anchor.id;
+ fgNotifyObj.options.dismissed = true;
+ let fgNotification = showNotification(fgNotifyObj);
+
+ let bgNotifyObj = new BasicNotification(this.id);
+ bgNotifyObj.anchorID = anchor.id;
+ bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
+ // show the notification in the background tab ...
+ let bgNotification = showNotification(bgNotifyObj);
+ // ... and re-show it
+ bgNotification = showNotification(bgNotifyObj);
+
+ ok(fgNotification.id, "notification has id");
+ is(fgNotification.id, bgNotification.id, "notification ids are the same");
+ is(anchor.getAttribute("showing"), "true", "anchor still showing");
+
+ fgNotification.remove();
+ gBrowser.removeTab(bgTab);
+ goNext();
+ },
+ },
+ // location change in an embedded frame should not remove a notification
+ {
+ id: "Test#9",
+ async run() {
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ "data:text/html;charset=utf8,<iframe%20id='iframe'%20src='http://example.com/'>"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(
+ false,
+ "Notification removed from browser when subframe navigated"
+ );
+ }
+ };
+ showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ info("Adding observer and performing navigation");
+
+ await Promise.all([
+ BrowserUtils.promiseObserved("window-global-created", wgp =>
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ wgp.documentURI.spec.startsWith("http://example.org/")
+ ),
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.document
+ .getElementById("iframe")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .setAttribute("src", "http://example.org/");
+ }),
+ ]);
+
+ executeSoon(() => {
+ let notification = PopupNotifications.getNotification(
+ this.notifyObj.id,
+ this.notifyObj.browser
+ );
+ ok(
+ notification != null,
+ "Notification remained when subframe navigated"
+ );
+ this.notifyObj.options.eventCallback = undefined;
+
+ notification.remove();
+ });
+ },
+ onHidden() {},
+ },
+ // Popup Notifications should catch exceptions from callbacks
+ {
+ id: "Test#10",
+ run() {
+ this.testNotif1 = new BasicNotification(this.id);
+ this.testNotif1.message += " 1";
+ this.notification1 = showNotification(this.testNotif1);
+ this.testNotif1.options.eventCallback = function (eventName) {
+ info("notifyObj1.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 1!");
+ }
+ };
+
+ this.testNotif2 = new BasicNotification(this.id);
+ this.testNotif2.message += " 2";
+ this.testNotif2.id += "-2";
+ this.testNotif2.options.eventCallback = function (eventName) {
+ info("notifyObj2.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 2!");
+ }
+ };
+ this.notification2 = showNotification(this.testNotif2);
+ },
+ onShown(popup) {
+ is(popup.children.length, 2, "two notifications are shown");
+ dismissNotification(popup);
+ },
+ onHidden() {
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
new file mode 100644
index 0000000000..b0e8f016ef
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Popup Notifications main actions should catch exceptions from callbacks
+ {
+ id: "Test#1",
+ run() {
+ this.testNotif = new ErrorNotification(this.id);
+ showNotification(this.testNotif);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.testNotif);
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(this.testNotif.mainActionClicked, "main action has been triggered");
+ },
+ },
+ // Popup Notifications secondary actions should catch exceptions from callbacks
+ {
+ id: "Test#2",
+ run() {
+ this.testNotif = new ErrorNotification(this.id);
+ showNotification(this.testNotif);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.testNotif);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ this.testNotif.secondaryActionClicked,
+ "secondary action has been triggered"
+ );
+ },
+ },
+ // Existing popup notification shouldn't disappear when adding a dismissed notification
+ {
+ id: "Test#3",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notification1 = showNotification(this.notifyObj1);
+ },
+ onShown(popup) {
+ // Now show a dismissed notification, and check that it doesn't clobber
+ // the showing one.
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.dismissed = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ checkPopup(popup, this.notifyObj1);
+
+ // check that both anchor icons are showing
+ is(
+ document
+ .getElementById("default-notification-icon")
+ .getAttribute("showing"),
+ "true",
+ "notification1 anchor should be visible"
+ );
+ is(
+ document
+ .getElementById("geo-notification-icon")
+ .getAttribute("showing"),
+ "true",
+ "notification2 anchor should be visible"
+ );
+
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ },
+ // Showing should be able to modify the popup data
+ {
+ id: "Test#4",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ let normalCallback = this.notifyObj.options.eventCallback;
+ this.notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "showing") {
+ this.mainAction.label = "Alternate Label";
+ }
+ normalCallback.call(this, eventName);
+ };
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ // checkPopup checks for the matching label. Note that this assumes that
+ // this.notifyObj.mainAction is the same as notification.mainAction,
+ // which could be a problem if we ever decided to deep-copy.
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+ // Moving a tab to a new window should remove non-swappable notifications.
+ {
+ id: "Test#5",
+ async run() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ let notifyObj = new BasicNotification(this.id);
+
+ let shown = waitForNotificationPanel();
+ showNotification(notifyObj);
+ await shown;
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ let win = await promiseWin;
+
+ let anchor = win.document.getElementById("default-notification-icon");
+ win.PopupNotifications._reshowNotifications(anchor);
+ ok(
+ !win.PopupNotifications.panel.children.length,
+ "no notification displayed in new window"
+ );
+ ok(
+ notifyObj.swappingCallbackTriggered,
+ "the swapping callback was triggered"
+ );
+ ok(
+ notifyObj.removedCallbackTriggered,
+ "the removed callback was triggered"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ goNext();
+ },
+ },
+ // Moving a tab to a new window should preserve swappable notifications.
+ {
+ id: "Test#6",
+ async run() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ let notifyObj = new BasicNotification(this.id);
+ let originalCallback = notifyObj.options.eventCallback;
+ notifyObj.options.eventCallback = function (eventName) {
+ originalCallback(eventName);
+ return eventName == "swapping";
+ };
+
+ let shown = waitForNotificationPanel();
+ let notification = showNotification(notifyObj);
+ await shown;
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ let win = await promiseWin;
+ await waitForWindowReadyForPopupNotifications(win);
+
+ await new Promise(resolve => {
+ let callback = notification.options.eventCallback;
+ notification.options.eventCallback = function (eventName) {
+ callback(eventName);
+ if (eventName == "shown") {
+ resolve();
+ }
+ };
+ info("Showing the notification again");
+ notification.reshow();
+ });
+
+ checkPopup(win.PopupNotifications.panel, notifyObj);
+ ok(
+ notifyObj.swappingCallbackTriggered,
+ "the swapping callback was triggered"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ goNext();
+ },
+ },
+ // the main action callback can keep the notification.
+ {
+ id: "Test#8",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction.dismiss = true;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was triggered"
+ );
+ ok(
+ !this.notifyObj.removedCallbackTriggered,
+ "removed callback wasn't triggered"
+ );
+ this.notification.remove();
+ },
+ },
+ // a secondary action callback can keep the notification.
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions[0].dismiss = true;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was triggered"
+ );
+ ok(
+ !this.notifyObj.removedCallbackTriggered,
+ "removed callback wasn't triggered"
+ );
+ this.notification.remove();
+ },
+ },
+ // returning true in the showing callback should dismiss the notification.
+ {
+ id: "Test#10",
+ run() {
+ let notifyObj = new BasicNotification(this.id);
+ let originalCallback = notifyObj.options.eventCallback;
+ notifyObj.options.eventCallback = function (eventName) {
+ originalCallback(eventName);
+ return eventName == "showing";
+ };
+
+ let notification = showNotification(notifyObj);
+ ok(
+ notifyObj.showingCallbackTriggered,
+ "the showing callback was triggered"
+ );
+ ok(
+ !notifyObj.shownCallbackTriggered,
+ "the shown callback wasn't triggered"
+ );
+ notification.remove();
+ goNext();
+ },
+ },
+ // the main action button should apply non-default(no highlight) style.
+ {
+ id: "Test#11",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = undefined;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden() {},
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_5.js b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
new file mode 100644
index 0000000000..839262caa0
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
@@ -0,0 +1,501 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var gNotification;
+
+var tests = [
+ // panel updates should fire the showing and shown callbacks again.
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ this.notifyObj.showingCallbackTriggered = false;
+ this.notifyObj.shownCallbackTriggered = false;
+
+ // Force an update of the panel. This is typically called
+ // automatically when receiving 'activate' or 'TabSelect' events,
+ // but from a setTimeout, which is inconvenient for the test.
+ PopupNotifications._update();
+
+ checkPopup(popup, this.notifyObj);
+
+ this.notification.remove();
+ },
+ onHidden() {},
+ },
+ // A first dismissed notification shouldn't stop _update from showing a second notification
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.dismissed = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.dismissed = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notification2.dismissed = false;
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj2);
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ onHidden(popup) {},
+ },
+ // The anchor icon should be shown for notifications in background windows.
+ {
+ id: "Test#3",
+ async run() {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.dismissed = true;
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Open the notification in the original window, now in the background.
+ let notification = showNotification(notifyObj);
+ let anchor = document.getElementById("default-notification-icon");
+ is(anchor.getAttribute("showing"), "true", "the anchor is shown");
+ notification.remove();
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ goNext();
+ },
+ },
+ // Test that persistent doesn't allow the notification to persist after
+ // navigation.
+ {
+ id: "Test#4",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+
+ // This code should not be executed.
+ ok(false, "Should have removed the notification after navigation");
+ // Properly dismiss and cleanup in case the unthinkable happens.
+ this.complete = true;
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ !this.complete,
+ "Should have hidden the notification after navigation"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that persistent allows the notification to persist until explicitly
+ // dismissed.
+ {
+ id: "Test#5",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+
+ // Notification should persist after attempt to dismiss by clicking on the
+ // content area.
+ let browser = gBrowser.selectedBrowser;
+ await BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
+
+ // Notification should be hidden after dismissal via Don't Allow.
+ this.complete = true;
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should have hidden the notification after clicking Not Now"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that persistent panels are still open after switching to another tab
+ // and back.
+ {
+ id: "Test#6a",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.persistent = true;
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ },
+ onHidden(popup) {
+ ok(true, "Should have hidden the notification after tab switch");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Second part of the previous test that compensates for the limitation in
+ // runNextTest that expects a single onShown/onHidden invocation per test.
+ {
+ id: "Test#6b",
+ run() {
+ let id =
+ PopupNotifications.panel.firstElementChild.getAttribute("popupid");
+ ok(
+ id.endsWith("Test#6a"),
+ "Should have found the notification from Test6a"
+ );
+ ok(
+ PopupNotifications.isPanelOpen,
+ "Should have shown the popup again after getting back to the tab"
+ );
+ gNotification.remove();
+ gNotification = null;
+ goNext();
+ },
+ },
+ // Test that persistent panels are still open after switching to another
+ // window and back.
+ {
+ id: "Test#7",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ let firstTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ let shown = waitForNotificationPanel();
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.persistent = true;
+ this.notification = showNotification(notifyObj);
+ await shown;
+
+ ok(
+ notifyObj.shownCallbackTriggered,
+ "Should have triggered the shown event"
+ );
+ ok(
+ notifyObj.showingCallbackTriggered,
+ "Should have triggered the showing event"
+ );
+ // Reset to false so that we can ensure these are not fired a second time.
+ notifyObj.shownCallbackTriggered = false;
+ notifyObj.showingCallbackTriggered = false;
+ let timeShown = this.notification.timeShown;
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(firstTab);
+ let win = await promiseWin;
+
+ let anchor = win.document.getElementById("default-notification-icon");
+ win.PopupNotifications._reshowNotifications(anchor);
+ ok(
+ !win.PopupNotifications.panel.children.length,
+ "no notification displayed in new window"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ let id =
+ PopupNotifications.panel.firstElementChild.getAttribute("popupid");
+ ok(
+ id.endsWith("Test#7"),
+ "Should have found the notification from Test7"
+ );
+ ok(
+ PopupNotifications.isPanelOpen,
+ "Should have kept the popup on the first window"
+ );
+ ok(
+ !notifyObj.dismissalCallbackTriggered,
+ "Should not have triggered a dismissed event"
+ );
+ ok(
+ !notifyObj.shownCallbackTriggered,
+ "Should not have triggered a second shown event"
+ );
+ ok(
+ !notifyObj.showingCallbackTriggered,
+ "Should not have triggered a second showing event"
+ );
+ ok(
+ this.notification.timeShown > timeShown,
+ "should have updated timeShown to restart the security delay"
+ );
+
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+
+ goNext();
+ },
+ },
+ // Test that only the first persistent notification is shown on update
+ {
+ id: "Test#8",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj1);
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that persistent notifications are shown stacked by anchor on update
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notifyObj3 = new BasicNotification(this.id);
+ this.notifyObj3.id += "_3";
+ this.notifyObj3.anchorID = "default-notification-icon";
+ this.notifyObj3.options.persistent = true;
+ this.notification3 = showNotification(this.notifyObj3);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 2, "two notifications displayed");
+ let [notification1, notification2] = notifications;
+ is(
+ notification1.id,
+ this.notifyObj1.id + "-notification",
+ "id 1 matches"
+ );
+ is(
+ notification2.id,
+ this.notifyObj3.id + "-notification",
+ "id 2 matches"
+ );
+
+ this.notification1.remove();
+ this.notification2.remove();
+ this.notification3.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that on closebutton click, only the persistent notification
+ // that contained the closebutton loses its persistent status.
+ {
+ id: "Test#10",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "geo-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notifyObj1.options.hideClose = false;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notifyObj2.options.hideClose = false;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notifyObj3 = new BasicNotification(this.id);
+ this.notifyObj3.id += "_3";
+ this.notifyObj3.anchorID = "geo-notification-icon";
+ this.notifyObj3.options.persistent = true;
+ this.notifyObj3.options.hideClose = false;
+ this.notification3 = showNotification(this.notifyObj3);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 3, "three notifications displayed");
+ EventUtils.synthesizeMouseAtCenter(notifications[1].closebutton, {});
+ },
+ onHidden(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 2, "two notifications displayed");
+
+ ok(this.notification1.options.persistent, "notification 1 is persistent");
+ ok(
+ !this.notification2.options.persistent,
+ "notification 2 is not persistent"
+ );
+ ok(this.notification3.options.persistent, "notification 3 is persistent");
+
+ this.notification1.remove();
+ this.notification2.remove();
+ this.notification3.remove();
+ },
+ },
+ // Test clicking the anchor icon.
+ // Clicking the anchor of an already visible persistent notification should
+ // focus the main action button, but not cause additional showing/shown event
+ // callback calls.
+ // Clicking the anchor of a dismissed notification should show it, even when
+ // the currently displayed notification is a persistent one.
+ {
+ id: "Test#11",
+ async run() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+
+ function clickAnchor(notifyObj) {
+ let anchor = document.getElementById(notifyObj.anchorID);
+ EventUtils.synthesizeMouseAtCenter(anchor, {});
+ }
+
+ let popup = PopupNotifications.panel;
+
+ let notifyObj1 = new BasicNotification(this.id);
+ notifyObj1.id += "_1";
+ notifyObj1.anchorID = "default-notification-icon";
+ notifyObj1.options.persistent = true;
+ let shown = waitForNotificationPanel();
+ let notification1 = showNotification(notifyObj1);
+ await shown;
+ checkPopup(popup, notifyObj1);
+ ok(
+ !notifyObj1.dismissalCallbackTriggered,
+ "Should not have dismissed the notification"
+ );
+ notifyObj1.shownCallbackTriggered = false;
+ notifyObj1.showingCallbackTriggered = false;
+
+ // Click the anchor. This should focus the closebutton
+ // (because it's the first focusable element), but not
+ // call event callbacks on the notification object.
+ clickAnchor(notifyObj1);
+ is(document.activeElement, popup.children[0].closebutton);
+ ok(
+ !notifyObj1.dismissalCallbackTriggered,
+ "Should not have dismissed the notification"
+ );
+ ok(
+ !notifyObj1.shownCallbackTriggered,
+ "Should have triggered the shown event again"
+ );
+ ok(
+ !notifyObj1.showingCallbackTriggered,
+ "Should have triggered the showing event again"
+ );
+
+ // Add another notification.
+ let notifyObj2 = new BasicNotification(this.id);
+ notifyObj2.id += "_2";
+ notifyObj2.anchorID = "geo-notification-icon";
+ notifyObj2.options.dismissed = true;
+ let notification2 = showNotification(notifyObj2);
+
+ // Click the anchor of the second notification, this should dismiss the
+ // first notification.
+ shown = waitForNotificationPanel();
+ clickAnchor(notifyObj2);
+ await shown;
+ checkPopup(popup, notifyObj2);
+ ok(
+ notifyObj1.dismissalCallbackTriggered,
+ "Should have dismissed the first notification"
+ );
+
+ // Click the anchor of the first notification, it should be shown again.
+ shown = waitForNotificationPanel();
+ clickAnchor(notifyObj1);
+ await shown;
+ checkPopup(popup, notifyObj1);
+ ok(
+ notifyObj2.dismissalCallbackTriggered,
+ "Should have dismissed the second notification"
+ );
+
+ // Cleanup.
+ notification1.remove();
+ notification2.remove();
+ goNext();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js
new file mode 100644
index 0000000000..4a68105e27
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+let buttonPressed = false;
+
+function commandTriggered() {
+ buttonPressed = true;
+}
+
+var tests = [
+ // This test ensures that the accesskey closes the popup.
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ window.addEventListener("command", commandTriggered, true);
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("VK_ALT", { type: "keydown" });
+ EventUtils.synthesizeKey("M", { altKey: true });
+ EventUtils.synthesizeKey("VK_ALT", { type: "keyup" });
+
+ // If bug xxx was present, then the popup would be in the
+ // process of being hidden right now.
+ isnot(popup.state, "hiding", "popup is not hiding");
+ },
+ onHidden(popup) {
+ window.removeEventListener("command", commandTriggered, true);
+ ok(buttonPressed, "button pressed");
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
new file mode 100644
index 0000000000..c1d82042c8
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
@@ -0,0 +1,248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+function checkCheckbox(checkbox, label, checked = false, hidden = false) {
+ is(checkbox.label, label, "Checkbox should have the correct label");
+ is(checkbox.hidden, hidden, "Checkbox should be shown");
+ is(checkbox.checked, checked, "Checkbox should be checked by default");
+}
+
+function checkMainAction(notification, disabled = false) {
+ let mainAction = notification.button;
+ let warningLabel = notification.querySelector(".popup-notification-warning");
+ is(warningLabel.hidden, !disabled, "Warning label should be shown");
+ is(mainAction.disabled, disabled, "MainAction should be disabled");
+}
+
+function promiseElementVisible(element) {
+ // HTMLElement.offsetParent is null when the element is not visisble
+ // (or if the element has |position: fixed|). See:
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+ return TestUtils.waitForCondition(
+ () => element.offsetParent !== null,
+ "Waiting for element to be visible"
+ );
+}
+
+var gNotification;
+
+var tests = [
+ // Test that passing the checkbox field shows the checkbox.
+ {
+ id: "show_checkbox",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ checkCheckbox(notification.checkbox, "This is a checkbox");
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+
+ // Test checkbox being checked by default
+ {
+ id: "checkbox_checked",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "Check this",
+ checked: true,
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ checkCheckbox(notification.checkbox, "Check this", true);
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+
+ // Test checkbox passing the checkbox state on mainAction
+ {
+ id: "checkbox_passCheckboxChecked_mainAction",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction.callback = ({ checkboxChecked }) =>
+ (this.mainActionChecked = checkboxChecked);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ triggerMainCommand(popup);
+ },
+ onHidden() {
+ is(
+ this.mainActionChecked,
+ true,
+ "mainAction callback is passed the correct checkbox value"
+ );
+ },
+ },
+
+ // Test checkbox passing the checkbox state on secondaryAction
+ {
+ id: "checkbox_passCheckboxChecked_secondaryAction",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = [
+ {
+ label: "Test Secondary",
+ accessKey: "T",
+ callback: ({ checkboxChecked }) =>
+ (this.secondaryActionChecked = checkboxChecked),
+ },
+ ];
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden() {
+ is(
+ this.secondaryActionChecked,
+ true,
+ "secondaryAction callback is passed the correct checkbox value"
+ );
+ },
+ },
+
+ // Test checkbox preserving its state through re-opening the doorhanger
+ {
+ id: "checkbox_reopen",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ checkedState: {
+ disableMainAction: true,
+ warningLabel: "Testing disable",
+ },
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ dismissNotification(popup);
+ },
+ async onHidden(popup) {
+ let icon = document.getElementById("default-notification-icon");
+ let shown = waitForNotificationPanel();
+ EventUtils.synthesizeMouseAtCenter(icon, {});
+ await shown;
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ checkMainAction(notification, true);
+ gNotification.remove();
+ },
+ },
+
+ // Test no checkbox hides warning label
+ {
+ id: "no_checkbox",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = null;
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ checkCheckbox(notification.checkbox, "", false, true);
+ checkMainAction(notification);
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+];
+
+// Test checkbox disabling the main action in different combinations
+["checkedState", "uncheckedState"].forEach(function (state) {
+ [true, false].forEach(function (checked) {
+ tests.push({
+ id: `checkbox_disableMainAction_${state}_${
+ checked ? "checked" : "unchecked"
+ }`,
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ checked,
+ [state]: {
+ disableMainAction: true,
+ warningLabel: "Testing disable",
+ },
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ let disabled =
+ (state === "checkedState" && checked) ||
+ (state === "uncheckedState" && !checked);
+
+ checkCheckbox(checkbox, "This is a checkbox", checked);
+ checkMainAction(notification, disabled);
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", !checked);
+ checkMainAction(notification, !disabled);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", checked);
+ checkMainAction(notification, disabled);
+
+ // Unblock the main command if it's currently disabled.
+ if (disabled) {
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ }
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ });
+ });
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js
new file mode 100644
index 0000000000..de930375f6
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_displayURI_geo() {
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com/",
+ async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.navigator.geolocation.getCurrentPosition(() => {});
+ });
+ await popupShownPromise;
+
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ EventUtils.synthesizeMouseAtCenter(gIdentityHandler._identityIconBox, {});
+ await popupShownPromise;
+
+ Assert.ok(!PopupNotifications.isPanelOpen, "Geolocation popup is hidden");
+
+ let popupHidden = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ gIdentityHandler._identityPopup.hidePopup();
+ await popupHidden;
+
+ Assert.ok(PopupNotifications.isPanelOpen, "Geolocation popup is showing");
+ }
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js
new file mode 100644
index 0000000000..f47f20a2d7
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_hide_popup_with_protections_panel_showing() {
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com/",
+ async function (browser) {
+ // Request location permissions and wait for that prompt to appear.
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.navigator.geolocation.getCurrentPosition(() => {});
+ });
+ await popupShownPromise;
+
+ // Click on the icon for the protections panel, to show the panel.
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gProtectionsHandler._protectionsPopup
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("tracking-protection-icon-container"),
+ {}
+ );
+ await popupShownPromise;
+
+ // Make sure the location permission prompt closed.
+ Assert.ok(!PopupNotifications.isPanelOpen, "Geolocation popup is hidden");
+
+ // Close the protections panel.
+ let popupHidden = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ gProtectionsHandler._protectionsPopup.hidePopup();
+ await popupHidden;
+
+ // Make sure the location permission prompt came back.
+ Assert.ok(PopupNotifications.isPanelOpen, "Geolocation popup is showing");
+ }
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
new file mode 100644
index 0000000000..5c20751c3f
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ // Force tabfocus for all elements on OSX.
+ SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] }).then(
+ setup
+ );
+}
+
+// Focusing on notification icon buttons is handled by the ToolbarKeyboardNavigator
+// component and arrow keys (see browser/base/content/browser-toolbarKeyNav.js).
+function focusNotificationAnchor(anchor) {
+ let urlbarContainer = anchor.closest("#urlbar-container");
+ urlbarContainer.querySelector("toolbartabstop").focus();
+ const trackingProtectionIconContainer = urlbarContainer.querySelector(
+ "#tracking-protection-icon-container"
+ );
+ is(
+ document.activeElement,
+ trackingProtectionIconContainer,
+ "tracking protection icon container is focused."
+ );
+ while (document.activeElement !== anchor) {
+ EventUtils.synthesizeKey("ArrowRight");
+ }
+}
+
+var tests = [
+ // Test that for persistent notifications,
+ // the secondary action is triggered by pressing the escape key.
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.persistent = true;
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("KEY_Escape");
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ is(
+ this.notifyObj.mainActionSource,
+ undefined,
+ "shouldn't have a main action source."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ "esc-press",
+ "secondary action should be from ESC key press"
+ );
+ },
+ },
+ // Test that for non-persistent notifications, the escape key dismisses the notification.
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("KEY_Escape");
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(
+ !this.notifyObj.secondaryActionClicked,
+ "secondaryAction was not clicked"
+ );
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ ok(
+ !this.notifyObj.removedCallbackTriggered,
+ "removed callback was not triggered"
+ );
+ is(
+ this.notifyObj.mainActionSource,
+ undefined,
+ "shouldn't have a main action source."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ undefined,
+ "shouldn't have a secondary action source."
+ );
+ this.notification.remove();
+ },
+ },
+ // Test that the space key on an anchor element focuses an active notification
+ {
+ id: "Test#3",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let anchor = document.getElementById(this.notifyObj.anchorID);
+ focusNotificationAnchor(anchor);
+ EventUtils.sendString(" ");
+ is(document.activeElement, popup.children[0].closebutton);
+ this.notification.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that you can switch between active notifications with the space key
+ // and that the notification is focused on selection.
+ {
+ id: "Test#4",
+ async run() {
+ let notifyObj1 = new BasicNotification(this.id);
+ notifyObj1.id += "_1";
+ notifyObj1.anchorID = "default-notification-icon";
+ notifyObj1.addOptions({
+ hideClose: true,
+ checkbox: {
+ label: "Test that elements inside the panel can be focused",
+ },
+ persistent: true,
+ });
+ let opened = waitForNotificationPanel();
+ let notification1 = showNotification(notifyObj1);
+ await opened;
+
+ let notifyObj2 = new BasicNotification(this.id);
+ notifyObj2.id += "_2";
+ notifyObj2.anchorID = "geo-notification-icon";
+ notifyObj2.addOptions({
+ persistent: true,
+ });
+ opened = waitForNotificationPanel();
+ let notification2 = showNotification(notifyObj2);
+ let popup = await opened;
+
+ // Make sure notification 2 is visible
+ checkPopup(popup, notifyObj2);
+
+ // Activate the anchor for notification 1 and wait until it's shown.
+ let anchor = document.getElementById(notifyObj1.anchorID);
+ focusNotificationAnchor(anchor);
+ is(document.activeElement, anchor);
+ opened = waitForNotificationPanel();
+ EventUtils.sendString(" ");
+ popup = await opened;
+ checkPopup(popup, notifyObj1);
+
+ is(document.activeElement, popup.children[0].checkbox);
+
+ // Activate the anchor for notification 2 and wait until it's shown.
+ anchor = document.getElementById(notifyObj2.anchorID);
+ focusNotificationAnchor(anchor);
+ is(document.activeElement, anchor);
+ opened = waitForNotificationPanel();
+ EventUtils.sendString(" ");
+ popup = await opened;
+ checkPopup(popup, notifyObj2);
+
+ is(document.activeElement, popup.children[0].closebutton);
+
+ notification1.remove();
+ notification2.remove();
+ goNext();
+ },
+ },
+ // Test that passing the autofocus option will focus an opened notification.
+ {
+ id: "Test#5",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ autofocus: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ // Initial focus on open is null because a panel itself
+ // can not be focused, next tab focus will be inside the panel.
+ is(Services.focus.focusedElement, null);
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(Services.focus.focusedElement, popup.children[0].closebutton);
+ dismissNotification(popup);
+ },
+ async onHidden() {
+ // Focus the urlbar to check that it stays focused.
+ gURLBar.focus();
+
+ // Show another notification and make sure it's not autofocused.
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.id += "_2";
+ notifyObj.anchorID = "default-notification-icon";
+
+ let opened = waitForNotificationPanel();
+ let notification = showNotification(notifyObj);
+ let popup = await opened;
+ checkPopup(popup, notifyObj);
+
+ // Check that the urlbar is still focused.
+ is(Services.focus.focusedElement, gURLBar.inputField);
+
+ this.notification.remove();
+ notification.remove();
+ },
+ },
+ // Test that focus is not moved out of a content element if autofocus is not set.
+ {
+ id: "Test#6",
+ async run() {
+ let id = this.id;
+ await BrowserTestUtils.withNewTab(
+ "data:text/html,<input id='test-input'/>",
+ async function (browser) {
+ let notifyObj = new BasicNotification(id);
+ await SpecialPowers.spawn(browser, [], function () {
+ content.document.getElementById("test-input").focus();
+ });
+
+ let opened = waitForNotificationPanel();
+ let notification = showNotification(notifyObj);
+ await opened;
+
+ // Check that the focused element in the chrome window
+ // is either the browser in case we're running on e10s
+ // or the input field in case of non-e10s.
+ if (gMultiProcessBrowser) {
+ is(Services.focus.focusedElement, browser);
+ } else {
+ is(
+ Services.focus.focusedElement,
+ browser.contentDocument.getElementById("test-input")
+ );
+ }
+
+ // Check that the input field is still focused inside the browser.
+ await SpecialPowers.spawn(browser, [], function () {
+ is(
+ content.document.activeElement,
+ content.document.getElementById("test-input")
+ );
+ });
+
+ notification.remove();
+ }
+ );
+ goNext();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js b/browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js
new file mode 100644
index 0000000000..fc3946598c
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Test checkbox being checked by default
+ {
+ id: "without_learn_more",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let link = notification.querySelector(
+ ".popup-notification-learnmore-link"
+ );
+ ok(!link.href, "no href");
+ is(
+ window.getComputedStyle(link).getPropertyValue("display"),
+ "none",
+ "link hidden"
+ );
+ dismissNotification(popup);
+ },
+ onHidden() {},
+ },
+
+ // Test that passing the learnMoreURL field sets up the link.
+ {
+ id: "with_learn_more",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.learnMoreURL = "https://mozilla.org";
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let link = notification.querySelector(
+ ".popup-notification-learnmore-link"
+ );
+ is(link.textContent, "Learn more", "correct label");
+ is(link.href, "https://mozilla.org", "correct href");
+ isnot(
+ window.getComputedStyle(link).getPropertyValue("display"),
+ "none",
+ "link not hidden"
+ );
+ dismissNotification(popup);
+ },
+ onHidden() {},
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js b/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js
new file mode 100644
index 0000000000..a73e1f5948
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+const FALLBACK_ANCHOR = gURLBar.searchButton
+ ? "urlbar-search-button"
+ : "identity-icon";
+
+var tests = [
+ // Test that popupnotifications are anchored to the fallback anchor on
+ // about:blank, where anchor icons are hidden.
+ {
+ id: "Test#1",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+ is(
+ popup.anchorNode.id,
+ FALLBACK_ANCHOR,
+ "notification anchored to fallback anchor"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that popupnotifications are anchored to the fallback anchor after
+ // navigation to about:blank.
+ {
+ id: "Test#2",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ persistence: 1,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ await promiseTabLoadEvent(gBrowser.selectedTab, "about:blank");
+
+ checkPopup(popup, this.notifyObj);
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+ is(
+ popup.anchorNode.id,
+ FALLBACK_ANCHOR,
+ "notification anchored to fallback anchor"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that dismissed popupnotifications cannot be opened on about:blank, but
+ // can be opened after navigation.
+ {
+ id: "Test#3",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ dismissed: true,
+ persistence: 1,
+ });
+ this.notification = showNotification(this.notifyObj);
+
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ EventUtils.synthesizeMouse(
+ document.getElementById("geo-notification-icon"),
+ 2,
+ 2,
+ {}
+ );
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that popupnotifications are hidden while editing the URL in the
+ // location bar, anchored to the fallback anchor when the focus is moved away
+ // from the location bar, and restored when the URL is reverted.
+ {
+ id: "Test#4",
+ async run() {
+ for (let persistent of [false, true]) {
+ let shown = waitForNotificationPanel();
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({ persistent });
+ this.notification = showNotification(this.notifyObj);
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ // Typing in the location bar should hide the notification.
+ let hidden = waitForNotificationPanelHidden();
+ gURLBar.select();
+ EventUtils.sendString("*");
+ await hidden;
+
+ is(
+ document
+ .getElementById("geo-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+
+ // Moving focus to the next control should show the notifications again,
+ // anchored to the fallback anchor. We clear the URL bar before moving the
+ // focus so that the awesomebar popup doesn't get in the way.
+ shown = waitForNotificationPanel();
+ EventUtils.synthesizeKey("KEY_Backspace");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await shown;
+
+ is(
+ PopupNotifications.panel.anchorNode.id,
+ FALLBACK_ANCHOR,
+ "notification anchored to fallback anchor"
+ );
+
+ // Moving focus to the location bar should hide the notification again.
+ hidden = waitForNotificationPanelHidden();
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await hidden;
+
+ // Reverting the URL should show the notification again.
+ shown = waitForNotificationPanel();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ hidden = waitForNotificationPanelHidden();
+ this.notification.remove();
+ await hidden;
+ }
+ goNext();
+ },
+ },
+ // Test that popupnotifications triggered while editing the URL in the
+ // location bar are only shown later when the URL is reverted.
+ {
+ id: "Test#5",
+ async run() {
+ for (let persistent of [false, true]) {
+ // Start editing the URL, ensuring that the awesomebar popup is hidden.
+ gURLBar.select();
+ EventUtils.sendString("*");
+ EventUtils.synthesizeKey("KEY_Backspace");
+ // autoOpen behavior will show the panel, so it must be closed.
+ gURLBar.view.close();
+
+ // Trying to show a notification should display nothing.
+ let notShowing = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({ persistent });
+ this.notification = showNotification(this.notifyObj);
+ await notShowing;
+
+ // Reverting the URL should show the notification.
+ let shown = waitForNotificationPanel();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ let hidden = waitForNotificationPanelHidden();
+ this.notification.remove();
+ await hidden;
+ }
+
+ goNext();
+ },
+ },
+ // Test that persistent panels are still open after switching to another tab
+ // and back, even while editing the URL in the new tab.
+ {
+ id: "Test#6",
+ async run() {
+ let shown = waitForNotificationPanel();
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ await shown;
+
+ // Switching to a new tab should hide the notification.
+ let hidden = waitForNotificationPanelHidden();
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ await hidden;
+
+ // Start editing the URL.
+ gURLBar.select();
+ EventUtils.sendString("*");
+
+ // Switching to the old tab should show the notification again.
+ shown = waitForNotificationPanel();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ hidden = waitForNotificationPanelHidden();
+ this.notification.remove();
+ await hidden;
+
+ goNext();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js
new file mode 100644
index 0000000000..515895f35a
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js
@@ -0,0 +1,296 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_SECURITY_DELAY = 5000;
+
+/**
+ * Shows a test PopupNotification.
+ */
+function showNotification() {
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "foo",
+ "Hello, World!",
+ "default-notification-icon",
+ {
+ label: "ok",
+ accessKey: "o",
+ callback: () => {},
+ },
+ [
+ {
+ label: "cancel",
+ accessKey: "c",
+ callback: () => {},
+ },
+ ],
+ {
+ // Make test notifications persistent to ensure they are only closed
+ // explicitly by test actions and survive tab switches.
+ persistent: true,
+ }
+ );
+}
+
+add_setup(async function () {
+ // Set a longer security delay for PopupNotification actions so we can test
+ // the delay even if the test runs slowly.
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.notification_enable_delay", TEST_SECURITY_DELAY]],
+ });
+});
+
+async function ensureSecurityDelayReady() {
+ /**
+ * The security delay calculation in PopupNotification.sys.mjs is dependent on
+ * the monotonically increasing value of performance.now. This timestamp is
+ * not relative to a fixed date, but to runtime.
+ * We need to wait for the value performance.now() to be larger than the
+ * security delay in order to observe the bug. Only then does the
+ * timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown
+ * value that is unconditionally greater than lazy.buttonDelay for
+ * notification.timeShown = null = 0.
+ * See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872
+ *
+ * When running in automation as part of a larger test suite performance.now()
+ * should usually be already sufficiently high in which case this check should
+ * directly resolve.
+ */
+ await TestUtils.waitForCondition(
+ () => performance.now() > TEST_SECURITY_DELAY,
+ "Wait for performance.now() > SECURITY_DELAY",
+ 500,
+ 50
+ );
+}
+
+/**
+ * Tests that when we show a second notification while the panel is open the
+ * timeShown attribute is correctly set and the security delay is enforced
+ * properly.
+ */
+add_task(async function test_timeShownMultipleNotifications() {
+ await ensureSecurityDelayReady();
+
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "PopupNotification panel should not be open initially."
+ );
+
+ info("Open the first notification.");
+ let popupShownPromise = waitForNotificationPanel();
+ showNotification();
+ await popupShownPromise;
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be open after first show call."
+ );
+
+ is(
+ PopupNotifications._currentNotifications.length,
+ 1,
+ "There should only be one notification"
+ );
+
+ let notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(notification?.id, "foo", "There should be a notification with id foo");
+ ok(notification.timeShown, "The notification should have timeShown set");
+
+ info(
+ "Call show again with the same notification id while the PopupNotification panel is still open."
+ );
+ showNotification();
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should still open after second show call."
+ );
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(
+ PopupNotifications._currentNotifications.length,
+ 1,
+ "There should still only be one notification"
+ );
+
+ is(
+ notification?.id,
+ "foo",
+ "There should still be a notification with id foo"
+ );
+ ok(notification.timeShown, "The notification should have timeShown set");
+
+ let notificationHiddenPromise = waitForNotificationPanelHidden();
+
+ info("Trigger main action via button click during security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ ok(
+ notification,
+ "Notification should still be open because we clicked during the security delay."
+ );
+
+ // If the notification is no longer shown (test failure) skip the remaining
+ // checks.
+ if (!notification) {
+ return;
+ }
+
+ // Ensure that once the security delay has passed the notification can be
+ // closed again.
+ let fakeTimeShown = TEST_SECURITY_DELAY + 500;
+ info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
+ notification.timeShown = performance.now() - fakeTimeShown;
+
+ info("Trigger main action via button click outside security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ info("Wait for panel to be hidden.");
+ await notificationHiddenPromise;
+
+ ok(
+ !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
+ "Should not longer see the notification."
+ );
+});
+
+/**
+ * Tests that when we reshow a notification after a tab switch the timeShown
+ * attribute is correctly reset and the security delay is enforced.
+ */
+add_task(async function test_notificationReshowTabSwitch() {
+ await ensureSecurityDelayReady();
+
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "PopupNotification panel should not be open initially."
+ );
+
+ info("Open the first notification.");
+ let popupShownPromise = waitForNotificationPanel();
+ showNotification();
+ await popupShownPromise;
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be open after first show call."
+ );
+
+ let notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(notification?.id, "foo", "There should be a notification with id foo");
+ ok(notification.timeShown, "The notification should have timeShown set");
+
+ info("Trigger main action via button click during security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ ok(
+ notification,
+ "Notification should still be open because we clicked during the security delay."
+ );
+
+ // If the notification is no longer shown (test failure) skip the remaining
+ // checks.
+ if (!notification) {
+ return;
+ }
+
+ let panelHiddenPromise = waitForNotificationPanelHidden();
+ let panelShownPromise;
+
+ info("Open a new tab which hides the notification panel.");
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ info("Wait for panel to be hidden by tab switch.");
+ await panelHiddenPromise;
+ info(
+ "Keep the tab open until the security delay for the original notification show has expired."
+ );
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, TEST_SECURITY_DELAY + 500)
+ );
+
+ panelShownPromise = waitForNotificationPanel();
+ });
+ info(
+ "Wait for the panel to show again after the tab close. We're showing the original tab again."
+ );
+ await panelShownPromise;
+
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be shown after tab close."
+ );
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(
+ notification?.id,
+ "foo",
+ "There should still be a notification with id foo"
+ );
+
+ let notificationHiddenPromise = waitForNotificationPanelHidden();
+
+ info(
+ "Because we re-show the panel after tab close / switch the security delay should have reset."
+ );
+ info("Trigger main action via button click during the new security delay.");
+ triggerMainCommand(PopupNotifications.panel);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ ok(
+ notification,
+ "Notification should still be open because we clicked during the security delay."
+ );
+ // If the notification is no longer shown (test failure) skip the remaining
+ // checks.
+ if (!notification) {
+ return;
+ }
+
+ // Ensure that once the security delay has passed the notification can be
+ // closed again.
+ let fakeTimeShown = TEST_SECURITY_DELAY + 500;
+ info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
+ notification.timeShown = performance.now() - fakeTimeShown;
+
+ info("Trigger main action via button click outside security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ info("Wait for panel to be hidden.");
+ await notificationHiddenPromise;
+
+ ok(
+ !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
+ "Should not longer see the notification."
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js b/browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js
new file mode 100644
index 0000000000..31463f5345
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+function promiseElementVisible(element) {
+ // HTMLElement.offsetParent is null when the element is not visisble
+ // (or if the element has |position: fixed|). See:
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+ return TestUtils.waitForCondition(
+ () => element.offsetParent !== null,
+ "Waiting for element to be visible"
+ );
+}
+
+var gNotification;
+
+var tests = [
+ // Test that passing selection required prevents the button from clicking
+ {
+ id: "require_selection_check",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ notification.setAttribute("invalidselection", true);
+ await promiseElementVisible(notification.checkbox);
+ EventUtils.synthesizeMouseAtCenter(notification.checkbox, {});
+ ok(
+ notification.button.disabled,
+ "should be disabled when invalidselection"
+ );
+ notification.removeAttribute("invalidselection");
+ EventUtils.synthesizeMouseAtCenter(notification.checkbox, {});
+ ok(
+ !notification.button.disabled,
+ "should not be disabled when invalidselection is not present"
+ );
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_reshow_in_background.js b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js
new file mode 100644
index 0000000000..bb2494a5b5
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js
@@ -0,0 +1,72 @@
+"use strict";
+
+/**
+ * Tests that when PopupNotifications for background tabs are reshown, they
+ * don't show up in the foreground tab, but only in the background tab that
+ * they belong to.
+ */
+add_task(
+ async function test_background_notifications_dont_reshow_in_foreground() {
+ // Our initial tab will be A. Let's open two more tabs B and C, but keep
+ // A selected. Then, we'll trigger a PopupNotification in C, and then make
+ // it reshow.
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tabB = BrowserTestUtils.addTab(gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tabB.linkedBrowser);
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tabC = BrowserTestUtils.addTab(gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tabC.linkedBrowser);
+
+ let seenEvents = [];
+
+ let options = {
+ dismissed: false,
+ eventCallback(popupEvent) {
+ seenEvents.push(popupEvent);
+ },
+ };
+
+ let notification = PopupNotifications.show(
+ tabC.linkedBrowser,
+ "test-notification",
+ "",
+ "plugins-notification-icon",
+ null,
+ null,
+ options
+ );
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ await BrowserTestUtils.switchTab(gBrowser, tabB);
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ notification.reshow();
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ let panelShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tabC);
+ await panelShown;
+
+ Assert.equal(seenEvents.length, 2, "Should have seen two events.");
+ Assert.equal(
+ seenEvents[0],
+ "showing",
+ "Should have said popup was showing."
+ );
+ Assert.equal(seenEvents[1], "shown", "Should have said popup was shown.");
+
+ let panelHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ PopupNotifications.remove(notification);
+ await panelHidden;
+
+ BrowserTestUtils.removeTab(tabB);
+ BrowserTestUtils.removeTab(tabC);
+ }
+);
diff --git a/browser/base/content/test/popupNotifications/head.js b/browser/base/content/test/popupNotifications/head.js
new file mode 100644
index 0000000000..f347f8dbf2
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -0,0 +1,367 @@
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+/**
+ * Called after opening a new window or switching windows, this will wait until
+ * we are sure that an attempt to display a notification will not fail.
+ */
+async function waitForWindowReadyForPopupNotifications(win) {
+ // These are the same checks that PopupNotifications.sys.mjs makes before it
+ // allows a notification to open.
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.selectedBrowser.docShellIsActive,
+ "The browser should be active"
+ );
+ await TestUtils.waitForCondition(
+ () => Services.focus.activeWindow == win,
+ "The window should be active"
+ );
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ let browser = tab.linkedBrowser;
+
+ if (url) {
+ BrowserTestUtils.loadURIString(browser, url);
+ }
+
+ return BrowserTestUtils.browserLoaded(browser, false, url);
+}
+
+// Tests that call setup() should have a `tests` array defined for the actual
+// tests to be run.
+/* global tests */
+function setup() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/").then(
+ goNext
+ );
+ registerCleanupFunction(() => {
+ gBrowser.removeTab(gBrowser.selectedTab);
+ });
+}
+
+function goNext() {
+ executeSoon(() => executeSoon(runNextTest));
+}
+
+async function runNextTest() {
+ if (!tests.length) {
+ executeSoon(finish);
+ return;
+ }
+
+ let nextTest = tests.shift();
+ if (nextTest.onShown) {
+ let shownState = false;
+ onPopupEvent("popupshowing", function () {
+ info("[" + nextTest.id + "] popup showing");
+ });
+ onPopupEvent("popupshown", function () {
+ shownState = true;
+ info("[" + nextTest.id + "] popup shown");
+ (nextTest.onShown(this) || Promise.resolve()).then(undefined, ex =>
+ Assert.ok(false, "onShown failed: " + ex)
+ );
+ });
+ onPopupEvent(
+ "popuphidden",
+ function () {
+ info("[" + nextTest.id + "] popup hidden");
+ (nextTest.onHidden(this) || Promise.resolve()).then(
+ () => goNext(),
+ ex => Assert.ok(false, "onHidden failed: " + ex)
+ );
+ },
+ () => shownState
+ );
+ info(
+ "[" +
+ nextTest.id +
+ "] added listeners; panel is open: " +
+ PopupNotifications.isPanelOpen
+ );
+ }
+
+ info("[" + nextTest.id + "] running test");
+ await nextTest.run();
+}
+
+function showNotification(notifyObj) {
+ info("Showing notification " + notifyObj.id);
+ return PopupNotifications.show(
+ notifyObj.browser,
+ notifyObj.id,
+ notifyObj.message,
+ notifyObj.anchorID,
+ notifyObj.mainAction,
+ notifyObj.secondaryActions,
+ notifyObj.options
+ );
+}
+
+function dismissNotification(popup) {
+ info("Dismissing notification " + popup.childNodes[0].id);
+ executeSoon(() => EventUtils.synthesizeKey("KEY_Escape"));
+}
+
+function BasicNotification(testId) {
+ this.browser = gBrowser.selectedBrowser;
+ this.id = "test-notification-" + testId;
+ this.message = testId + ": Will you allow <> to perform this action?";
+ this.anchorID = null;
+ this.mainAction = {
+ label: "Main Action",
+ accessKey: "M",
+ callback: ({ source }) => {
+ this.mainActionClicked = true;
+ this.mainActionSource = source;
+ },
+ };
+ this.secondaryActions = [
+ {
+ label: "Secondary Action",
+ accessKey: "S",
+ callback: ({ source }) => {
+ this.secondaryActionClicked = true;
+ this.secondaryActionSource = source;
+ },
+ },
+ ];
+ this.options = {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ name: "http://example.com",
+ eventCallback: eventName => {
+ switch (eventName) {
+ case "dismissed":
+ this.dismissalCallbackTriggered = true;
+ break;
+ case "showing":
+ this.showingCallbackTriggered = true;
+ break;
+ case "shown":
+ this.shownCallbackTriggered = true;
+ break;
+ case "removed":
+ this.removedCallbackTriggered = true;
+ break;
+ case "swapping":
+ this.swappingCallbackTriggered = true;
+ break;
+ }
+ },
+ };
+}
+
+BasicNotification.prototype.addOptions = function (options) {
+ for (let [name, value] of Object.entries(options)) {
+ this.options[name] = value;
+ }
+};
+
+function ErrorNotification(testId) {
+ BasicNotification.call(this, testId);
+ this.mainAction.callback = () => {
+ this.mainActionClicked = true;
+ throw new Error("Oops!");
+ };
+ this.secondaryActions[0].callback = () => {
+ this.secondaryActionClicked = true;
+ throw new Error("Oops!");
+ };
+}
+
+ErrorNotification.prototype = BasicNotification.prototype;
+
+function checkPopup(popup, notifyObj) {
+ info("Checking notification " + notifyObj.id);
+
+ ok(notifyObj.showingCallbackTriggered, "showing callback was triggered");
+ ok(notifyObj.shownCallbackTriggered, "shown callback was triggered");
+
+ let notifications = popup.childNodes;
+ is(notifications.length, 1, "one notification displayed");
+ let notification = notifications[0];
+ if (!notification) {
+ return;
+ }
+
+ // PopupNotifications are not expected to show icons
+ // unless popupIconURL or popupIconClass is passed in the options object.
+ if (notifyObj.options.popupIconURL || notifyObj.options.popupIconClass) {
+ let icon = notification.querySelector(".popup-notification-icon");
+ if (notifyObj.id == "geolocation") {
+ isnot(icon.getBoundingClientRect().width, 0, "icon for geo displayed");
+ ok(
+ popup.anchorNode.classList.contains("notification-anchor-icon"),
+ "notification anchored to icon"
+ );
+ }
+ }
+
+ let description = notifyObj.message.split("<>");
+ let text = {};
+ text.start = description[0];
+ text.end = description[1];
+ is(notification.getAttribute("label"), text.start, "message matches");
+ is(
+ notification.getAttribute("name"),
+ notifyObj.options.name,
+ "message matches"
+ );
+ is(notification.getAttribute("endlabel"), text.end, "message matches");
+
+ is(notification.id, notifyObj.id + "-notification", "id matches");
+ if (notifyObj.mainAction) {
+ is(
+ notification.getAttribute("buttonlabel"),
+ notifyObj.mainAction.label,
+ "main action label matches"
+ );
+ is(
+ notification.getAttribute("buttonaccesskey"),
+ notifyObj.mainAction.accessKey,
+ "main action accesskey matches"
+ );
+ }
+ if (notifyObj.secondaryActions && notifyObj.secondaryActions.length) {
+ let secondaryAction = notifyObj.secondaryActions[0];
+ is(
+ notification.getAttribute("secondarybuttonlabel"),
+ secondaryAction.label,
+ "secondary action label matches"
+ );
+ is(
+ notification.getAttribute("secondarybuttonaccesskey"),
+ secondaryAction.accessKey,
+ "secondary action accesskey matches"
+ );
+ }
+ // Additional secondary actions appear as menu items.
+ let actualExtraSecondaryActions = Array.prototype.filter.call(
+ notification.menupopup.childNodes,
+ child => child.nodeName == "menuitem"
+ );
+ let extraSecondaryActions = notifyObj.secondaryActions
+ ? notifyObj.secondaryActions.slice(1)
+ : [];
+ is(
+ actualExtraSecondaryActions.length,
+ extraSecondaryActions.length,
+ "number of extra secondary actions matches"
+ );
+ extraSecondaryActions.forEach(function (a, i) {
+ is(
+ actualExtraSecondaryActions[i].getAttribute("label"),
+ a.label,
+ "label for extra secondary action " + i + " matches"
+ );
+ is(
+ actualExtraSecondaryActions[i].getAttribute("accesskey"),
+ a.accessKey,
+ "accessKey for extra secondary action " + i + " matches"
+ );
+ });
+}
+
+XPCOMUtils.defineLazyGetter(this, "gActiveListeners", () => {
+ let listeners = new Map();
+ registerCleanupFunction(() => {
+ for (let [listener, eventName] of listeners) {
+ PopupNotifications.panel.removeEventListener(eventName, listener);
+ }
+ });
+ return listeners;
+});
+
+function onPopupEvent(eventName, callback, condition) {
+ let listener = event => {
+ if (
+ event.target != PopupNotifications.panel ||
+ (condition && !condition())
+ ) {
+ return;
+ }
+ PopupNotifications.panel.removeEventListener(eventName, listener);
+ gActiveListeners.delete(listener);
+ executeSoon(() => callback.call(PopupNotifications.panel));
+ };
+ gActiveListeners.set(listener, eventName);
+ PopupNotifications.panel.addEventListener(eventName, listener);
+}
+
+function waitForNotificationPanel() {
+ return new Promise(resolve => {
+ onPopupEvent("popupshown", function () {
+ resolve(this);
+ });
+ });
+}
+
+function waitForNotificationPanelHidden() {
+ return new Promise(resolve => {
+ onPopupEvent("popuphidden", function () {
+ resolve(this);
+ });
+ });
+}
+
+function triggerMainCommand(popup) {
+ let notifications = popup.childNodes;
+ ok(!!notifications.length, "at least one notification displayed");
+ let notification = notifications[0];
+ info("Triggering main command for notification " + notification.id);
+ EventUtils.synthesizeMouseAtCenter(notification.button, {});
+}
+
+function triggerSecondaryCommand(popup, index) {
+ let notifications = popup.childNodes;
+ ok(!!notifications.length, "at least one notification displayed");
+ let notification = notifications[0];
+ info("Triggering secondary command for notification " + notification.id);
+
+ if (index == 0) {
+ EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
+ return;
+ }
+
+ // Extra secondary actions appear in a menu.
+ notification.secondaryButton.nextElementSibling.focus();
+
+ popup.addEventListener(
+ "popupshown",
+ function () {
+ info("Command popup open for notification " + notification.id);
+ // Press down until the desired command is selected. Decrease index by one
+ // since the secondary action was handled above.
+ for (let i = 0; i <= index - 1; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ // Activate
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ { once: true }
+ );
+
+ // One down event to open the popup
+ info(
+ "Open the popup to trigger secondary command for notification " +
+ notification.id
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", {
+ altKey: !navigator.platform.includes("Mac"),
+ });
+}
diff --git a/browser/base/content/test/popups/browser.ini b/browser/base/content/test/popups/browser.ini
new file mode 100644
index 0000000000..710ea28633
--- /dev/null
+++ b/browser/base/content/test/popups/browser.ini
@@ -0,0 +1,69 @@
+[DEFAULT]
+support-files =
+ head.js
+ popup_blocker_a.html # used as dummy file
+prefs =
+ # TODO: Port browser_popup_{move,move_instant,resize}.js to use move/resizeTo
+ # instead of individual properties.
+ dom.window_position_size_properties_replaceable.enabled=false
+[browser_popupUI.js]
+[browser_popup_blocker.js]
+support-files =
+ popup_blocker.html
+ popup_blocker_a.html
+ popup_blocker_b.html
+ popup_blocker_10_popups.html
+skip-if = (os == 'linux') || debug # Frequent bug 1081925 and bug 1125520 failures
+[browser_popup_blocker_frames.js]
+https_first_disabled = true
+support-files =
+ popup_blocker.html
+ popup_blocker_a.html
+ popup_blocker_b.html
+[browser_popup_blocker_identity_block.js]
+https_first_disabled = true
+support-files =
+ popup_blocker2.html
+ popup_blocker_a.html
+[browser_popup_blocker_iframes.js]
+https_first_disabled = true
+support-files =
+ popup_blocker.html
+ popup_blocker_frame.html
+ popup_blocker_a.html
+ popup_blocker_b.html
+skip-if =
+ debug # This test triggers Bug 1578794 due to opening many popups.
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_popup_close_main_window.js]
+[browser_popup_frames.js]
+https_first_disabled = true
+support-files =
+ popup_blocker.html
+ popup_blocker_a.html
+ popup_blocker_b.html
+[browser_popup_inner_outer_size.js]
+[browser_popup_linux_move.js]
+run-if = os == 'linux' && !headless # subset of other move tests
+[browser_popup_linux_resize.js]
+run-if = os == 'linux' && !headless # subset of other resize tests
+[browser_popup_move.js]
+skip-if = os == 'linux' && !headless # Wayland doesn't like moving windows, X11/XWayland unreliable current positions
+[browser_popup_move_instant.js]
+skip-if = os == 'linux' && !headless # Wayland doesn't like moving windows, X11/XWayland unreliable current positions
+[browser_popup_new_window_resize.js]
+[browser_popup_new_window_size.js]
+support-files =
+ popup_size.html
+[browser_popup_resize.js]
+skip-if = os == 'linux' && !headless # outdated current sizes
+[browser_popup_resize_instant.js]
+skip-if = os == 'linux' && !headless # outdated current sizes
+[browser_popup_resize_repeat.js]
+skip-if = os == 'linux' && !headless # outdated current sizes
+[browser_popup_resize_repeat_instant.js]
+skip-if = os == 'linux' && !headless # outdated current sizes
+[browser_popup_resize_revert.js]
+skip-if = os == 'linux' && !headless # outdated current sizes
+[browser_popup_resize_revert_instant.js]
+skip-if = os == 'linux' && !headless # outdated current sizes
diff --git a/browser/base/content/test/popups/browser_popupUI.js b/browser/base/content/test/popups/browser_popupUI.js
new file mode 100644
index 0000000000..27423b5868
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popupUI.js
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function toolbar_ui_visibility() {
+ SpecialPowers.pushPrefEnv({ set: [["dom.disable_open_during_load", false]] });
+
+ let popupOpened = BrowserTestUtils.waitForNewWindow({ url: "about:blank" });
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<html><script>popup=open('about:blank','','width=300,height=200')</script>"
+ );
+ const win = await popupOpened;
+ const doc = win.document;
+
+ ok(win.gURLBar, "location bar exists in the popup");
+ isnot(win.gURLBar.clientWidth, 0, "location bar is visible in the popup");
+ ok(win.gURLBar.readOnly, "location bar is read-only in the popup");
+ isnot(
+ doc.getElementById("Browser:OpenLocation").getAttribute("disabled"),
+ "true",
+ "'open location' command is not disabled in the popup"
+ );
+
+ EventUtils.synthesizeKey("t", { accelKey: true }, win);
+ is(
+ win.gBrowser.browsers.length,
+ 1,
+ "Accel+T doesn't open a new tab in the popup"
+ );
+ is(
+ gBrowser.browsers.length,
+ 3,
+ "Accel+T opened a new tab in the parent window"
+ );
+ gBrowser.removeCurrentTab();
+
+ EventUtils.synthesizeKey("w", { accelKey: true }, win);
+ ok(win.closed, "Accel+W closes the popup");
+
+ if (!win.closed) {
+ win.close();
+ }
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function titlebar_buttons_visibility() {
+ if (!navigator.platform.startsWith("Win")) {
+ ok(true, "Testing only on Windows");
+ return;
+ }
+
+ const BUTTONS_MAY_VISIBLE = true;
+ const BUTTONS_NEVER_VISIBLE = false;
+
+ // Always open a new window.
+ // With default behavior, it opens a new tab, that doesn't affect button
+ // visibility at all.
+ Services.prefs.setIntPref("browser.link.open_newwindow", 2);
+
+ const drawInTitlebarValues = [
+ [1, BUTTONS_MAY_VISIBLE],
+ [0, BUTTONS_NEVER_VISIBLE],
+ ];
+ const windowFeaturesValues = [
+ // Opens a popup
+ ["width=300,height=100", BUTTONS_NEVER_VISIBLE],
+ ["toolbar", BUTTONS_NEVER_VISIBLE],
+ ["menubar", BUTTONS_NEVER_VISIBLE],
+ ["menubar,toolbar", BUTTONS_NEVER_VISIBLE],
+
+ // Opens a new window
+ ["", BUTTONS_MAY_VISIBLE],
+ ];
+ const menuBarShownValues = [true, false];
+
+ for (const [drawInTitlebar, drawInTitlebarButtons] of drawInTitlebarValues) {
+ Services.prefs.setIntPref("browser.tabs.inTitlebar", drawInTitlebar);
+
+ for (const [
+ windowFeatures,
+ windowFeaturesButtons,
+ ] of windowFeaturesValues) {
+ for (const menuBarShown of menuBarShownValues) {
+ CustomizableUI.setToolbarVisibility("toolbar-menubar", menuBarShown);
+
+ const popupPromise = BrowserTestUtils.waitForNewWindow("about:blank");
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `data:text/html;charset=UTF-8,<html><script>window.open("about:blank","","${windowFeatures}")</script>`
+ );
+ const popupWin = await popupPromise;
+
+ const menubar = popupWin.document.querySelector("#toolbar-menubar");
+ const menubarIsShown =
+ menubar.getAttribute("autohide") != "true" ||
+ menubar.getAttribute("inactive") != "true";
+ const buttonsInMenubar = menubar.querySelector(
+ ".titlebar-buttonbox-container"
+ );
+ const buttonsInMenubarShown =
+ menubarIsShown &&
+ popupWin.getComputedStyle(buttonsInMenubar).display != "none";
+
+ const buttonsInTabbar = popupWin.document.querySelector(
+ "#TabsToolbar .titlebar-buttonbox-container"
+ );
+ const buttonsInTabbarShown =
+ popupWin.getComputedStyle(buttonsInTabbar).display != "none";
+
+ const params = `drawInTitlebar=${drawInTitlebar}, windowFeatures=${windowFeatures}, menuBarShown=${menuBarShown}`;
+ if (
+ drawInTitlebarButtons == BUTTONS_MAY_VISIBLE &&
+ windowFeaturesButtons == BUTTONS_MAY_VISIBLE
+ ) {
+ ok(
+ buttonsInMenubarShown || buttonsInTabbarShown,
+ `Titlebar buttons should be visible: ${params}`
+ );
+ } else {
+ ok(
+ !buttonsInMenubarShown,
+ `Titlebar buttons should not be visible: ${params}`
+ );
+ ok(
+ !buttonsInTabbarShown,
+ `Titlebar buttons should not be visible: ${params}`
+ );
+ }
+
+ const closedPopupPromise = BrowserTestUtils.windowClosed(popupWin);
+ popupWin.close();
+ await closedPopupPromise;
+ gBrowser.removeCurrentTab();
+ }
+ }
+ }
+
+ CustomizableUI.setToolbarVisibility("toolbar-menubar", false);
+ Services.prefs.clearUserPref("browser.tabs.inTitlebar");
+ Services.prefs.clearUserPref("browser.link.open_newwindow");
+});
+
+// Test only `visibility` rule here, to verify bug 1636229 fix.
+// Other styles and ancestors can be different for each OS.
+function isVisible(element) {
+ const style = element.ownerGlobal.getComputedStyle(element);
+ return style.visibility == "visible";
+}
+
+async function testTabBarVisibility() {
+ SpecialPowers.pushPrefEnv({ set: [["dom.disable_open_during_load", false]] });
+
+ const popupOpened = BrowserTestUtils.waitForNewWindow({ url: "about:blank" });
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<html><script>popup=open('about:blank','','width=300,height=200')</script>"
+ );
+ const win = await popupOpened;
+ const doc = win.document;
+
+ ok(
+ !isVisible(doc.getElementById("TabsToolbar")),
+ "tabbar should be hidden for popup"
+ );
+
+ const closedPopupPromise = BrowserTestUtils.windowClosed(win);
+ win.close();
+ await closedPopupPromise;
+
+ gBrowser.removeCurrentTab();
+}
+
+add_task(async function tabbar_visibility() {
+ await testTabBarVisibility();
+});
+
+add_task(async function tabbar_visibility_with_theme() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {},
+ },
+ });
+
+ await extension.startup();
+
+ await testTabBarVisibility();
+
+ await extension.unload();
+});
diff --git a/browser/base/content/test/popups/browser_popup_blocker.js b/browser/base/content/test/popups/browser_popup_blocker.js
new file mode 100644
index 0000000000..bfda12331e
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker.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/. */
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+function clearAllPermissionsByPrefix(aPrefix) {
+ for (let perm of Services.perms.all) {
+ if (perm.type.startsWith(aPrefix)) {
+ Services.perms.removePermission(perm);
+ }
+ }
+}
+
+add_setup(async function () {
+ // Enable the popup blocker.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+ });
+});
+
+// Tests that we show a special message when popup blocking exceeds
+// a certain maximum of popups per page.
+add_task(async function test_maximum_reported_blocks() {
+ Services.prefs.setIntPref("privacy.popups.maxReported", 5);
+
+ // Open the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ baseURL + "popup_blocker_10_popups.html"
+ );
+
+ // Wait for the popup-blocked notification.
+ let notification = await TestUtils.waitForCondition(() =>
+ gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked")
+ );
+
+ // Slightly hacky way to ensure we show the correct message in this case.
+ ok(
+ notification.messageText.textContent.includes("more than"),
+ "Notification label has 'more than'"
+ );
+ ok(
+ notification.messageText.textContent.includes("5"),
+ "Notification label shows the maximum number of popups"
+ );
+
+ gBrowser.removeTab(tab);
+
+ Services.prefs.clearUserPref("privacy.popups.maxReported");
+});
+
+add_task(async function test_opening_blocked_popups() {
+ // Open the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ baseURL + "popup_blocker.html"
+ );
+
+ await testPopupBlockingToolbar(tab);
+});
+
+add_task(async function test_opening_blocked_popups_privateWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ // Open the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ baseURL + "popup_blocker.html"
+ );
+ await testPopupBlockingToolbar(tab);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+async function testPopupBlockingToolbar(tab) {
+ let win = tab.ownerGlobal;
+ // Wait for the popup-blocked notification.
+ let notification;
+ await TestUtils.waitForCondition(
+ () =>
+ (notification = win.gBrowser
+ .getNotificationBox()
+ .getNotificationWithValue("popup-blocked"))
+ );
+
+ // Show the menu.
+ let popupShown = BrowserTestUtils.waitForEvent(win, "popupshown");
+ let popupFilled = waitForBlockedPopups(2, {
+ doc: win.document,
+ });
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.querySelector("button"),
+ {},
+ win
+ );
+ let popup_event = await popupShown;
+ let menu = popup_event.target;
+ is(menu.id, "blockedPopupOptions", "Blocked popup menu shown");
+
+ await popupFilled;
+
+ // Pressing "allow" should open all blocked popups.
+ let popupTabs = [];
+ function onTabOpen(event) {
+ popupTabs.push(event.target);
+ }
+ win.gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
+
+ // Press the button.
+ let allow = win.document.getElementById("blockedPopupAllowSite");
+ allow.doCommand();
+ await TestUtils.waitForCondition(
+ () =>
+ popupTabs.length == 2 &&
+ popupTabs.every(
+ aTab => aTab.linkedBrowser.currentURI.spec != "about:blank"
+ )
+ );
+
+ win.gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen);
+
+ ok(
+ popupTabs[0].linkedBrowser.currentURI.spec.endsWith("popup_blocker_a.html"),
+ "Popup a"
+ );
+ ok(
+ popupTabs[1].linkedBrowser.currentURI.spec.endsWith("popup_blocker_b.html"),
+ "Popup b"
+ );
+
+ let popupPerms = Services.perms.getAllByTypeSince("popup", 0);
+ is(popupPerms.length, 1, "One popup permission added");
+ let popupPerm = popupPerms[0];
+ let expectedExpireType = PrivateBrowsingUtils.isWindowPrivate(win)
+ ? Services.perms.EXPIRE_SESSION
+ : Services.perms.EXPIRE_NEVER;
+ is(
+ popupPerm.expireType,
+ expectedExpireType,
+ "Check expireType is appropriate for the window"
+ );
+
+ // Clean up.
+ win.gBrowser.removeTab(tab);
+ for (let popup of popupTabs) {
+ win.gBrowser.removeTab(popup);
+ }
+ clearAllPermissionsByPrefix("popup");
+ // Ensure the menu closes.
+ menu.hidePopup();
+}
diff --git a/browser/base/content/test/popups/browser_popup_blocker_frames.js b/browser/base/content/test/popups/browser_popup_blocker_frames.js
new file mode 100644
index 0000000000..163fa4a0bb
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker_frames.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/. */
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+async function test_opening_blocked_popups(testURL) {
+ // Enable the popup blocker.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+ });
+
+ // Open the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [baseURL + "popup_blocker.html"],
+ uri => {
+ let iframe = content.document.createElement("iframe");
+ iframe.id = "popupframe";
+ iframe.src = uri;
+ content.document.body.appendChild(iframe);
+ }
+ );
+
+ // Wait for the popup-blocked notification.
+ await TestUtils.waitForCondition(
+ () =>
+ gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked"),
+ "Waiting for the popup-blocked notification."
+ );
+
+ let popupTabs = [];
+ function onTabOpen(event) {
+ popupTabs.push(event.target);
+ }
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
+
+ await SpecialPowers.pushPermissions([
+ { type: "popup", allow: true, context: testURL },
+ ]);
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [baseURL + "popup_blocker.html"],
+ uri => {
+ content.document.getElementById("popupframe").remove();
+ let iframe = content.document.createElement("iframe");
+ iframe.id = "popupframe";
+ iframe.src = uri;
+ content.document.body.appendChild(iframe);
+ }
+ );
+
+ await TestUtils.waitForCondition(
+ () =>
+ popupTabs.length == 2 &&
+ popupTabs.every(
+ aTab => aTab.linkedBrowser.currentURI.spec != "about:blank"
+ ),
+ "Waiting for two tabs to be opened."
+ );
+
+ ok(
+ popupTabs[0].linkedBrowser.currentURI.spec.endsWith("popup_blocker_a.html"),
+ "Popup a"
+ );
+ ok(
+ popupTabs[1].linkedBrowser.currentURI.spec.endsWith("popup_blocker_b.html"),
+ "Popup b"
+ );
+
+ await SpecialPowers.popPermissions();
+
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("popupframe").remove();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+ for (let popup of popupTabs) {
+ gBrowser.removeTab(popup);
+ }
+}
+
+add_task(async function () {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await test_opening_blocked_popups("http://example.com/");
+});
+
+add_task(async function () {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await test_opening_blocked_popups("http://w3c-test.org/");
+});
diff --git a/browser/base/content/test/popups/browser_popup_blocker_identity_block.js b/browser/base/content/test/popups/browser_popup_blocker_identity_block.js
new file mode 100644
index 0000000000..c277be2c40
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker_identity_block.js
@@ -0,0 +1,242 @@
+"use strict";
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const URL = baseURL + "popup_blocker2.html";
+const URI = Services.io.newURI(URL);
+const PRINCIPAL = Services.scriptSecurityManager.createContentPrincipal(
+ URI,
+ {}
+);
+
+function openPermissionPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gPermissionPanel._permissionPopup
+ );
+ gPermissionPanel._identityPermissionBox.click();
+ return promise;
+}
+
+function closePermissionPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gPermissionPanel._permissionPopup,
+ "popuphidden"
+ );
+ gPermissionPanel._permissionPopup.hidePopup();
+ return promise;
+}
+
+add_task(async function enable_popup_blocker() {
+ // Enable popup blocker.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_click_delay", 0]],
+ });
+});
+
+add_task(async function check_blocked_popup_indicator() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+
+ // Blocked popup indicator should not exist in the identity popup when there are no blocked popups.
+ await openPermissionPopup();
+ Assert.equal(document.getElementById("blocked-popup-indicator-item"), null);
+ await closePermissionPopup();
+
+ // Blocked popup notification icon should be hidden in the identity block when no popups are blocked.
+ let icon = gPermissionPanel._identityPermissionBox.querySelector(
+ ".blocked-permission-icon[data-permission-id='popup']"
+ );
+ Assert.equal(icon.hasAttribute("showing"), false);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ let open = content.document.getElementById("pop");
+ open.click();
+ });
+
+ // Wait for popup block.
+ await TestUtils.waitForCondition(() =>
+ gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked")
+ );
+
+ // Check if blocked popup indicator text is visible in the identity popup. It should be visible.
+ document.getElementById("identity-permission-box").click();
+ await openPermissionPopup();
+ await TestUtils.waitForCondition(
+ () => document.getElementById("blocked-popup-indicator-item") !== null
+ );
+
+ // Check that the default state is correctly set to "Block".
+ let menulist = document.getElementById("permission-popup-menulist");
+ Assert.equal(menulist.value, "0");
+ Assert.equal(menulist.label, "Block");
+
+ await closePermissionPopup();
+
+ // Check if blocked popup icon is visible in the identity block.
+ Assert.equal(icon.getAttribute("showing"), "true");
+
+ gBrowser.removeTab(tab);
+});
+
+// Check if clicking on "Show blocked popups" shows blocked popups.
+add_task(async function check_popup_showing() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ let open = content.document.getElementById("pop");
+ open.click();
+ });
+
+ // Wait for popup block.
+ await TestUtils.waitForCondition(() =>
+ gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked")
+ );
+
+ // Store the popup that opens in this array.
+ let popup;
+ function onTabOpen(event) {
+ popup = event.target;
+ }
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
+
+ // Open identity popup and click on "Show blocked popups".
+ await openPermissionPopup();
+ let e = document.getElementById("blocked-popup-indicator-item");
+ let text = e.getElementsByTagName("label")[0];
+ text.click();
+
+ await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ await TestUtils.waitForCondition(
+ () => popup.linkedBrowser.currentURI.spec != "about:blank"
+ );
+
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen);
+
+ ok(
+ popup.linkedBrowser.currentURI.spec.endsWith("popup_blocker_a.html"),
+ "Popup a"
+ );
+
+ gBrowser.removeTab(popup);
+ gBrowser.removeTab(tab);
+});
+
+// Test if changing menulist values of blocked popup indicator changes permission state and popup behavior.
+add_task(async function check_permission_state_change() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+
+ // Initially the permission state is BLOCK for popups (set by the prefs).
+ let state = SitePermissions.getForPrincipal(
+ PRINCIPAL,
+ "popup",
+ gBrowser
+ ).state;
+ Assert.equal(state, SitePermissions.BLOCK);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ let open = content.document.getElementById("pop");
+ open.click();
+ });
+
+ // Wait for popup block.
+ await TestUtils.waitForCondition(() =>
+ gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked")
+ );
+
+ // Open identity popup and change permission state to allow.
+ await openPermissionPopup();
+ let menulist = document.getElementById("permission-popup-menulist");
+ menulist.menupopup.openPopup(); // Open the allow/block menu
+ let menuitem = menulist.getElementsByTagName("menuitem")[0];
+ menuitem.click();
+ await closePermissionPopup();
+
+ state = SitePermissions.getForPrincipal(PRINCIPAL, "popup", gBrowser).state;
+ Assert.equal(state, SitePermissions.ALLOW);
+
+ // Store the popup that opens in this array.
+ let popup;
+ function onTabOpen(event) {
+ popup = event.target;
+ }
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
+
+ // Check if a popup opens.
+ await Promise.all([
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let open = content.document.getElementById("pop");
+ open.click();
+ }),
+ BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen"),
+ ]);
+ await TestUtils.waitForCondition(
+ () => popup.linkedBrowser.currentURI.spec != "about:blank"
+ );
+
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen);
+
+ ok(
+ popup.linkedBrowser.currentURI.spec.endsWith("popup_blocker_a.html"),
+ "Popup a"
+ );
+
+ gBrowser.removeTab(popup);
+
+ // Open identity popup and change permission state to block.
+ await openPermissionPopup();
+ menulist = document.getElementById("permission-popup-menulist");
+ menulist.menupopup.openPopup(); // Open the allow/block menu
+ menuitem = menulist.getElementsByTagName("menuitem")[1];
+ menuitem.click();
+ await closePermissionPopup();
+
+ // Clicking on the "Block" menuitem should remove the permission object(same behavior as UNKNOWN state).
+ // We have already confirmed that popups are blocked when the permission state is BLOCK.
+ state = SitePermissions.getForPrincipal(PRINCIPAL, "popup", gBrowser).state;
+ Assert.equal(state, SitePermissions.BLOCK);
+
+ gBrowser.removeTab(tab);
+});
+
+// Explicitly set the permission to the otherwise default state and check that
+// the label still displays correctly.
+add_task(async function check_explicit_default_permission() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+
+ // DENY only works if triggered through Services.perms (it's very edge-casey),
+ // since SitePermissions.sys.mjs considers setting default permissions to be removal.
+ PermissionTestUtils.add(URI, "popup", Ci.nsIPermissionManager.DENY_ACTION);
+
+ await openPermissionPopup();
+ let menulist = document.getElementById("permission-popup-menulist");
+ Assert.equal(menulist.value, "0");
+ Assert.equal(menulist.label, "Block");
+ await closePermissionPopup();
+
+ PermissionTestUtils.add(URI, "popup", Services.perms.ALLOW_ACTION);
+
+ await openPermissionPopup();
+ menulist = document.getElementById("permission-popup-menulist");
+ Assert.equal(menulist.value, "1");
+ Assert.equal(menulist.label, "Allow");
+ await closePermissionPopup();
+
+ PermissionTestUtils.remove(URI, "popup");
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/popups/browser_popup_blocker_iframes.js b/browser/base/content/test/popups/browser_popup_blocker_iframes.js
new file mode 100644
index 0000000000..aa93a7acac
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker_iframes.js
@@ -0,0 +1,186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+requestLongerTimeout(2);
+
+const testURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org"
+);
+
+const examplecomURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+const w3cURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://w3c-test.org"
+);
+
+const examplenetURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.net"
+);
+
+const prefixexamplecomURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://prefixexample.com"
+);
+
+class TestCleanup {
+ constructor() {
+ this.tabs = [];
+ }
+
+ count() {
+ return this.tabs.length;
+ }
+
+ static setup() {
+ let cleaner = new TestCleanup();
+ this.onTabOpen = event => {
+ cleaner.tabs.push(event.target);
+ };
+ gBrowser.tabContainer.addEventListener("TabOpen", this.onTabOpen);
+ return cleaner;
+ }
+
+ clean() {
+ gBrowser.tabContainer.removeEventListener("TabOpen", this.onTabOpen);
+ for (let tab of this.tabs) {
+ gBrowser.removeTab(tab);
+ }
+ }
+}
+
+async function runTest(count, urls, permissions, delayedAllow) {
+ let cleaner = TestCleanup.setup();
+
+ // Enable the popup blocker.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+ });
+
+ await SpecialPowers.pushPermissions(permissions);
+
+ // Open the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ let contexts = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [...urls, !!delayedAllow],
+ async (url1, url2, url3, url4, delay) => {
+ let iframe1 = content.document.createElement("iframe");
+ let iframe2 = content.document.createElement("iframe");
+ iframe1.id = "iframe1";
+ iframe2.id = "iframe2";
+ iframe1.src = new URL(
+ `popup_blocker_frame.html?delayed=${delay}&base=${url3}`,
+ url1
+ );
+ iframe2.src = new URL(
+ `popup_blocker_frame.html?delayed=${delay}&base=${url4}`,
+ url2
+ );
+
+ let promises = [
+ new Promise(resolve => (iframe1.onload = resolve)),
+ new Promise(resolve => (iframe2.onload = resolve)),
+ ];
+
+ content.document.body.appendChild(iframe1);
+ content.document.body.appendChild(iframe2);
+
+ await Promise.all(promises);
+ return [iframe1.browsingContext, iframe2.browsingContext];
+ }
+ );
+
+ if (delayedAllow) {
+ await delayedAllow();
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ contexts,
+ async function (bc1, bc2) {
+ bc1.window.postMessage("allow", "*");
+ bc2.window.postMessage("allow", "*");
+ }
+ );
+ }
+
+ await TestUtils.waitForCondition(
+ () => cleaner.count() == count,
+ `waiting for ${count} tabs, got ${cleaner.count()}`
+ );
+
+ ok(cleaner.count() == count, `should have ${count} tabs`);
+
+ await SpecialPowers.popPermissions();
+ cleaner.clean();
+}
+
+add_task(async function () {
+ let permission = {
+ type: "popup",
+ allow: true,
+ context: "",
+ };
+
+ let expected = [];
+
+ let tests = [
+ [examplecomURL, w3cURL, prefixexamplecomURL, examplenetURL],
+ [examplecomURL, examplecomURL, prefixexamplecomURL, examplenetURL],
+ [examplecomURL, examplecomURL, prefixexamplecomURL, prefixexamplecomURL],
+ [examplecomURL, w3cURL, prefixexamplecomURL, prefixexamplecomURL],
+ ];
+
+ permission.context = testURL;
+ expected = [5, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [permission]);
+ }
+
+ permission.context = examplecomURL;
+ expected = [3, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [permission]);
+ }
+
+ permission.context = prefixexamplecomURL;
+ expected = [3, 3, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [permission]);
+ }
+
+ async function allowPopup() {
+ await SpecialPowers.pushPermissions([permission]);
+ }
+
+ permission.context = testURL;
+ expected = [5, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [], allowPopup);
+ }
+
+ permission.context = examplecomURL;
+ expected = [3, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [], allowPopup);
+ }
+
+ permission.context = prefixexamplecomURL;
+ expected = [3, 3, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [], allowPopup);
+ }
+});
diff --git a/browser/base/content/test/popups/browser_popup_close_main_window.js b/browser/base/content/test/popups/browser_popup_close_main_window.js
new file mode 100644
index 0000000000..148e937bca
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_close_main_window.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function muffleMainWindowType() {
+ let oldWinType = document.documentElement.getAttribute("windowtype");
+ // Check if we've already done this to allow calling multiple times:
+ if (oldWinType != "navigator:testrunner") {
+ // Make the main test window not count as a browser window any longer
+ document.documentElement.setAttribute("windowtype", "navigator:testrunner");
+
+ registerCleanupFunction(() => {
+ document.documentElement.setAttribute("windowtype", oldWinType);
+ });
+ }
+}
+
+/**
+ * Check that if we close the 1 remaining window, we treat it as quitting on
+ * non-mac.
+ *
+ * Sets the window type for the main browser test window to something else to
+ * avoid having to actually close the main browser window.
+ */
+add_task(async function closing_last_window_equals_quitting() {
+ if (navigator.platform.startsWith("Mac")) {
+ ok(true, "Not testing on mac");
+ return;
+ }
+ muffleMainWindowType();
+
+ let observed = 0;
+ function obs() {
+ observed++;
+ }
+ Services.obs.addObserver(obs, "browser-lastwindow-close-requested");
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let closedPromise = BrowserTestUtils.windowClosed(newWin);
+ newWin.BrowserTryToCloseWindow();
+ await closedPromise;
+ is(observed, 1, "Got a notification for closing the normal window.");
+ Services.obs.removeObserver(obs, "browser-lastwindow-close-requested");
+});
+
+/**
+ * Check that if we close the 1 remaining window and also have a popup open,
+ * we don't treat it as quitting.
+ *
+ * Sets the window type for the main browser test window to something else to
+ * avoid having to actually close the main browser window.
+ */
+add_task(async function closing_last_window_equals_quitting() {
+ if (navigator.platform.startsWith("Mac")) {
+ ok(true, "Not testing on mac");
+ return;
+ }
+ muffleMainWindowType();
+ let observed = 0;
+ function obs() {
+ observed++;
+ }
+ Services.obs.addObserver(obs, "browser-lastwindow-close-requested");
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let popupPromise = BrowserTestUtils.waitForNewWindow("https://example.com/");
+ SpecialPowers.spawn(newWin.gBrowser.selectedBrowser, [], function () {
+ content.open("https://example.com/", "_blank", "height=500");
+ });
+ let popupWin = await popupPromise;
+ let closedPromise = BrowserTestUtils.windowClosed(newWin);
+ newWin.BrowserTryToCloseWindow();
+ await closedPromise;
+ is(observed, 0, "Got no notification for closing the normal window.");
+
+ closedPromise = BrowserTestUtils.windowClosed(popupWin);
+ popupWin.BrowserTryToCloseWindow();
+ await closedPromise;
+ is(
+ observed,
+ 0,
+ "Got no notification now that we're closing the last window, as it's a popup."
+ );
+ Services.obs.removeObserver(obs, "browser-lastwindow-close-requested");
+});
diff --git a/browser/base/content/test/popups/browser_popup_frames.js b/browser/base/content/test/popups/browser_popup_frames.js
new file mode 100644
index 0000000000..838eb5c045
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_frames.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/. */
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+async function test_opening_blocked_popups(testURL) {
+ // Enable the popup blocker.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+ });
+
+ // Open the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ let popupframeBC = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [baseURL + "popup_blocker.html"],
+ uri => {
+ let iframe = content.document.createElement("iframe");
+ iframe.id = "popupframe";
+ iframe.src = uri;
+ content.document.body.appendChild(iframe);
+ return iframe.browsingContext;
+ }
+ );
+
+ // Wait for the popup-blocked notification.
+ let notification;
+ await TestUtils.waitForCondition(
+ () =>
+ (notification = gBrowser
+ .getNotificationBox()
+ .getNotificationWithValue("popup-blocked")),
+ "Waiting for the popup-blocked notification."
+ );
+
+ ok(notification, "Should have notification.");
+
+ let pageHideHappened = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pagehide",
+ true
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [baseURL], async function (uri) {
+ let iframe = content.document.createElement("iframe");
+ content.document.body.appendChild(iframe);
+ iframe.src = uri;
+ });
+
+ await pageHideHappened;
+ notification = gBrowser
+ .getNotificationBox()
+ .getNotificationWithValue("popup-blocked");
+ ok(notification, "Should still have notification");
+
+ pageHideHappened = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pagehide",
+ true
+ );
+ // Now navigate the subframe.
+ await SpecialPowers.spawn(popupframeBC, [], async function () {
+ content.document.location.href = "about:blank";
+ });
+ await pageHideHappened;
+ await TestUtils.waitForCondition(
+ () =>
+ !gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked"),
+ "Notification should go away"
+ );
+ ok(
+ !gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked"),
+ "Should no longer have notification"
+ );
+
+ // Remove the frame and add another one:
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [baseURL + "popup_blocker.html"],
+ uri => {
+ content.document.getElementById("popupframe").remove();
+ let iframe = content.document.createElement("iframe");
+ iframe.id = "popupframe";
+ iframe.src = uri;
+ content.document.body.appendChild(iframe);
+ }
+ );
+
+ // Wait for the popup-blocked notification.
+ await TestUtils.waitForCondition(
+ () =>
+ (notification = gBrowser
+ .getNotificationBox()
+ .getNotificationWithValue("popup-blocked"))
+ );
+
+ ok(notification, "Should have notification.");
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("popupframe").remove();
+ });
+
+ await TestUtils.waitForCondition(
+ () =>
+ !gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked")
+ );
+ ok(
+ !gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked"),
+ "Should no longer have notification"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function () {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await test_opening_blocked_popups("http://example.com/");
+});
+
+add_task(async function () {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await test_opening_blocked_popups("http://w3c-test.org/");
+});
diff --git a/browser/base/content/test/popups/browser_popup_inner_outer_size.js b/browser/base/content/test/popups/browser_popup_inner_outer_size.js
new file mode 100644
index 0000000000..7e2e0e43fe
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_inner_outer_size.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function checkForDeltaMismatch(aMsg) {
+ let getDelta = () => {
+ return {
+ width: this.content.outerWidth - this.content.innerWidth,
+ height: this.content.outerHeight - this.content.innerHeight,
+ };
+ };
+
+ let initialDelta = getDelta();
+ let latestDelta = initialDelta;
+
+ this.content.testerPromise = new Promise(resolve => {
+ // Called from stopCheck
+ this.content.resolveFunc = resolve;
+ info(`[${aMsg}] Starting interval tester.`);
+ this.content.intervalID = this.content.setInterval(() => {
+ let currentDelta = getDelta();
+ if (
+ latestDelta.width != currentDelta.width ||
+ latestDelta.height != currentDelta.height
+ ) {
+ latestDelta = currentDelta;
+
+ let { innerWidth: iW, outerWidth: oW } = this.content;
+ let { innerHeight: iH, outerHeight: oH } = this.content;
+ info(`[${aMsg}] Delta changed. (inner ${iW}x${iH}, outer ${oW}x${oH})`);
+
+ let { width: w, height: h } = currentDelta;
+ is(w, initialDelta.width, `[${aMsg}] Inner to outer width delta.`);
+ is(h, initialDelta.height, `[${aMsg}] Inner to outer height delta.`);
+ }
+ }, 0);
+ }).then(() => {
+ let { width: w, height: h } = latestDelta;
+ is(w, initialDelta.width, `[${aMsg}] Final inner to outer width delta.`);
+ is(h, initialDelta.height, `[${aMsg}] Final Inner to outer height delta.`);
+ });
+}
+
+async function stopCheck(aMsg) {
+ info(`[${aMsg}] Stopping interval tester.`);
+ this.content.clearInterval(this.content.intervalID);
+ info(`[${aMsg}] Resolving interval tester.`);
+ this.content.resolveFunc();
+ await this.content.testerPromise;
+}
+
+add_task(async function test_innerToOuterDelta() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.net"
+ );
+ let popupBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ info("Opening popup.");
+ let popup = this.content.open(
+ "https://example.net",
+ "",
+ "width=200,height=200"
+ );
+ info("Waiting for load event.");
+ await ContentTaskUtils.waitForEvent(popup, "load");
+ return popup.browsingContext;
+ }
+ );
+
+ await SpecialPowers.spawn(
+ popupBrowsingContext,
+ ["Content"],
+ checkForDeltaMismatch
+ );
+ let popupChrome = popupBrowsingContext.topChromeWindow;
+ await SpecialPowers.spawn(popupChrome, ["Chrome"], checkForDeltaMismatch);
+
+ let numResizes = 3;
+ let resizeStep = 5;
+ let { outerWidth: width, outerHeight: height } = popupChrome;
+ let finalWidth = width + numResizes * resizeStep;
+ let finalHeight = height + numResizes * resizeStep;
+
+ info(`Starting ${numResizes} resizes.`);
+ await new Promise(resolve => {
+ let resizeListener = () => {
+ if (
+ popupChrome.outerWidth == finalWidth &&
+ popupChrome.outerHeight == finalHeight
+ ) {
+ popupChrome.removeEventListener("resize", resizeListener);
+ resolve();
+ }
+ };
+ popupChrome.addEventListener("resize", resizeListener);
+
+ let resizeNext = () => {
+ width += resizeStep;
+ height += resizeStep;
+ info(`Resizing to ${width}x${height}`);
+ popupChrome.resizeTo(width, height);
+ numResizes--;
+ if (numResizes > 0) {
+ info(`${numResizes} resizes remaining.`);
+ popupChrome.requestAnimationFrame(resizeNext);
+ }
+ };
+ resizeNext();
+ });
+
+ await SpecialPowers.spawn(popupBrowsingContext, ["Content"], stopCheck);
+ await SpecialPowers.spawn(popupChrome, ["Chrome"], stopCheck);
+
+ await SpecialPowers.spawn(popupBrowsingContext, [], () => {
+ this.content.close();
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/popups/browser_popup_linux_move.js b/browser/base/content/test/popups/browser_popup_linux_move.js
new file mode 100644
index 0000000000..f318ee1873
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_linux_move.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function createLinuxMoveTests(aFirstValue, aSecondValue, aMsg) {
+ for (let prop of ["screenX", "screenY"]) {
+ let first = {};
+ first[prop] = aFirstValue;
+ let second = {};
+ second[prop] = aSecondValue;
+ new ResizeMoveTest(
+ [first, second],
+ /* aInstant */ true,
+ `${aMsg} ${prop},${prop}`
+ );
+ new ResizeMoveTest(
+ [first, second],
+ /* aInstant */ false,
+ `${aMsg} ${prop},${prop}`
+ );
+ }
+}
+
+if (AppConstants.platform == "linux" && gfxInfo.windowProtocol == "wayland") {
+ add_task(async () => {
+ let tab = await ResizeMoveTest.GetOrCreateTab();
+ let browsingContext =
+ await ResizeMoveTest.GetOrCreatePopupBrowsingContext();
+ let win = browsingContext.topChromeWindow;
+ let targetX = win.screenX + 10;
+ win.moveTo(targetX, win.screenY);
+ await BrowserTestUtils.waitForCondition(() => win.screenX == targetX).catch(
+ () => {}
+ );
+ todo(win.screenX == targetX, "Moving windows on wayland.");
+ win.close();
+ await BrowserTestUtils.removeTab(tab);
+ });
+} else {
+ createLinuxMoveTests(9, 10, "Move");
+ createLinuxMoveTests(10, 0, "Move revert");
+ createLinuxMoveTests(10, 10, "Move repeat");
+
+ new ResizeMoveTest(
+ [{ screenX: 10 }, { screenY: 10 }, { screenX: 20 }],
+ /* aInstant */ true,
+ "Move sequence",
+ /* aWaitForCompletion */ true
+ );
+
+ new ResizeMoveTest(
+ [{ screenX: 10 }, { screenY: 10 }, { screenX: 20 }],
+ /* aInstant */ false,
+ "Move sequence",
+ /* aWaitForCompletion */ true
+ );
+}
diff --git a/browser/base/content/test/popups/browser_popup_linux_resize.js b/browser/base/content/test/popups/browser_popup_linux_resize.js
new file mode 100644
index 0000000000..6917c5823d
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_linux_resize.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function createLinuxResizeTests(aFirstValue, aSecondValue, aMsg) {
+ for (let prop of ResizeMoveTest.PropInfo.sizeProps) {
+ // For e.g 'outerWidth' this will be 'innerWidth'.
+ let otherProp = ResizeMoveTest.PropInfo.crossBoundsMapping[prop];
+ let first = {};
+ first[prop] = aFirstValue;
+ let second = {};
+ second[otherProp] = aSecondValue;
+ new ResizeMoveTest(
+ [first, second],
+ /* aInstant */ true,
+ `${aMsg} ${prop},${otherProp}`
+ );
+ new ResizeMoveTest(
+ [first, second],
+ /* aInstant */ false,
+ `${aMsg} ${prop},${otherProp}`
+ );
+ }
+}
+
+createLinuxResizeTests(9, 10, "Resize");
+createLinuxResizeTests(10, 0, "Resize revert");
+createLinuxResizeTests(10, 10, "Resize repeat");
+
+new ResizeMoveTest(
+ [
+ { outerWidth: 10 },
+ { innerHeight: 10 },
+ { innerWidth: 20 },
+ { outerHeight: 20 },
+ { outerWidth: 30 },
+ ],
+ /* aInstant */ true,
+ "Resize sequence",
+ /* aWaitForCompletion */ true
+);
+
+new ResizeMoveTest(
+ [
+ { outerWidth: 10 },
+ { innerHeight: 10 },
+ { innerWidth: 20 },
+ { outerHeight: 20 },
+ { outerWidth: 30 },
+ ],
+ /* aInstant */ false,
+ "Resize sequence",
+ /* aWaitForCompletion */ true
+);
diff --git a/browser/base/content/test/popups/browser_popup_move.js b/browser/base/content/test/popups/browser_popup_move.js
new file mode 100644
index 0000000000..d7d47e12f5
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_move.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericMoveTests(/* aInstant */ false, "Move");
diff --git a/browser/base/content/test/popups/browser_popup_move_instant.js b/browser/base/content/test/popups/browser_popup_move_instant.js
new file mode 100644
index 0000000000..c8a9219d82
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_move_instant.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericMoveTests(/* aInstant */ true, "Move");
diff --git a/browser/base/content/test/popups/browser_popup_new_window_resize.js b/browser/base/content/test/popups/browser_popup_new_window_resize.js
new file mode 100644
index 0000000000..81d3cf5a9a
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_new_window_resize.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_new_window_resize() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.net"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ info("Opening popup.");
+ let win = this.content.open(
+ "https://example.net",
+ "",
+ "width=200,height=200"
+ );
+
+ await ContentTaskUtils.waitForEvent(win, "load");
+
+ let { outerWidth: initialWidth, outerHeight: initialHeight } = win;
+ let targetWidth = initialWidth + 100;
+ let targetHeight = initialHeight + 100;
+
+ let observedOurResizeEvent = false;
+ let resizeListener = () => {
+ let { outerWidth: currentWidth, outerHeight: currentHeight } = win;
+ info(`Resize event for ${currentWidth}x${currentHeight}.`);
+ if (currentWidth == targetWidth && currentHeight == targetHeight) {
+ ok(!observedOurResizeEvent, "First time we receive our resize event.");
+ observedOurResizeEvent = true;
+ }
+ };
+ win.addEventListener("resize", resizeListener);
+ win.resizeTo(targetWidth, targetHeight);
+
+ await ContentTaskUtils.waitForCondition(
+ () => observedOurResizeEvent,
+ `Waiting for our resize event (${targetWidth}x${targetHeight}).`
+ );
+
+ info("Waiting for potentially incoming resize events.");
+ for (let i = 0; i < 10; i++) {
+ await new Promise(r => win.requestAnimationFrame(r));
+ }
+ win.removeEventListener("resize", resizeListener);
+ info("Closing popup.");
+ win.close();
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/popups/browser_popup_new_window_size.js b/browser/base/content/test/popups/browser_popup_new_window_size.js
new file mode 100644
index 0000000000..5f5d57e31e
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_new_window_size.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function test_new_window_size() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ baseURL
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ info("Opening popup.");
+ let requestedWidth = 200;
+ let requestedHeight = 200;
+ let win = this.content.open(
+ "popup_size.html",
+ "",
+ `width=${requestedWidth},height=${requestedHeight}`
+ );
+
+ let loadPromise = ContentTaskUtils.waitForEvent(win, "load");
+
+ let { innerWidth: preLoadWidth, innerHeight: preLoadHeight } = win;
+ is(preLoadWidth, requestedWidth, "Width before load event.");
+ is(preLoadHeight, requestedHeight, "Height before load event.");
+
+ await loadPromise;
+
+ let { innerWidth: postLoadWidth, innerHeight: postLoadHeight } = win;
+ is(postLoadWidth, requestedWidth, "Width after load event.");
+ is(postLoadHeight, requestedHeight, "Height after load event.");
+
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ win.innerWidth == requestedWidth && win.innerHeight == requestedHeight,
+ "Waiting for window to become request size."
+ );
+
+ let { innerWidth: finalWidth, innerHeight: finalHeight } = win;
+ is(finalWidth, requestedWidth, "Final width.");
+ is(finalHeight, requestedHeight, "Final height.");
+
+ await SpecialPowers.spawn(
+ win,
+ [{ requestedWidth, requestedHeight }],
+ async input => {
+ let { initialSize, loadSize } = this.content.wrappedJSObject;
+ is(
+ initialSize.width,
+ input.requestedWidth,
+ "Content width before load event."
+ );
+ is(
+ initialSize.height,
+ input.requestedHeight,
+ "Content height before load event."
+ );
+ is(
+ loadSize.width,
+ input.requestedWidth,
+ "Content width after load event."
+ );
+ is(
+ loadSize.height,
+ input.requestedHeight,
+ "Content height after load event."
+ );
+ is(
+ this.content.innerWidth,
+ input.requestedWidth,
+ "Content final width."
+ );
+ is(
+ this.content.innerHeight,
+ input.requestedHeight,
+ "Content final height."
+ );
+ }
+ );
+
+ info("Closing popup.");
+ win.close();
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/popups/browser_popup_resize.js b/browser/base/content/test/popups/browser_popup_resize.js
new file mode 100644
index 0000000000..c73ffb58c5
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericResizeTests(9, 10, /* aInstant */ false, "Resize");
diff --git a/browser/base/content/test/popups/browser_popup_resize_instant.js b/browser/base/content/test/popups/browser_popup_resize_instant.js
new file mode 100644
index 0000000000..4613e272d6
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize_instant.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericResizeTests(9, 10, /* aInstant */ true, "Resize");
diff --git a/browser/base/content/test/popups/browser_popup_resize_repeat.js b/browser/base/content/test/popups/browser_popup_resize_repeat.js
new file mode 100644
index 0000000000..d76f0fd3a2
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize_repeat.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericResizeTests(10, 10, /* aInstant */ false, "Resize repeat");
diff --git a/browser/base/content/test/popups/browser_popup_resize_repeat_instant.js b/browser/base/content/test/popups/browser_popup_resize_repeat_instant.js
new file mode 100644
index 0000000000..9870813d87
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize_repeat_instant.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericResizeTests(10, 10, /* aInstant */ true, "Resize repeat");
diff --git a/browser/base/content/test/popups/browser_popup_resize_revert.js b/browser/base/content/test/popups/browser_popup_resize_revert.js
new file mode 100644
index 0000000000..e87ef30f69
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize_revert.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericResizeTests(10, 0, /* aInstant */ false, "Resize revert");
diff --git a/browser/base/content/test/popups/browser_popup_resize_revert_instant.js b/browser/base/content/test/popups/browser_popup_resize_revert_instant.js
new file mode 100644
index 0000000000..5fa2ce7d5d
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize_revert_instant.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+createGenericResizeTests(10, 0, /* aInstant */ true, "Resize revert");
diff --git a/browser/base/content/test/popups/head.js b/browser/base/content/test/popups/head.js
new file mode 100644
index 0000000000..f72bba7dca
--- /dev/null
+++ b/browser/base/content/test/popups/head.js
@@ -0,0 +1,574 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ SpecialPowers.Ci.nsIGfxInfo
+);
+
+async function waitForBlockedPopups(numberOfPopups, { doc }) {
+ let toolbarDoc = doc || document;
+ let menupopup = toolbarDoc.getElementById("blockedPopupOptions");
+ await BrowserTestUtils.waitForCondition(() => {
+ let popups = menupopup.querySelectorAll("[popupReportIndex]");
+ return popups.length == numberOfPopups;
+ }, `Waiting for ${numberOfPopups} popups`);
+}
+
+/*
+ * Tests that a sequence of size changes ultimately results in the latest
+ * requested size. The test also fails when an unexpected window size is
+ * observed in a resize event.
+ *
+ * aPropertyDeltas List of objects where keys describe the name of a window
+ * property and the values the difference to its initial
+ * value.
+ *
+ * aInstant Issue changes without additional waiting in between.
+ *
+ * A brief example of the resutling code that is effectively run for the
+ * following list of deltas:
+ * [{outerWidth: 5, outerHeight: 10}, {outerWidth: 10}]
+ *
+ * let initialWidth = win.outerWidth;
+ * let initialHeight = win.outerHeight;
+ *
+ * if (aInstant) {
+ * win.outerWidth = initialWidth + 5;
+ * win.outerHeight = initialHeight + 10;
+ *
+ * win.outerWidth = initialWidth + 10;
+ * } else {
+ * win.requestAnimationFrame(() => {
+ * win.outerWidth = initialWidth + 5;
+ * win.outerHeight = initialHeight + 10;
+ *
+ * win.requestAnimationFrame(() => {
+ * win.outerWidth = initialWidth + 10;
+ * });
+ * });
+ * }
+ */
+async function testPropertyDeltas(
+ aPropertyDeltas,
+ aInstant,
+ aPropInfo,
+ aMsg,
+ aWaitForCompletion
+) {
+ let msg = `[${aMsg}]`;
+
+ let win = this.content.popup || this.content.wrappedJSObject;
+
+ // Property names and mapping from ResizeMoveTest
+ let {
+ sizeProps,
+ positionProps /* can be empty/incomplete as workaround on Linux */,
+ readonlyProps,
+ crossBoundsMapping,
+ } = aPropInfo;
+
+ let stringifyState = state => {
+ let stateMsg = sizeProps
+ .concat(positionProps)
+ .filter(prop => state[prop] !== undefined)
+ .map(prop => `${prop}: ${state[prop]}`)
+ .join(", ");
+ return `{ ${stateMsg} }`;
+ };
+
+ let initialState = {};
+ let finalState = {};
+
+ info("Initializing all values to current state.");
+ for (let prop of sizeProps.concat(positionProps)) {
+ let value = win[prop];
+ initialState[prop] = value;
+ finalState[prop] = value;
+ }
+
+ // List of potential states during resize events. The current state is also
+ // considered valid, as the resize event might still be outstanding.
+ let validResizeStates = [initialState];
+
+ let updateFinalState = (aProp, aDelta) => {
+ if (
+ readonlyProps.includes(aProp) ||
+ !sizeProps.concat(positionProps).includes(aProp)
+ ) {
+ throw new Error(`Unexpected property "${aProp}".`);
+ }
+
+ // Update both properties of the same axis.
+ let otherProp = crossBoundsMapping[aProp];
+ finalState[aProp] = initialState[aProp] + aDelta;
+ finalState[otherProp] = initialState[otherProp] + aDelta;
+
+ // Mark size as valid in resize event.
+ if (sizeProps.includes(aProp)) {
+ let state = {};
+ sizeProps.forEach(p => (state[p] = finalState[p]));
+ validResizeStates.push(state);
+ }
+ };
+
+ info("Adding resize event listener.");
+ let resizeCount = 0;
+ let resizeListener = evt => {
+ resizeCount++;
+
+ let currentSizeState = {};
+ sizeProps.forEach(p => (currentSizeState[p] = win[p]));
+
+ info(
+ `${msg} ${resizeCount}. resize event: ${stringifyState(currentSizeState)}`
+ );
+ let matchingIndex = validResizeStates.findIndex(state =>
+ sizeProps.every(p => state[p] == currentSizeState[p])
+ );
+ if (matchingIndex < 0) {
+ info(`${msg} Size state should have been one of:`);
+ for (let state of validResizeStates) {
+ info(stringifyState(state));
+ }
+ }
+
+ if (win.gBrowser && evt.target != win) {
+ // Without e10s we receive content resize events in chrome windows.
+ todo(false, `${msg} Resize event target is our window.`);
+ return;
+ }
+
+ ok(
+ matchingIndex >= 0,
+ `${msg} Valid intermediate state. Current: ` +
+ stringifyState(currentSizeState)
+ );
+
+ // No longer allow current and preceding states.
+ validResizeStates.splice(0, matchingIndex + 1);
+ };
+ win.addEventListener("resize", resizeListener);
+
+ const useProperties = !Services.prefs.getBoolPref(
+ "dom.window_position_size_properties_replaceable.enabled",
+ true
+ );
+
+ info("Starting property changes.");
+ await new Promise(resolve => {
+ let index = 0;
+ let next = async () => {
+ let pre = `${msg} [${index + 1}/${aPropertyDeltas.length}]`;
+
+ let deltaObj = aPropertyDeltas[index];
+ for (let prop in deltaObj) {
+ updateFinalState(prop, deltaObj[prop]);
+
+ let targetValue = initialState[prop] + deltaObj[prop];
+ info(`${pre} Setting ${prop} to ${targetValue}.`);
+ if (useProperties) {
+ win[prop] = targetValue;
+ } else if (sizeProps.includes(prop)) {
+ win.resizeTo(finalState.outerWidth, finalState.outerHeight);
+ } else {
+ win.moveTo(finalState.screenX, finalState.screenY);
+ }
+ if (aWaitForCompletion) {
+ await ContentTaskUtils.waitForCondition(
+ () => win[prop] == targetValue,
+ `${msg} Waiting for ${prop} to be ${targetValue}.`
+ );
+ }
+ }
+
+ index++;
+ if (index < aPropertyDeltas.length) {
+ scheduleNext();
+ } else {
+ resolve();
+ }
+ };
+
+ let scheduleNext = () => {
+ if (aInstant) {
+ next();
+ } else {
+ info(`${msg} Requesting animation frame.`);
+ win.requestAnimationFrame(next);
+ }
+ };
+ scheduleNext();
+ });
+
+ try {
+ info(`${msg} Waiting for window to match the final state.`);
+ await ContentTaskUtils.waitForCondition(
+ () => sizeProps.concat(positionProps).every(p => win[p] == finalState[p]),
+ "Waiting for final state."
+ );
+ } catch (e) {}
+
+ info(`${msg} Checking final state.`);
+ info(`${msg} Exepected: ${stringifyState(finalState)}`);
+ info(`${msg} Actual: ${stringifyState(win)}`);
+ for (let prop of sizeProps.concat(positionProps)) {
+ is(win[prop], finalState[prop], `${msg} Expected final value for ${prop}`);
+ }
+
+ win.removeEventListener("resize", resizeListener);
+}
+
+function roundedCenter(aDimension, aOrigin) {
+ let center = aOrigin + Math.floor(aDimension / 2);
+ return center - (center % 100);
+}
+
+class ResizeMoveTest {
+ static WindowWidth = 200;
+ static WindowHeight = 200;
+ static WindowLeft = roundedCenter(screen.availWidth - 200, screen.left);
+ static WindowTop = roundedCenter(screen.availHeight - 200, screen.top);
+
+ static PropInfo = {
+ sizeProps: ["outerWidth", "outerHeight", "innerWidth", "innerHeight"],
+ positionProps: [
+ "screenX",
+ "screenY",
+ /* readonly */ "mozInnerScreenX",
+ /* readonly */ "mozInnerScreenY",
+ ],
+ readonlyProps: ["mozInnerScreenX", "mozInnerScreenY"],
+ crossAxisMapping: {
+ outerWidth: "outerHeight",
+ outerHeight: "outerWidth",
+ innerWidth: "innerHeight",
+ innerHeight: "innerWidth",
+ screenX: "screenY",
+ screenY: "screenX",
+ mozInnerScreenX: "mozInnerScreenY",
+ mozInnerScreenY: "mozInnerScreenX",
+ },
+ crossBoundsMapping: {
+ outerWidth: "innerWidth",
+ outerHeight: "innerHeight",
+ innerWidth: "outerWidth",
+ innerHeight: "outerHeight",
+ screenX: "mozInnerScreenX",
+ screenY: "mozInnerScreenY",
+ mozInnerScreenX: "screenX",
+ mozInnerScreenY: "screenY",
+ },
+ };
+
+ constructor(
+ aPropertyDeltas,
+ aInstant = false,
+ aMsg = "ResizeMoveTest",
+ aWaitForCompletion = false
+ ) {
+ this.propertyDeltas = aPropertyDeltas;
+ this.instant = aInstant;
+ this.msg = aMsg;
+ this.waitForCompletion = aWaitForCompletion;
+
+ // Allows to ignore positions while testing.
+ this.ignorePositions = false;
+ // Allows to ignore only mozInnerScreenX/Y properties while testing.
+ this.ignoreMozInnerScreen = false;
+ // Allows to skip checking the restored position after testing.
+ this.ignoreRestoredPosition = false;
+
+ if (AppConstants.platform == "linux" && !SpecialPowers.isHeadless) {
+ // We can occasionally start the test while nsWindow reports a wrong
+ // client offset (gdk origin and root_origin are out of sync). This
+ // results in false expectations for the final mozInnerScreenX/Y values.
+ this.ignoreMozInnerScreen = !ResizeMoveTest.hasCleanUpTask;
+
+ let { positionProps } = ResizeMoveTest.PropInfo;
+ let resizeOnlyTest = aPropertyDeltas.every(deltaObj =>
+ positionProps.every(prop => deltaObj[prop] === undefined)
+ );
+
+ let isWayland = gfxInfo.windowProtocol == "wayland";
+ if (resizeOnlyTest && isWayland) {
+ // On Wayland we can't move the window in general. The window also
+ // doesn't necessarily open our specified position.
+ this.ignoreRestoredPosition = true;
+ // We can catch bad screenX/Y at the start of the first test in a
+ // window.
+ this.ignorePositions = !ResizeMoveTest.hasCleanUpTask;
+ }
+ }
+
+ if (!ResizeMoveTest.hasCleanUpTask) {
+ ResizeMoveTest.hasCleanUpTask = true;
+ registerCleanupFunction(ResizeMoveTest.Cleanup);
+ }
+
+ add_task(async () => {
+ let tab = await ResizeMoveTest.GetOrCreateTab();
+ let browsingContext =
+ await ResizeMoveTest.GetOrCreatePopupBrowsingContext();
+ if (!browsingContext) {
+ return;
+ }
+
+ info("=== Running in content. ===");
+ await this.run(browsingContext, `${this.msg} (content)`);
+ await this.restorePopupState(browsingContext);
+
+ info("=== Running in chrome. ===");
+ let popupChrome = browsingContext.topChromeWindow;
+ await this.run(popupChrome.browsingContext, `${this.msg} (chrome)`);
+ await this.restorePopupState(browsingContext);
+
+ info("=== Running in opener. ===");
+ await this.run(tab.linkedBrowser, `${this.msg} (opener)`);
+ await this.restorePopupState(browsingContext);
+ });
+ }
+
+ async run(aBrowsingContext, aMsg) {
+ let testType = this.instant ? "instant" : "fanned out";
+ let msg = `${aMsg} (${testType})`;
+
+ let propInfo = {};
+ for (let k in ResizeMoveTest.PropInfo) {
+ propInfo[k] = ResizeMoveTest.PropInfo[k];
+ }
+ if (this.ignoreMozInnerScreen) {
+ todo(false, `[${aMsg}] Shouldn't ignore mozInnerScreenX/Y.`);
+ propInfo.positionProps = propInfo.positionProps.filter(
+ prop => !["mozInnerScreenX", "mozInnerScreenY"].includes(prop)
+ );
+ }
+ if (this.ignorePositions) {
+ todo(false, `[${aMsg}] Shouldn't ignore position.`);
+ propInfo.positionProps = [];
+ }
+
+ info(`${msg}: ` + JSON.stringify(this.propertyDeltas));
+ await SpecialPowers.spawn(
+ aBrowsingContext,
+ [
+ this.propertyDeltas,
+ this.instant,
+ propInfo,
+ msg,
+ this.waitForCompletion,
+ ],
+ testPropertyDeltas
+ );
+ }
+
+ async restorePopupState(aBrowsingContext) {
+ info("Restore popup state.");
+
+ let { deltaWidth, deltaHeight } = await SpecialPowers.spawn(
+ aBrowsingContext,
+ [],
+ () => {
+ return {
+ deltaWidth: this.content.outerWidth - this.content.innerWidth,
+ deltaHeight: this.content.outerHeight - this.content.innerHeight,
+ };
+ }
+ );
+
+ let chromeWindow = aBrowsingContext.topChromeWindow;
+ let {
+ WindowLeft: left,
+ WindowTop: top,
+ WindowWidth: width,
+ WindowHeight: height,
+ } = ResizeMoveTest;
+
+ chromeWindow.resizeTo(width + deltaWidth, height + deltaHeight);
+ chromeWindow.moveTo(left, top);
+
+ await SpecialPowers.spawn(
+ aBrowsingContext,
+ [left, top, width, height, this.ignoreRestoredPosition],
+ async (aLeft, aTop, aWidth, aHeight, aIgnorePosition) => {
+ let win = this.content.wrappedJSObject;
+
+ info("Waiting for restored size.");
+ await ContentTaskUtils.waitForCondition(
+ () => win.innerWidth == aWidth && win.innerHeight === aHeight,
+ "Waiting for restored size."
+ );
+ is(win.innerWidth, aWidth, "Restored width.");
+ is(win.innerHeight, aHeight, "Restored height.");
+
+ if (!aIgnorePosition) {
+ info("Waiting for restored position.");
+ await ContentTaskUtils.waitForCondition(
+ () => win.screenX == aLeft && win.screenY === aTop,
+ "Waiting for restored position."
+ );
+ is(win.screenX, aLeft, "Restored screenX.");
+ is(win.screenY, aTop, "Restored screenY.");
+ } else {
+ todo(false, "Shouldn't ignore restored position.");
+ }
+ }
+ );
+ }
+
+ static async GetOrCreateTab() {
+ if (ResizeMoveTest.tab) {
+ return ResizeMoveTest.tab;
+ }
+
+ info("Opening tab.");
+ ResizeMoveTest.tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.net/browser/browser/base/content/test/popups/popup_blocker_a.html"
+ );
+ return ResizeMoveTest.tab;
+ }
+
+ static async GetOrCreatePopupBrowsingContext() {
+ if (ResizeMoveTest.popupBrowsingContext) {
+ if (!ResizeMoveTest.popupBrowsingContext.isActive) {
+ return undefined;
+ }
+ return ResizeMoveTest.popupBrowsingContext;
+ }
+
+ let tab = await ResizeMoveTest.GetOrCreateTab();
+ info("Opening popup.");
+ ResizeMoveTest.popupBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [
+ ResizeMoveTest.WindowWidth,
+ ResizeMoveTest.WindowHeight,
+ ResizeMoveTest.WindowLeft,
+ ResizeMoveTest.WindowTop,
+ ],
+ async (aWidth, aHeight, aLeft, aTop) => {
+ let win = this.content.open(
+ this.content.document.location.href,
+ "_blank",
+ `left=${aLeft},top=${aTop},width=${aWidth},height=${aHeight}`
+ );
+ this.content.popup = win;
+
+ await new Promise(r => (win.onload = r));
+
+ return win.browsingContext;
+ }
+ );
+
+ return ResizeMoveTest.popupBrowsingContext;
+ }
+
+ static async Cleanup() {
+ let browsingContext = ResizeMoveTest.popupBrowsingContext;
+ if (browsingContext) {
+ await SpecialPowers.spawn(browsingContext, [], () => {
+ this.content.close();
+ });
+ delete ResizeMoveTest.popupBrowsingContext;
+ }
+
+ let tab = ResizeMoveTest.tab;
+ if (tab) {
+ await BrowserTestUtils.removeTab(tab);
+ delete ResizeMoveTest.tab;
+ }
+ ResizeMoveTest.hasCleanUpTask = false;
+ }
+}
+
+function chaosRequestLongerTimeout(aDoRequest) {
+ if (aDoRequest && parseInt(Services.env.get("MOZ_CHAOSMODE"), 16)) {
+ requestLongerTimeout(2);
+ }
+}
+
+function createGenericResizeTests(aFirstValue, aSecondValue, aInstant, aMsg) {
+ // Runtime almost doubles in chaos mode on Mac.
+ chaosRequestLongerTimeout(AppConstants.platform == "macosx");
+
+ let { crossBoundsMapping, crossAxisMapping } = ResizeMoveTest.PropInfo;
+
+ for (let prop of ["innerWidth", "outerHeight"]) {
+ // Mixing inner and outer property.
+ for (let secondProp of [prop, crossBoundsMapping[prop]]) {
+ let first = {};
+ first[prop] = aFirstValue;
+ let second = {};
+ second[secondProp] = aSecondValue;
+ new ResizeMoveTest(
+ [first, second],
+ aInstant,
+ `${aMsg} ${prop},${secondProp}`
+ );
+ }
+ }
+
+ for (let prop of ["innerHeight", "outerWidth"]) {
+ let first = {};
+ first[prop] = aFirstValue;
+ let second = {};
+ second[prop] = aSecondValue;
+
+ // Setting property of other axis before/between two changes.
+ let otherProps = [
+ crossAxisMapping[prop],
+ crossAxisMapping[crossBoundsMapping[prop]],
+ ];
+ for (let interferenceProp of otherProps) {
+ let interference = {};
+ interference[interferenceProp] = 20;
+ new ResizeMoveTest(
+ [first, interference, second],
+ aInstant,
+ `${aMsg} ${prop},${interferenceProp},${prop}`
+ );
+ new ResizeMoveTest(
+ [interference, first, second],
+ aInstant,
+ `${aMsg} ${interferenceProp},${prop},${prop}`
+ );
+ }
+ }
+}
+
+function createGenericMoveTests(aInstant, aMsg) {
+ // Runtime almost doubles in chaos mode on Mac.
+ chaosRequestLongerTimeout(AppConstants.platform == "macosx");
+
+ let { crossAxisMapping } = ResizeMoveTest.PropInfo;
+
+ for (let prop of ["screenX", "screenY"]) {
+ for (let [v1, v2, msg] of [
+ [9, 10, `${aMsg}`],
+ [11, 11, `${aMsg} repeat`],
+ [12, 0, `${aMsg} revert`],
+ ]) {
+ let first = {};
+ first[prop] = v1;
+ let second = {};
+ second[prop] = v2;
+ new ResizeMoveTest([first, second], aInstant, `${msg} ${prop},${prop}`);
+
+ let interferenceProp = crossAxisMapping[prop];
+ let interference = {};
+ interference[interferenceProp] = 20;
+ new ResizeMoveTest(
+ [first, interference, second],
+ aInstant,
+ `${aMsg} ${prop},${interferenceProp},${prop}`
+ );
+ new ResizeMoveTest(
+ [interference, first, second],
+ aInstant,
+ `${msg} ${interferenceProp},${prop},${prop}`
+ );
+ }
+ }
+}
diff --git a/browser/base/content/test/popups/popup_blocker.html b/browser/base/content/test/popups/popup_blocker.html
new file mode 100644
index 0000000000..8e2d958059
--- /dev/null
+++ b/browser/base/content/test/popups/popup_blocker.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page creating two popups</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ window.open("popup_blocker_a.html", "a");
+ window.open("popup_blocker_b.html", "b");
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/popups/popup_blocker2.html b/browser/base/content/test/popups/popup_blocker2.html
new file mode 100644
index 0000000000..ec880c0821
--- /dev/null
+++ b/browser/base/content/test/popups/popup_blocker2.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page creating a popup</title>
+ </head>
+ <body>
+ <button id="pop" onclick='window.setTimeout(() => {window.open("popup_blocker_a.html", "a");}, 10);'>Open Popup</button>
+ </body>
+</html>
diff --git a/browser/base/content/test/popups/popup_blocker_10_popups.html b/browser/base/content/test/popups/popup_blocker_10_popups.html
new file mode 100644
index 0000000000..9dc288f472
--- /dev/null
+++ b/browser/base/content/test/popups/popup_blocker_10_popups.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page creating ten popups</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ for (let i = 0; i < 10; i++) {
+ window.open("https://example.com");
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/popups/popup_blocker_a.html b/browser/base/content/test/popups/popup_blocker_a.html
new file mode 100644
index 0000000000..b6f94b5b26
--- /dev/null
+++ b/browser/base/content/test/popups/popup_blocker_a.html
@@ -0,0 +1 @@
+<html><body>a</body></html>
diff --git a/browser/base/content/test/popups/popup_blocker_b.html b/browser/base/content/test/popups/popup_blocker_b.html
new file mode 100644
index 0000000000..954061e2ce
--- /dev/null
+++ b/browser/base/content/test/popups/popup_blocker_b.html
@@ -0,0 +1 @@
+<html><body>b</body></html>
diff --git a/browser/base/content/test/popups/popup_blocker_frame.html b/browser/base/content/test/popups/popup_blocker_frame.html
new file mode 100644
index 0000000000..e29452fb63
--- /dev/null
+++ b/browser/base/content/test/popups/popup_blocker_frame.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page with iframe that contains page that opens two popups</title>
+ </head>
+ <body>
+ <iframe id="iframe"></iframe>
+ <script type="text/javascript">
+ let params = new URLSearchParams(location.search);
+ let base = params.get('base') || location.href;
+ let frame = document.getElementById('iframe');
+
+ function addPopupOpeningFrame() {
+ frame.src = new URL("popup_blocker.html", base);
+ }
+
+ if (params.get('delayed') !== 'true') {
+ addPopupOpeningFrame();
+ } else {
+ addEventListener("message", () => {
+ addPopupOpeningFrame();
+ }, {once: true});
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/popups/popup_size.html b/browser/base/content/test/popups/popup_size.html
new file mode 100644
index 0000000000..c214486dee
--- /dev/null
+++ b/browser/base/content/test/popups/popup_size.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page recording its size</title>
+ </head>
+ <body>
+ <script>
+ var initialSize = { width: innerWidth, height: innerHeight };
+ var loadSize;
+ onload = () => {
+ loadSize = { width: innerWidth, height: innerHeight };
+ };
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/benignPage.html b/browser/base/content/test/protectionsUI/benignPage.html
new file mode 100644
index 0000000000..0be1cbc1c7
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/benignPage.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <!--TODO: We used to have an iframe here, to double-check that benign-->
+ <!--iframes may be included in pages. However, the cookie restrictions-->
+ <!--project introduced a change that declared blockable content to be-->
+ <!--found on any page that embeds iframes, rendering this unusable for-->
+ <!--our purposes. That's not ideal and we intend to restore this iframe.-->
+ <!--(See bug 1511303 for a more detailed technical explanation.)-->
+ <!--<iframe src="http://not-tracking.example.com/"></iframe>-->
+ </body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/browser.ini b/browser/base/content/test/protectionsUI/browser.ini
new file mode 100644
index 0000000000..acf3f7125b
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser.ini
@@ -0,0 +1,63 @@
+[DEFAULT]
+tags = trackingprotection
+support-files =
+ head.js
+ benignPage.html
+ containerPage.html
+ cookiePage.html
+ cookieSetterPage.html
+ cookieServer.sjs
+ emailTrackingPage.html
+ embeddedPage.html
+ trackingAPI.js
+ trackingPage.html
+
+[browser_protectionsUI.js]
+https_first_disabled = true
+skip-if =
+ os == 'linux' && !debug && tsan # Bug 1675107
+[browser_protectionsUI_3.js]
+[browser_protectionsUI_background_tabs.js]
+https_first_disabled = true
+[browser_protectionsUI_categories.js]
+https_first_disabled = true
+[browser_protectionsUI_cookie_banner.js]
+[browser_protectionsUI_cookies_subview.js]
+https_first_disabled = true
+[browser_protectionsUI_cryptominers.js]
+https_first_disabled = true
+[browser_protectionsUI_email_trackers_subview.js]
+[browser_protectionsUI_fetch.js]
+https_first_disabled = true
+support-files =
+ file_protectionsUI_fetch.html
+ file_protectionsUI_fetch.js
+ file_protectionsUI_fetch.js^headers^
+[browser_protectionsUI_fingerprinters.js]
+https_first_disabled = true
+[browser_protectionsUI_icon_state.js]
+https_first_disabled = true
+[browser_protectionsUI_milestones.js]
+[browser_protectionsUI_open_preferences.js]
+https_first_disabled = true
+[browser_protectionsUI_pbmode_exceptions.js]
+https_first_disabled = true
+[browser_protectionsUI_report_breakage.js]
+https_first_disabled = true
+skip-if = debug || asan # Bug 1546797
+[browser_protectionsUI_shield_visibility.js]
+support-files =
+ sandboxed.html
+ sandboxed.html^headers^
+[browser_protectionsUI_socialtracking.js]
+https_first_disabled = true
+[browser_protectionsUI_state.js]
+https_first_disabled = true
+[browser_protectionsUI_state_reset.js]
+https_first_disabled = true
+[browser_protectionsUI_subview_shim.js]
+https_first_disabled = true
+[browser_protectionsUI_telemetry.js]
+https_first_disabled = true
+[browser_protectionsUI_trackers_subview.js]
+https_first_disabled = true
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI.js b/browser/base/content/test/protectionsUI/browser_protectionsUI.js
new file mode 100644
index 0000000000..ea8737f28a
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI.js
@@ -0,0 +1,713 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Basic UI tests for the protections panel */
+
+"use strict";
+
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ContentBlockingAllowList:
+ "resource://gre/modules/ContentBlockingAllowList.sys.mjs",
+});
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set the auto hide timing to 100ms for blocking the test less.
+ ["browser.protections_panel.toast.timeout", 100],
+ // Hide protections cards so as not to trigger more async messaging
+ // when landing on the page.
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["privacy.trackingprotection.enabled", true],
+ ],
+ });
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.telemetry.clearEvents();
+
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.clearEvents();
+ });
+});
+
+add_task(async function testToggleSwitch() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TRACKING_PAGE
+ );
+
+ await openProtectionsPanel();
+
+ await TestUtils.waitForCondition(() => {
+ return gProtectionsHandler._protectionsPopup.hasAttribute("blocking");
+ });
+
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ let buttonEvents = events.filter(
+ e =>
+ e[1] == "security.ui.protectionspopup" &&
+ e[2] == "open" &&
+ e[3] == "protections_popup"
+ );
+ is(buttonEvents.length, 1, "recorded telemetry for opening the popup");
+
+ // Check the visibility of the "Site not working?" link.
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be visible."
+ );
+
+ // The 'Site Fixed?' link should be hidden.
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be hidden."
+ );
+
+ // Navigate through the 'Site Not Working?' flow and back to the main view,
+ // checking for telemetry on the way.
+ let siteNotWorkingView = document.getElementById(
+ "protections-popup-siteNotWorkingView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ siteNotWorkingView,
+ "ViewShown"
+ );
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink.click();
+ await viewShown;
+
+ checkClickTelemetry("sitenotworking_link");
+
+ let sendReportButton = document.getElementById(
+ "protections-popup-siteNotWorkingView-sendReport"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ sendReportButton.click();
+ await viewShown;
+
+ checkClickTelemetry("send_report_link");
+
+ viewShown = BrowserTestUtils.waitForEvent(siteNotWorkingView, "ViewShown");
+ sendReportView.querySelector(".subviewbutton-back").click();
+ await viewShown;
+
+ let mainView = document.getElementById("protections-popup-mainView");
+
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ siteNotWorkingView.querySelector(".subviewbutton-back").click();
+ await viewShown;
+
+ ok(
+ gProtectionsHandler._protectionsPopupTPSwitch.hasAttribute("enabled"),
+ "TP Switch should be enabled"
+ );
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ // The 'Site not working?' link should be hidden after clicking the TP switch.
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be hidden after TP switch turns to off."
+ );
+ // Same for the 'Site Fixed?' link
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be hidden."
+ );
+
+ await popuphiddenPromise;
+ checkClickTelemetry("etp_toggle_off");
+
+ // We need to wait toast's popup shown and popup hidden events. It won't fire
+ // the popup shown event if we open the protections panel while the toast is
+ // opening.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+
+ await browserLoadedPromise;
+
+ // Wait until the toast is shown and hidden.
+ await popupShownPromise;
+ await popuphiddenPromise;
+
+ await openProtectionsPanel();
+ ok(
+ !gProtectionsHandler._protectionsPopupTPSwitch.hasAttribute("enabled"),
+ "TP Switch should be disabled"
+ );
+
+ // The 'Site not working?' link should be hidden if the TP is off.
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be hidden if TP is off."
+ );
+
+ // The 'Site Fixed?' link should be shown if TP is off.
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be visible."
+ );
+
+ // Check telemetry for 'Site Fixed?' link.
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink.click();
+ await viewShown;
+
+ checkClickTelemetry("sitenotworking_link", "sitefixed");
+
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ sendReportView.querySelector(".subviewbutton-back").click();
+ await viewShown;
+
+ // Click the TP switch again and check the visibility of the 'Site not
+ // Working?'. It should be hidden after toggling the TP switch.
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ `The 'Site not working?' link should be still hidden after toggling TP
+ switch to on from off.`
+ );
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be hidden."
+ );
+
+ await browserLoadedPromise;
+ checkClickTelemetry("etp_toggle_on");
+
+ ContentBlockingAllowList.remove(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for the protection settings button.
+ */
+add_task(async function testSettingsButton() {
+ // Open a tab and its protection panel.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+ gProtectionsHandler._protectionsPopupSettingsButton.click();
+
+ // The protection popup should be hidden after clicking settings button.
+ await popuphiddenPromise;
+ // Wait until the about:preferences has been opened correctly.
+ let newTab = await newTabPromise;
+
+ ok(true, "about:preferences has been opened successfully");
+ checkClickTelemetry("settings");
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring Tracking Protection label is shown correctly
+ */
+add_task(async function testTrackingProtectionLabel() {
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let trackingProtectionLabel = document.getElementById(
+ "protections-popup-footer-protection-type-label"
+ );
+
+ is(
+ trackingProtectionLabel.textContent,
+ "Custom",
+ "The label is correctly set to Custom."
+ );
+ await closeProtectionsPanel();
+
+ Services.prefs.setStringPref("browser.contentblocking.category", "standard");
+ await openProtectionsPanel();
+
+ is(
+ trackingProtectionLabel.textContent,
+ "Standard",
+ "The label is correctly set to Standard."
+ );
+ await closeProtectionsPanel();
+
+ Services.prefs.setStringPref("browser.contentblocking.category", "strict");
+ await openProtectionsPanel();
+ is(
+ trackingProtectionLabel.textContent,
+ "Strict",
+ "The label is correctly set to Strict."
+ );
+
+ await closeProtectionsPanel();
+ Services.prefs.setStringPref("browser.contentblocking.category", "custom");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for the 'Show Full Report' button in the footer section.
+ */
+add_task(async function testShowFullReportButton() {
+ // Open a tab and its protection panel.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let newTabPromise = waitForAboutProtectionsTab();
+ let showFullReportButton = document.getElementById(
+ "protections-popup-show-report-button"
+ );
+
+ showFullReportButton.click();
+
+ // The protection popup should be hidden after clicking the link.
+ await popuphiddenPromise;
+ // Wait until the 'about:protections' has been opened correctly.
+ let newTab = await newTabPromise;
+
+ ok(true, "about:protections has been opened successfully");
+
+ checkClickTelemetry("full_report");
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring the mini panel is working correctly
+ */
+add_task(async function testMiniPanel() {
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ // Open the mini panel.
+ await openProtectionsPanel(true);
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+
+ // Check that only the header is displayed.
+ let mainView = document.getElementById("protections-popup-mainView");
+ for (let item of mainView.childNodes) {
+ if (item.id !== "protections-popup-mainView-panel-header-section") {
+ ok(
+ !BrowserTestUtils.is_visible(item),
+ `The section '${item.id}' is hidden in the toast.`
+ );
+ } else {
+ ok(
+ BrowserTestUtils.is_visible(item),
+ "The panel header is displayed as the content of the toast."
+ );
+ }
+ }
+
+ // Wait until the auto hide is happening.
+ await popuphiddenPromise;
+
+ ok(true, "The mini panel hides automatically.");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for the toggle switch flow
+ */
+add_task(async function testToggleSwitchFlow() {
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Click the TP switch, from On -> Off.
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ // Check that the icon state has been changed.
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "The tracking protection icon state has been changed to disabled."
+ );
+
+ // The panel should be closed and the mini panel will show up after refresh.
+ await popuphiddenPromise;
+ await browserLoadedPromise;
+ await popupShownPromise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+ "The protections popup should have the 'toast' attribute."
+ );
+
+ // Click on the mini panel and making sure the protection popup shows up.
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ document.getElementById("protections-popup-mainView-panel-header").click();
+ await popuphiddenPromise;
+ await popupShownPromise;
+
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+ "The 'toast' attribute should be cleared on the protections popup."
+ );
+
+ // Click the TP switch again, from Off -> On.
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ // Check that the icon state has been changed.
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "The tracking protection icon state has been changed to enabled."
+ );
+
+ // Protections popup hidden -> Page refresh -> Mini panel shows up.
+ await popuphiddenPromise;
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ await browserLoadedPromise;
+ await popupShownPromise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+ "The protections popup should have the 'toast' attribute."
+ );
+
+ // Wait until the auto hide is happening.
+ await popuphiddenPromise;
+
+ // Clean up the TP state.
+ ContentBlockingAllowList.remove(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring the tracking protection icon will show a correct
+ * icon according to the TP enabling state.
+ */
+add_task(async function testTrackingProtectionIcon() {
+ // Open a tab and its protection panel.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ let TPIcon = document.getElementById("tracking-protection-icon");
+ // Check the icon url. It will show a shield icon if TP is enabled.
+ is(
+ gBrowser.ownerGlobal
+ .getComputedStyle(TPIcon)
+ .getPropertyValue("list-style-image"),
+ `url("chrome://browser/skin/tracking-protection.svg")`,
+ "The tracking protection icon shows a shield icon."
+ );
+
+ // Disable the tracking protection.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ "https://example.com/"
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await browserLoadedPromise;
+
+ // Check that the tracking protection icon should show a strike-through shield
+ // icon after page is reloaded.
+ is(
+ gBrowser.ownerGlobal
+ .getComputedStyle(TPIcon)
+ .getPropertyValue("list-style-image"),
+ `url("chrome://browser/skin/tracking-protection-disabled.svg")`,
+ "The tracking protection icon shows a strike through shield icon."
+ );
+
+ // Clean up the TP state.
+ ContentBlockingAllowList.remove(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring the number of blocked trackers is displayed properly.
+ */
+add_task(async function testNumberOfBlockedTrackers() {
+ // First, clear the tracking database.
+ await TrackingDBService.clearAll();
+
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let trackerCounterBox = document.getElementById(
+ "protections-popup-trackers-blocked-counter-box"
+ );
+ let trackerCounterDesc = document.getElementById(
+ "protections-popup-trackers-blocked-counter-description"
+ );
+
+ // Check that whether the counter is not shown if the number of blocked
+ // trackers is zero.
+ ok(
+ BrowserTestUtils.is_hidden(trackerCounterBox),
+ "The blocked tracker counter is hidden if there is no blocked tracker."
+ );
+
+ await closeProtectionsPanel();
+
+ // Add one tracker into the database and check that the tracker counter is
+ // properly shown.
+ await addTrackerDataIntoDB(1);
+
+ // A promise for waiting the `showing` attributes has been set to the counter
+ // box. This means the database access is finished.
+ let counterShownPromise = BrowserTestUtils.waitForAttribute(
+ "showing",
+ trackerCounterBox
+ );
+
+ await openProtectionsPanel();
+ await counterShownPromise;
+
+ // Check that the number of blocked trackers is shown.
+ ok(
+ BrowserTestUtils.is_visible(trackerCounterBox),
+ "The blocked tracker counter is shown if there is one blocked tracker."
+ );
+ is(
+ trackerCounterDesc.textContent,
+ "1 Blocked",
+ "The blocked tracker counter is correct."
+ );
+
+ await closeProtectionsPanel();
+ await TrackingDBService.clearAll();
+
+ // Add trackers into the database and check that the tracker counter is
+ // properly shown as well as whether the pre-fetch is triggered by the
+ // keyboard navigation.
+ await addTrackerDataIntoDB(10);
+
+ // We cannot wait for the change of "showing" attribute here since this
+ // attribute will only be set if the previous counter is zero. Instead, we
+ // wait for the change of the text content of the counter.
+ let updateCounterPromise = new Promise(resolve => {
+ let mut = new MutationObserver(mutations => {
+ resolve();
+ mut.disconnect();
+ });
+
+ mut.observe(trackerCounterDesc, {
+ childList: true,
+ });
+ });
+
+ await openProtectionsPanelWithKeyNav();
+ await updateCounterPromise;
+
+ // Check that the number of blocked trackers is shown.
+ ok(
+ BrowserTestUtils.is_visible(trackerCounterBox),
+ "The blocked tracker counter is shown if there are more than one blocked tracker."
+ );
+ is(
+ trackerCounterDesc.textContent,
+ "10 Blocked",
+ "The blocked tracker counter is correct."
+ );
+
+ await closeProtectionsPanel();
+ await TrackingDBService.clearAll();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSubViewTelemetry() {
+ let items = [
+ ["protections-popup-category-trackers", "trackers"],
+ ["protections-popup-category-socialblock", "social"],
+ ["protections-popup-category-cookies", "cookies"],
+ ["protections-popup-category-cryptominers", "cryptominers"],
+ ["protections-popup-category-fingerprinters", "fingerprinters"],
+ ].map(item => [document.getElementById(item[0]), item[1]]);
+
+ for (let [item, telemetryId] of items) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://www.example.com", async () => {
+ await openProtectionsPanel();
+
+ item.classList.remove("notFound"); // Force visible for test
+ gProtectionsHandler._categoryItemOrderInvalidated = true;
+ gProtectionsHandler.reorderCategoryItems();
+
+ let viewShownEvent = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopupMultiView,
+ "ViewShown"
+ );
+ item.click();
+ let panelView = (await viewShownEvent).originalTarget;
+ checkClickTelemetry(telemetryId);
+ let prefsTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+ panelView.querySelector(".panel-subview-footer-button").click();
+ let prefsTab = await prefsTabPromise;
+ BrowserTestUtils.removeTab(prefsTab);
+ checkClickTelemetry("subview_settings", telemetryId);
+ });
+ }
+});
+
+/**
+ * A test to make sure the TP state won't apply incorrectly if we quickly switch
+ * tab after toggling the TP switch.
+ */
+add_task(async function testQuickSwitchTabAfterTogglingTPSwitch() {
+ const FIRST_TEST_SITE = "https://example.com/";
+ const SECOND_TEST_SITE = "https://example.org/";
+
+ // First, clear the tracking database.
+ await TrackingDBService.clearAll();
+
+ // Open two tabs with different origins.
+ let tabOne = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FIRST_TEST_SITE
+ );
+ let tabTwo = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SECOND_TEST_SITE
+ );
+
+ // Open the protection panel of the second tab.
+ await openProtectionsPanel();
+
+ // A promise to check the reload happens on the second tab.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tabTwo.linkedBrowser,
+ false,
+ SECOND_TEST_SITE
+ );
+
+ // Toggle the TP state and switch tab without waiting it to be finished.
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+ gBrowser.selectedTab = tabOne;
+
+ // Wait for the second tab to be reloaded.
+ await browserLoadedPromise;
+
+ // Check that the first tab is still with ETP enabled.
+ ok(
+ !ContentBlockingAllowList.includes(gBrowser.selectedBrowser),
+ "The ETP state of the first tab is still enabled."
+ );
+
+ // Check the ETP is disabled on the second origin.
+ ok(
+ ContentBlockingAllowList.includes(tabTwo.linkedBrowser),
+ "The ETP state of the second tab has been changed to disabled."
+ );
+
+ // Clean up the state of the allow list for the second tab.
+ ContentBlockingAllowList.remove(tabTwo.linkedBrowser);
+
+ BrowserTestUtils.removeTab(tabOne);
+ BrowserTestUtils.removeTab(tabTwo);
+
+ // Finally, clear the tracking database.
+ await TrackingDBService.clearAll();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_3.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_3.js
new file mode 100644
index 0000000000..353c6a099b
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_3.js
@@ -0,0 +1,224 @@
+/*
+ * Test that the Tracking Protection is correctly enabled / disabled
+ * in both normal and private windows given all possible states of the prefs:
+ * privacy.trackingprotection.enabled
+ * privacy.trackingprotection.pbmode.enabled
+ * privacy.trackingprotection.emailtracking.enabled
+ * privacy.trackingprotection.emailtracking.pbmode.enabled
+ * See also Bug 1178985, Bug 1819662.
+ */
+
+const PREF = "privacy.trackingprotection.enabled";
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const EMAIL_PREF = "privacy.trackingprotection.emailtracking.enabled";
+const EMAIL_PB_PREF = "privacy.trackingprotection.emailtracking.pbmode.enabled";
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF);
+ Services.prefs.clearUserPref(PB_PREF);
+ Services.prefs.clearUserPref(EMAIL_PREF);
+ Services.prefs.clearUserPref(EMAIL_PB_PREF);
+});
+
+add_task(async function testNormalBrowsing() {
+ let { TrackingProtection } =
+ gBrowser.ownerGlobal.gProtectionsHandler.blockers;
+ ok(
+ TrackingProtection,
+ "Normal window gProtectionsHandler should have TrackingProtection blocker."
+ );
+
+ Services.prefs.setBoolPref(PREF, true);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=true,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=true,EmailPB=true)"
+ );
+
+ Services.prefs.setBoolPref(PREF, false);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ !TrackingProtection.enabled,
+ "TP is disabled (ENABLED=false,PB=false,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(
+ !TrackingProtection.enabled,
+ "TP is disabled (ENABLED=false,PB=true,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=true,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=true,EmailEnabled=true,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ ok(
+ !TrackingProtection.enabled,
+ "TP is disabled (ENABLED=false,PB=true,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(
+ !TrackingProtection.enabled,
+ "TP is disabled (ENABLED=false,PB=false,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=false,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=false,EmailEnabled=true,EmailPB=true)"
+ );
+});
+
+add_task(async function testPrivateBrowsing() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let { TrackingProtection } =
+ privateWin.gBrowser.ownerGlobal.gProtectionsHandler.blockers;
+ ok(
+ TrackingProtection,
+ "Private window gProtectionsHandler should have TrackingProtection blocker."
+ );
+
+ Services.prefs.setBoolPref(PREF, true);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=true,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=true,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=true,PB=false,EmailEnabled=true,EmailPB=true)"
+ );
+
+ Services.prefs.setBoolPref(PREF, false);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ !TrackingProtection.enabled,
+ "TP is disabled (ENABLED=false,PB=false,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=true,EmailEnabled=false,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=true,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=true,EmailEnabled=true,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=true,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=false,EmailEnabled=false,EmailPB=true)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PREF, true);
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, false);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=false,EmailEnabled=true,EmailPB=false)"
+ );
+ Services.prefs.setBoolPref(EMAIL_PB_PREF, true);
+ ok(
+ TrackingProtection.enabled,
+ "TP is enabled (ENABLED=false,PB=false,EmailEnabled=true,EmailPB=true)"
+ );
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_background_tabs.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_background_tabs.js
new file mode 100644
index 0000000000..24a83c9588
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_background_tabs.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const BENIGN_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+
+add_task(async function testBackgroundTabs() {
+ info(
+ "Testing receiving and storing content blocking events in non-selected tabs."
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[TP_PREF, true]],
+ });
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BENIGN_PAGE);
+
+ let backgroundTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = backgroundTab.linkedBrowser;
+ let hasContentBlockingEvent = TestUtils.waitForCondition(
+ () => browser.getContentBlockingEvents() != 0
+ );
+ await promiseTabLoadEvent(backgroundTab, TRACKING_PAGE);
+ await hasContentBlockingEvent;
+
+ is(
+ browser.getContentBlockingEvents(),
+ Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+ "Background tab has the correct content blocking event."
+ );
+
+ is(
+ tab.linkedBrowser.getContentBlockingEvents(),
+ 0,
+ "Foreground tab has the correct content blocking event."
+ );
+
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+
+ is(
+ browser.getContentBlockingEvents(),
+ Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+ "Background tab still has the correct content blocking event."
+ );
+
+ is(
+ tab.linkedBrowser.getContentBlockingEvents(),
+ 0,
+ "Foreground tab still has the correct content blocking event."
+ );
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+
+ gBrowser.removeTab(backgroundTab);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_categories.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_categories.js
new file mode 100644
index 0000000000..b647b44d64
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_categories.js
@@ -0,0 +1,300 @@
+const CAT_PREF = "browser.contentblocking.category";
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TP_PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const TPC_PREF = "network.cookie.cookieBehavior";
+const CM_PREF = "privacy.trackingprotection.cryptomining.enabled";
+const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+const ST_PREF = "privacy.trackingprotection.socialtracking.enabled";
+const STC_PREF = "privacy.socialtracking.block_cookies.enabled";
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(CAT_PREF);
+ Services.prefs.clearUserPref(TP_PREF);
+ Services.prefs.clearUserPref(TP_PB_PREF);
+ Services.prefs.clearUserPref(TPC_PREF);
+ Services.prefs.clearUserPref(CM_PREF);
+ Services.prefs.clearUserPref(FP_PREF);
+ Services.prefs.clearUserPref(ST_PREF);
+ Services.prefs.clearUserPref(STC_PREF);
+});
+
+add_task(async function testCookieCategoryLabel() {
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.example.com",
+ async function () {
+ // Ensure the category nodes exist.
+ await openProtectionsPanel();
+ await closeProtectionsPanel();
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+ let categoryLabel = document.getElementById(
+ "protections-popup-cookies-category-label"
+ );
+
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ await TestUtils.waitForCondition(
+ () => !categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(!categoryItem.classList.contains("blocked"));
+
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_REJECT);
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingAll2.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingAll2.label"
+ )
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blocking3rdParty2.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blocking3rdParty2.label"
+ )
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ )
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ )
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN
+ );
+ await TestUtils.waitForCondition(
+ () => !categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(!categoryItem.classList.contains("blocked"));
+ }
+ );
+});
+
+let categoryEnabledPrefs = [TP_PREF, STC_PREF, TPC_PREF, CM_PREF, FP_PREF];
+
+let detectedStateFlags = [
+ Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+ Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT,
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED,
+ Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT,
+ Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT,
+];
+
+async function waitForClass(item, className, shouldBePresent = true) {
+ await TestUtils.waitForCondition(() => {
+ return item.classList.contains(className) == shouldBePresent;
+ }, `Target class ${className} should be ${shouldBePresent ? "present" : "not present"} on item ${item.id}`);
+
+ ok(
+ item.classList.contains(className) == shouldBePresent,
+ `item.classList.contains(${className}) is ${shouldBePresent} for ${item.id}`
+ );
+}
+
+add_task(async function testCategorySections() {
+ Services.prefs.setBoolPref(ST_PREF, true);
+
+ for (let pref of categoryEnabledPrefs) {
+ if (pref == TPC_PREF) {
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ } else {
+ Services.prefs.setBoolPref(pref, false);
+ }
+ }
+
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.example.com",
+ async function () {
+ // Ensure the category nodes exist.
+ await openProtectionsPanel();
+ await closeProtectionsPanel();
+
+ let categoryItems = [
+ "protections-popup-category-trackers",
+ "protections-popup-category-socialblock",
+ "protections-popup-category-cookies",
+ "protections-popup-category-cryptominers",
+ "protections-popup-category-fingerprinters",
+ ].map(id => document.getElementById(id));
+
+ for (let item of categoryItems) {
+ await waitForClass(item, "notFound");
+ await waitForClass(item, "blocked", false);
+ }
+
+ // For every item, we enable the category and spoof a content blocking event,
+ // and check that .notFound goes away and .blocked is set. Then we disable the
+ // category and checks that .blocked goes away, and .notFound is still unset.
+ let contentBlockingState = 0;
+ for (let i = 0; i < categoryItems.length; i++) {
+ let itemToTest = categoryItems[i];
+ let enabledPref = categoryEnabledPrefs[i];
+ contentBlockingState |= detectedStateFlags[i];
+ if (enabledPref == TPC_PREF) {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT
+ );
+ } else {
+ Services.prefs.setBoolPref(enabledPref, true);
+ }
+ gProtectionsHandler.onContentBlockingEvent(contentBlockingState);
+ gProtectionsHandler.updatePanelForBlockingEvent(contentBlockingState);
+ await waitForClass(itemToTest, "notFound", false);
+ await waitForClass(itemToTest, "blocked", true);
+ if (enabledPref == TPC_PREF) {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+ } else {
+ Services.prefs.setBoolPref(enabledPref, false);
+ }
+ await waitForClass(itemToTest, "notFound", false);
+ await waitForClass(itemToTest, "blocked", false);
+ }
+ }
+ );
+});
+
+/**
+ * Check that when we open the popup in a new window, the initial state is correct
+ * wrt the pref.
+ */
+add_task(async function testCategorySectionInitial() {
+ let categoryItems = [
+ "protections-popup-category-trackers",
+ "protections-popup-category-socialblock",
+ "protections-popup-category-cookies",
+ "protections-popup-category-cryptominers",
+ "protections-popup-category-fingerprinters",
+ ];
+ for (let i = 0; i < categoryItems.length; i++) {
+ for (let shouldBlock of [true, false]) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ // Open non-about: page so our protections are active.
+ await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "https://example.com/"
+ );
+ let enabledPref = categoryEnabledPrefs[i];
+ let contentBlockingState = detectedStateFlags[i];
+ if (enabledPref == TPC_PREF) {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ shouldBlock
+ ? Ci.nsICookieService.BEHAVIOR_REJECT
+ : Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+ } else {
+ Services.prefs.setBoolPref(enabledPref, shouldBlock);
+ }
+ win.gProtectionsHandler.onContentBlockingEvent(contentBlockingState);
+ await openProtectionsPanel(false, win);
+ let categoryItem = win.document.getElementById(categoryItems[i]);
+ let expectedFound = true;
+ // Accepting cookies outright won't mark this as found.
+ if (i == 2 && !shouldBlock) {
+ // See bug 1653019
+ expectedFound = false;
+ }
+ is(
+ categoryItem.classList.contains("notFound"),
+ !expectedFound,
+ `Should have found ${categoryItems[i]} when it was ${
+ shouldBlock ? "blocked" : "allowed"
+ }`
+ );
+ is(
+ categoryItem.classList.contains("blocked"),
+ shouldBlock,
+ `Should ${shouldBlock ? "have blocked" : "not have blocked"} ${
+ categoryItems[i]
+ }`
+ );
+ await closeProtectionsPanel(win);
+ await BrowserTestUtils.closeWindow(win);
+ }
+ }
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_cookie_banner.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookie_banner.js
new file mode 100644
index 0000000000..1d71f718fb
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookie_banner.js
@@ -0,0 +1,475 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the cookie banner handling section in the protections panel.
+ */
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const { MODE_DISABLED, MODE_REJECT, MODE_REJECT_OR_ACCEPT, MODE_UNSET } =
+ Ci.nsICookieBannerService;
+
+const exampleRules = JSON.stringify([
+ {
+ id: "4b18afb0-76db-4f9e-a818-ed9a783fae6a",
+ cookies: {},
+ click: {
+ optIn: "#foo",
+ presence: "#bar",
+ },
+ domains: ["example.com"],
+ },
+]);
+
+/**
+ * Determines whether the cookie banner section in the protections panel should
+ * be visible with the given configuration.
+ * @param {*} options - Configuration to test.
+ * @param {Number} options.featureMode - nsICookieBannerService::Modes value for
+ * normal browsing.
+ * @param {Number} options.featureModePBM - nsICookieBannerService::Modes value
+ * for private browsing.
+ * @param {boolean} options.visibilityPref - State of the cookie banner UI
+ * visibility pref.
+ * @param {boolean} options.testPBM - Whether the window is in private browsing
+ * mode (true) or not (false).
+ * @returns {boolean} Whether the section should be visible for the given
+ * config.
+ */
+function cookieBannerSectionIsVisible({
+ featureMode,
+ featureModePBM,
+ detectOnly,
+ visibilityPref,
+ testPBM,
+}) {
+ if (!visibilityPref) {
+ return false;
+ }
+ if (detectOnly) {
+ return false;
+ }
+
+ return (
+ (testPBM && featureModePBM != MODE_DISABLED) ||
+ (!testPBM && featureMode != MODE_DISABLED)
+ );
+}
+
+/**
+ * Runs a visibility test of the cookie banner section in the protections panel.
+ * @param {*} options - Test options.
+ * @param {Window} options.win - Browser window to use for testing. It's
+ * browsing mode should match the testPBM variable.
+ * @param {Number} options.featureMode - nsICookieBannerService::Modes value for
+ * normal browsing.
+ * @param {Number} options.featureModePBM - nsICookieBannerService::Modes value
+ * for private browsing.
+ * @param {boolean} options.visibilityPref - State of the cookie banner UI
+ * visibility pref.
+ * @param {boolean} options.testPBM - Whether the window is in private browsing
+ * mode (true) or not (false).
+ * @returns {Promise} Resolves once the test is complete.
+ */
+async function testSectionVisibility({
+ win,
+ featureMode,
+ featureModePBM,
+ visibilityPref,
+ testPBM,
+}) {
+ info(
+ "testSectionVisibility " +
+ JSON.stringify({ featureMode, featureModePBM, visibilityPref, testPBM })
+ );
+ // initialize the pref environment
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["cookiebanners.service.mode", featureMode],
+ ["cookiebanners.service.mode.privateBrowsing", featureModePBM],
+ ["cookiebanners.ui.desktop.enabled", visibilityPref],
+ ],
+ });
+
+ // Open a tab with example.com so the protections panel can be opened.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com" },
+ async () => {
+ await openProtectionsPanel(null, win);
+
+ // Get panel elements to test
+ let el = {
+ section: win.document.getElementById(
+ "protections-popup-cookie-banner-section"
+ ),
+ sectionSeparator: win.document.getElementById(
+ "protections-popup-cookie-banner-section-separator"
+ ),
+ switch: win.document.getElementById(
+ "protections-popup-cookie-banner-switch"
+ ),
+ };
+
+ let expectVisible = cookieBannerSectionIsVisible({
+ featureMode,
+ featureModePBM,
+ visibilityPref,
+ testPBM,
+ });
+ is(
+ BrowserTestUtils.is_visible(el.section),
+ expectVisible,
+ `Cookie banner section should be ${
+ expectVisible ? "visible" : "not visible"
+ }.`
+ );
+ is(
+ BrowserTestUtils.is_visible(el.sectionSeparator),
+ expectVisible,
+ `Cookie banner section separator should be ${
+ expectVisible ? "visible" : "not visible"
+ }.`
+ );
+ is(
+ BrowserTestUtils.is_visible(el.switch),
+ expectVisible,
+ `Cookie banner switch should be ${
+ expectVisible ? "visible" : "not visible"
+ }.`
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+}
+
+/**
+ * Tests cookie banner section visibility state in different configurations.
+ */
+add_task(async function test_section_visibility() {
+ // Test all combinations of cookie banner service modes and normal and
+ // private browsing.
+
+ for (let testPBM of [false, true]) {
+ let win = window;
+ // Open a new private window to test the panel in for testing PBM, otherwise
+ // reuse the existing window.
+ if (testPBM) {
+ win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ win.focus();
+ }
+
+ for (let featureMode of [
+ MODE_DISABLED,
+ MODE_REJECT,
+ MODE_REJECT_OR_ACCEPT,
+ ]) {
+ for (let featureModePBM of [
+ MODE_DISABLED,
+ MODE_REJECT,
+ MODE_REJECT_OR_ACCEPT,
+ ]) {
+ for (let detectOnly of [false, true]) {
+ // Testing detect only mode for normal browsing is sufficient.
+ if (detectOnly && featureModePBM != MODE_DISABLED) {
+ continue;
+ }
+ await testSectionVisibility({
+ win,
+ featureMode,
+ featureModePBM,
+ detectOnly,
+ testPBM,
+ visibilityPref: true,
+ });
+ }
+ }
+ }
+
+ if (testPBM) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+ }
+});
+
+/**
+ * Tests that the cookie banner section is only visible if enabled by UI pref.
+ */
+add_task(async function test_section_visibility_pref() {
+ for (let visibilityPref of [false, true]) {
+ await testSectionVisibility({
+ win: window,
+ featureMode: MODE_REJECT,
+ featureModePBM: MODE_DISABLED,
+ testPBM: false,
+ visibilityPref,
+ });
+ }
+});
+
+/**
+ * Test the state of the per-site exception switch in the cookie banner section
+ * and whether a matching per-site exception is set.
+ * @param {*} options
+ * @param {Window} options.win - Chrome window to test exception for (selected
+ * tab).
+ * @param {boolean} options.isPBM - Whether the given window is in private
+ * browsing mode.
+ * @param {string} options.expectedSwitchState - Whether the switch is expected to be
+ * "on" (CBH enabled), "off" (user added exception), or "unsupported" (no rules for site).
+ */
+function assertSwitchAndPrefState({ win, isPBM, expectedSwitchState }) {
+ let el = {
+ section: win.document.getElementById(
+ "protections-popup-cookie-banner-section"
+ ),
+ switch: win.document.getElementById(
+ "protections-popup-cookie-banner-switch"
+ ),
+ labelON: win.document.querySelector(
+ "#protections-popup-cookie-banner-detected"
+ ),
+ labelOFF: win.document.querySelector(
+ "#protections-popup-cookie-banner-site-disabled"
+ ),
+ labelUNDETECTED: win.document.querySelector(
+ "#protections-popup-cookie-banner-undetected"
+ ),
+ };
+
+ let currentURI = win.gBrowser.currentURI;
+ let pref = Services.cookieBanners.getDomainPref(currentURI, isPBM);
+ if (expectedSwitchState == "on") {
+ ok(el.section.dataset.state == "detected", "CBH switch is set to ON");
+
+ ok(BrowserTestUtils.is_visible(el.labelON), "ON label should be visible");
+ ok(
+ !BrowserTestUtils.is_visible(el.labelOFF),
+ "OFF label should not be visible"
+ );
+ ok(
+ !BrowserTestUtils.is_visible(el.labelUNDETECTED),
+ "UNDETECTED label should not be visible"
+ );
+
+ is(
+ pref,
+ MODE_UNSET,
+ `There should be no per-site exception for ${currentURI.spec}.`
+ );
+ } else if (expectedSwitchState === "off") {
+ ok(el.section.dataset.state == "site-disabled", "CBH switch is set to OFF");
+
+ ok(
+ !BrowserTestUtils.is_visible(el.labelON),
+ "ON label should not be visible"
+ );
+ ok(BrowserTestUtils.is_visible(el.labelOFF), "OFF label should be visible");
+ ok(
+ !BrowserTestUtils.is_visible(el.labelUNDETECTED),
+ "UNDETECTED label should not be visible"
+ );
+
+ is(
+ pref,
+ MODE_DISABLED,
+ `There should be a per-site exception for ${currentURI.spec}.`
+ );
+ } else {
+ ok(el.section.dataset.state == "undetected", "CBH not supported for site");
+
+ ok(
+ !BrowserTestUtils.is_visible(el.labelON),
+ "ON label should not be visible"
+ );
+ ok(
+ !BrowserTestUtils.is_visible(el.labelOFF),
+ "OFF label should not be visible"
+ );
+ ok(
+ BrowserTestUtils.is_visible(el.labelUNDETECTED),
+ "UNDETECTED label should be visible"
+ );
+ }
+}
+
+/**
+ * Test the telemetry associated with the cookie banner toggle. To be called
+ * after interacting with the toggle.
+ * @param {*} options
+ * @param {boolean|null} - Expected telemetry state matching the button state.
+ * button on = true = cookieb_toggle_on event. Pass null to expect no event
+ * recorded.
+ */
+function assertTelemetryState({ expectEnabled = null } = {}) {
+ info("Test telemetry state.");
+
+ let events = [];
+ const CATEGORY = "security.ui.protectionspopup";
+ const METHOD = "click";
+
+ if (expectEnabled != null) {
+ events.push({
+ category: CATEGORY,
+ method: METHOD,
+ object: expectEnabled ? "cookieb_toggle_on" : "cookieb_toggle_off",
+ });
+ }
+
+ // Assert event state and clear event list.
+ TelemetryTestUtils.assertEvents(events, {
+ category: CATEGORY,
+ method: METHOD,
+ });
+}
+
+/**
+ * Test the cookie banner enable / disable by clicking the switch, then
+ * clicking the on/off button in the cookie banner subview. Assumes the
+ * protections panel is already open.
+ *
+ * @param {boolean} enable - Whether we want to enable or disable.
+ * @param {Window} win - Current chrome window under test.
+ */
+async function toggleCookieBannerHandling(enable, win) {
+ let switchEl = win.document.getElementById(
+ "protections-popup-cookie-banner-switch"
+ );
+ let enableButton = win.document.getElementById(
+ "protections-popup-cookieBannerView-enable-button"
+ );
+ let disableButton = win.document.getElementById(
+ "protections-popup-cookieBannerView-disable-button"
+ );
+ let subView = win.document.getElementById(
+ "protections-popup-cookieBannerView"
+ );
+
+ let subViewShownPromise = BrowserTestUtils.waitForEvent(subView, "ViewShown");
+ switchEl.click();
+ await subViewShownPromise;
+
+ if (enable) {
+ ok(BrowserTestUtils.is_visible(enableButton), "Enable button is visible");
+ enableButton.click();
+ } else {
+ ok(BrowserTestUtils.is_visible(disableButton), "Disable button is visible");
+ disableButton.click();
+ }
+}
+
+function waitForProtectionsPopupHide(win = window) {
+ return BrowserTestUtils.waitForEvent(
+ win.document.getElementById("protections-popup"),
+ "popuphidden"
+ );
+}
+
+/**
+ * Tests the cookie banner section per-site preference toggle.
+ */
+add_task(async function test_section_toggle() {
+ // initialize the pref environment
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["cookiebanners.service.mode", MODE_REJECT_OR_ACCEPT],
+ ["cookiebanners.service.mode.privateBrowsing", MODE_REJECT_OR_ACCEPT],
+ ["cookiebanners.ui.desktop.enabled", true],
+ ["cookiebanners.listService.testRules", exampleRules],
+ ["cookiebanners.listService.testSkipRemoteSettings", true],
+ ],
+ });
+
+ Services.cookieBanners.resetRules(false);
+ await BrowserTestUtils.waitForCondition(
+ () => !!Services.cookieBanners.rules.length,
+ "waiting for Services.cookieBanners.rules.length to be greater than 0"
+ );
+
+ // Test both normal and private browsing windows. For normal windows we reuse
+ // the existing one, for private windows we need to open a new window.
+ for (let testPBM of [false, true]) {
+ let win = window;
+ if (testPBM) {
+ win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ }
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com" },
+ async () => {
+ let clearSiteDataSpy = sinon.spy(window.SiteDataManager, "remove");
+
+ await openProtectionsPanel(null, win);
+ let switchEl = win.document.getElementById(
+ "protections-popup-cookie-banner-switch"
+ );
+ info("Testing initial switch ON state.");
+ assertSwitchAndPrefState({
+ win,
+ isPBM: testPBM,
+ switchEl,
+ expectedSwitchState: "on",
+ });
+ assertTelemetryState();
+
+ info("Testing switch state after toggle OFF");
+ let closePromise = waitForProtectionsPopupHide(win);
+ await toggleCookieBannerHandling(false, win);
+ await closePromise;
+ if (testPBM) {
+ Assert.ok(
+ clearSiteDataSpy.notCalled,
+ "clearSiteData should not be called in private browsing mode"
+ );
+ } else {
+ Assert.ok(
+ clearSiteDataSpy.calledOnce,
+ "clearSiteData should be called in regular browsing mode"
+ );
+ }
+ clearSiteDataSpy.restore();
+
+ await openProtectionsPanel(null, win);
+ assertSwitchAndPrefState({
+ win,
+ isPBM: testPBM,
+ switchEl,
+ expectedSwitchState: "off",
+ });
+ assertTelemetryState({ expectEnabled: false });
+
+ info("Testing switch state after toggle ON.");
+ closePromise = waitForProtectionsPopupHide(win);
+ await toggleCookieBannerHandling(true, win);
+ await closePromise;
+
+ await openProtectionsPanel(null, win);
+ assertSwitchAndPrefState({
+ win,
+ isPBM: testPBM,
+ switchEl,
+ expectedSwitchState: "on",
+ });
+ assertTelemetryState({ expectEnabled: true });
+ }
+ );
+
+ if (testPBM) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js
new file mode 100644
index 0000000000..5eb16dbac7
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js
@@ -0,0 +1,537 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+const COOKIE_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+const CONTAINER_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/containerPage.html";
+
+const TPC_PREF = "network.cookie.cookieBehavior";
+
+add_setup(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+/*
+ * Accepts an array containing 6 elements that identify the testcase:
+ * [0] - boolean indicating whether trackers are blocked.
+ * [1] - boolean indicating whether third party cookies are blocked.
+ * [2] - boolean indicating whether first party cookies are blocked.
+ * [3] - integer indicating number of expected content blocking events.
+ * [4] - integer indicating number of expected subview list headers.
+ * [5] - integer indicating number of expected cookie list items.
+ * [6] - integer indicating number of expected cookie list items
+ * after loading a cookie-setting third party URL in an iframe
+ * [7] - integer indicating number of expected cookie list items
+ * after loading a cookie-setting first party URL in an iframe
+ */
+async function assertSitesListed(testCase) {
+ let sitesListedTestCases = [
+ [true, false, false, 4, 1, 1, 1, 1],
+ [true, true, false, 5, 1, 1, 2, 2],
+ [true, true, true, 6, 2, 2, 3, 3],
+ [false, false, false, 3, 1, 1, 1, 1],
+ ];
+ let [
+ trackersBlocked,
+ thirdPartyBlocked,
+ firstPartyBlocked,
+ contentBlockingEventCount,
+ listHeaderCount,
+ cookieItemsCount1,
+ cookieItemsCount2,
+ cookieItemsCount3,
+ ] = sitesListedTestCases[testCase];
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([
+ promise,
+ waitForContentBlockingEvent(contentBlockingEventCount),
+ ]);
+ let browser = tab.linkedBrowser;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listHeaders = cookiesView.querySelectorAll(
+ ".protections-popup-cookiesView-list-header"
+ );
+ is(
+ listHeaders.length,
+ listHeaderCount,
+ `We have ${listHeaderCount} list headers.`
+ );
+ if (listHeaderCount == 1) {
+ ok(
+ !BrowserTestUtils.is_visible(listHeaders[0]),
+ "Only one header, should be hidden"
+ );
+ } else {
+ for (let header of listHeaders) {
+ ok(
+ BrowserTestUtils.is_visible(header),
+ "Multiple list headers - all should be visible."
+ );
+ }
+ }
+
+ let emptyLabels = cookiesView.querySelectorAll(
+ ".protections-popup-empty-label"
+ );
+ is(emptyLabels.length, 0, `We have no empty labels`);
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(
+ listItems.length,
+ cookieItemsCount1,
+ `We have ${cookieItemsCount1} cookies in the list`
+ );
+
+ if (trackersBlocked) {
+ let trackerTestItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ if (label.value == "http://trackertest.org") {
+ trackerTestItem = item;
+ break;
+ }
+ }
+ ok(trackerTestItem, "Has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(trackerTestItem), "List item is visible");
+ }
+
+ if (firstPartyBlocked) {
+ let notTrackingExampleItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ if (label.value == "http://not-tracking.example.com") {
+ notTrackingExampleItem = item;
+ break;
+ }
+ }
+ ok(notTrackingExampleItem, "Has an item for not-tracking.example.com");
+ ok(
+ BrowserTestUtils.is_visible(notTrackingExampleItem),
+ "List item is visible"
+ );
+ }
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = cookiesView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ let change = waitForContentBlockingEvent();
+ let timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
+
+ await SpecialPowers.spawn(browser, [], function () {
+ content.postMessage("third-party-cookie", "*");
+ });
+
+ let result = await Promise.race([change, timeoutPromise]);
+ is(result, undefined, "No contentBlockingEvent events should be received");
+
+ viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ emptyLabels = cookiesView.querySelectorAll(".protections-popup-empty-label");
+ is(emptyLabels.length, 0, `We have no empty labels`);
+
+ listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(
+ listItems.length,
+ cookieItemsCount2,
+ `We have ${cookieItemsCount2} cookies in the list`
+ );
+
+ if (thirdPartyBlocked) {
+ let test1ExampleItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ if (label.value == "https://test1.example.org") {
+ test1ExampleItem = item;
+ break;
+ }
+ }
+ ok(test1ExampleItem, "Has an item for test1.example.org");
+ ok(BrowserTestUtils.is_visible(test1ExampleItem), "List item is visible");
+ }
+
+ if (trackersBlocked || thirdPartyBlocked || firstPartyBlocked) {
+ let trackerTestItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ if (label.value == "http://trackertest.org") {
+ trackerTestItem = item;
+ break;
+ }
+ }
+ ok(trackerTestItem, "List item should exist for http://trackertest.org");
+ ok(BrowserTestUtils.is_visible(trackerTestItem), "List item is visible");
+ }
+
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ change = waitForSecurityChange();
+ timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
+
+ await SpecialPowers.spawn(browser, [], function () {
+ content.postMessage("first-party-cookie", "*");
+ });
+
+ result = await Promise.race([change, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ emptyLabels = cookiesView.querySelectorAll(".protections-popup-empty-label");
+ is(emptyLabels.length, 0, "We have no empty labels");
+
+ listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(
+ listItems.length,
+ cookieItemsCount3,
+ `We have ${cookieItemsCount3} cookies in the list`
+ );
+
+ if (firstPartyBlocked) {
+ let notTrackingExampleItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ if (label.value == "http://not-tracking.example.com") {
+ notTrackingExampleItem = item;
+ break;
+ }
+ }
+ ok(notTrackingExampleItem, "Has an item for not-tracking.example.com");
+ ok(
+ BrowserTestUtils.is_visible(notTrackingExampleItem),
+ "List item is visible"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testCookiesSubView() {
+ info("Testing cookies subview with reject tracking cookies.");
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ let testCaseIndex = 0;
+ await assertSitesListed(testCaseIndex++);
+ info("Testing cookies subview with reject third party cookies.");
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+ await assertSitesListed(testCaseIndex++);
+ info("Testing cookies subview with reject all cookies.");
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_REJECT);
+ await assertSitesListed(testCaseIndex++);
+ info("Testing cookies subview with accept all cookies.");
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ await assertSitesListed(testCaseIndex++);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testCookiesSubViewAllowed() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://trackertest.org/"
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "cookie",
+ Services.perms.ALLOW_ACTION
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(3)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 cookie in the list");
+
+ let listItem = listItems[0];
+ let label = listItem.querySelector(".protections-popup-list-host-label");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(label.value, "http://trackertest.org", "has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "list item is visible");
+ ok(
+ listItem.classList.contains("allowed"),
+ "indicates whether the cookie was blocked or allowed"
+ );
+
+ let stateLabel = listItem.querySelector(
+ ".protections-popup-list-state-label"
+ );
+ ok(stateLabel, "List item has a state label");
+ ok(BrowserTestUtils.is_visible(stateLabel), "State label is visible");
+ is(
+ stateLabel.value,
+ gNavigatorBundle.getString("contentBlocking.cookiesView.allowed.label"),
+ "State label has correct text"
+ );
+
+ let button = listItem.querySelector(
+ ".permission-popup-permission-remove-button"
+ );
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Permission remove button is visible"
+ );
+ button.click();
+ is(
+ Services.perms.testExactPermissionFromPrincipal(principal, "cookie"),
+ Services.perms.UNKNOWN_ACTION,
+ "Button click should remove cookie pref."
+ );
+ ok(!listItem.classList.contains("allowed"), "Has removed the allowed class");
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testCookiesSubViewAllowedHeuristic() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://not-tracking.example.com/"
+ );
+
+ // Pretend that the tracker has already been interacted with
+ let trackerPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://trackertest.org/"
+ );
+ Services.perms.addFromPrincipal(
+ trackerPrincipal,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(5)]);
+ let browser = tab.linkedBrowser;
+
+ let popup;
+ let windowCreated = TestUtils.topicObserved(
+ "chrome-document-global-created",
+ (subject, data) => {
+ popup = subject;
+ return true;
+ }
+ );
+ let permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => {
+ return (
+ subject &&
+ subject.QueryInterface(Ci.nsIPermission).type ==
+ "3rdPartyStorage^http://trackertest.org" &&
+ subject.principal.origin == principal.origin &&
+ data == "added"
+ );
+ });
+
+ await SpecialPowers.spawn(browser, [], function () {
+ content.postMessage("window-open", "*");
+ });
+ await Promise.all([windowCreated, permChanged]);
+
+ await new Promise(resolve => waitForFocus(resolve, popup));
+ await new Promise(resolve => waitForFocus(resolve, window));
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 cookie in the list");
+
+ let listItem = listItems[0];
+ let label = listItem.querySelector(".protections-popup-list-host-label");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(label.value, "http://trackertest.org", "has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "list item is visible");
+ ok(
+ listItem.classList.contains("allowed"),
+ "indicates whether the cookie was blocked or allowed"
+ );
+
+ let button = listItem.querySelector(
+ ".permission-popup-permission-remove-button"
+ );
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Permission remove button is visible"
+ );
+ button.click();
+ is(
+ Services.perms.testExactPermissionFromPrincipal(
+ principal,
+ "3rdPartyStorage^http://trackertest.org"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Button click should remove the storage pref."
+ );
+ ok(!listItem.classList.contains("allowed"), "Has removed the allowed class");
+
+ await SpecialPowers.spawn(browser, [], function () {
+ content.postMessage("window-close", "*");
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testCookiesSubViewBlockedDoublyNested() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: CONTAINER_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(3)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 cookie in the list");
+
+ let listItem = listItems[0];
+ let label = listItem.querySelector(".protections-popup-list-host-label");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(label.value, "http://trackertest.org", "has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "list item is visible");
+ ok(
+ !listItem.classList.contains("allowed"),
+ "indicates whether the cookie was blocked or allowed"
+ );
+
+ let button = listItem.querySelector(
+ ".permission-popup-permission-remove-button"
+ );
+ ok(!button, "Permission remove button doesn't exist");
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_cryptominers.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_cryptominers.js
new file mode 100644
index 0000000000..13fb3e08ee
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cryptominers.js
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const CM_PROTECTION_PREF = "privacy.trackingprotection.cryptomining.enabled";
+let cmHistogram;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "urlclassifier.features.cryptomining.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ [
+ "urlclassifier.features.cryptomining.annotate.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", false],
+ ["privacy.trackingprotection.fingerprinting.enabled", false],
+ ["urlclassifier.features.fingerprinting.annotate.blacklistHosts", ""],
+ ],
+ });
+ cmHistogram = Services.telemetry.getHistogramById(
+ "CRYPTOMINERS_BLOCKED_COUNT"
+ );
+ registerCleanupFunction(() => {
+ cmHistogram.clear();
+ });
+});
+
+async function testIdentityState(hasException) {
+ cmHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ await openProtectionsPanel();
+
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "cryptominers are not detected"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible regardless the exception"
+ );
+
+ promise = waitForContentBlockingEvent();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("cryptomining", "*");
+ });
+
+ await promise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ hasException,
+ "Shows an exception when appropriate"
+ );
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ let loads = hasException ? 3 : 1;
+ testTelemetry(loads, 1, hasException);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testSubview(hasException) {
+ cmHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("cryptomining", "*");
+ });
+ await promise;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cryptominers"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ // We have to wait until the ContentBlockingLog gets updated in the content.
+ // Unfortunately, we need to use the setTimeout here since we don't have an
+ // easy to know whether the log is updated in the content. This should be
+ // removed after the log been removed in the content (Bug 1599046).
+ await new Promise(resolve => {
+ setTimeout(resolve, 500);
+ });
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let subview = document.getElementById("protections-popup-cryptominersView");
+ let viewShown = BrowserTestUtils.waitForEvent(subview, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ let trackersViewShimHint = document.getElementById(
+ "protections-popup-cryptominersView-shim-allow-hint"
+ );
+ ok(trackersViewShimHint.hidden, "Shim hint is hidden");
+
+ let listItems = subview.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 item in the list");
+ let listItem = listItems[0];
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.querySelector("label").value,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://cryptomining.example.com",
+ "Has the correct host"
+ );
+ is(
+ listItem.classList.contains("allowed"),
+ hasException,
+ "Indicates the miner was blocked or allowed"
+ );
+
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = subview.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ let loads = hasException ? 3 : 1;
+ testTelemetry(loads, 1, hasException);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testCategoryItem() {
+ Services.prefs.setBoolPref(CM_PROTECTION_PREF, false);
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cryptominers"
+ );
+
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ Services.prefs.setBoolPref(CM_PROTECTION_PREF, true);
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ Services.prefs.setBoolPref(CM_PROTECTION_PREF, false);
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ await closeProtectionsPanel();
+
+ promise = waitForContentBlockingEvent();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("cryptomining", "*");
+ });
+
+ await promise;
+
+ await openProtectionsPanel();
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ Services.prefs.setBoolPref(CM_PROTECTION_PREF, true);
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ Services.prefs.setBoolPref(CM_PROTECTION_PREF, false);
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ await closeProtectionsPanel();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+function testTelemetry(pagesVisited, pagesWithBlockableContent, hasException) {
+ let results = cmHistogram.snapshot();
+ Assert.equal(
+ results.values[0],
+ pagesVisited,
+ "The correct number of page loads have been recorded"
+ );
+ let expectedValue = hasException ? 2 : 1;
+ Assert.equal(
+ results.values[expectedValue],
+ pagesWithBlockableContent,
+ "The correct number of cryptominers have been recorded as blocked or allowed."
+ );
+}
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(CM_PROTECTION_PREF, true);
+
+ await testIdentityState(false);
+ await testIdentityState(true);
+
+ await testSubview(false);
+ await testSubview(true);
+
+ await testCategoryItem();
+
+ Services.prefs.clearUserPref(CM_PROTECTION_PREF);
+ Services.prefs.setStringPref("browser.contentblocking.category", "standard");
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_email_trackers_subview.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_email_trackers_subview.js
new file mode 100644
index 0000000000..6b97b83087
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_email_trackers_subview.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 1819662 - Testing the tracking category of the protection panel shows the
+ * email tracker domain if the email tracking protection is
+ * enabled
+ */
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const TEST_PAGE =
+ "https://www.example.com/browser/browser/base/content/test/protectionsUI/emailTrackingPage.html";
+const TEST_TRACKER_PAGE = "https://itisatracker.org/";
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const EMAIL_TP_PREF = "privacy.trackingprotection.emailtracking.enabled";
+
+/**
+ * A helper function to check whether or not an element has "notFound" class.
+ *
+ * @param {String} id The id of the testing element.
+ * @returns {Boolean} true when the element has "notFound" class.
+ */
+function notFound(id) {
+ return document.getElementById(id).classList.contains("notFound");
+}
+
+/**
+ * A helper function to test the protection UI tracker category.
+ *
+ * @param {Boolean} blocked - true if the email tracking protection is enabled.
+ */
+async function assertSitesListed(blocked) {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: TEST_PAGE,
+ gBrowser,
+ });
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-trackers"
+ );
+
+ if (!blocked) {
+ // The tracker category should have the 'notFound' class to indicate that
+ // no tracker was blocked in the page.
+ ok(
+ notFound("protections-popup-category-trackers"),
+ "Tracker category is not found"
+ );
+
+ ok(
+ !BrowserTestUtils.is_visible(categoryItem),
+ "TP category item is not visible"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ return;
+ }
+
+ // Testing if the tracker category is visible.
+
+ // Explicitly waiting for the category item becoming visible.
+ await BrowserTestUtils.waitForMutationCondition(categoryItem, {}, () =>
+ BrowserTestUtils.is_visible(categoryItem)
+ );
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+
+ // Click the tracker category and wait until the tracker view is shown.
+ let trackersView = document.getElementById("protections-popup-trackersView");
+ let viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Trackers view was shown");
+
+ // Ensure the email tracker is listed on the tracker list.
+ let listItems = Array.from(
+ trackersView.querySelectorAll(".protections-popup-list-item")
+ );
+ is(listItems.length, 1, "We have 1 trackers in the list");
+
+ let listItem = listItems.find(
+ item =>
+ item.querySelector("label").value == "https://email-tracking.example.org"
+ );
+ ok(listItem, "Has an item for email-tracking.example.org");
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+
+ // Back to the popup main view.
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = trackersView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ // Add an iframe to a tracker domain and wait until the content event files.
+ let contentBlockingEventPromise = waitForContentBlockingEvent(1);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_TRACKER_PAGE],
+ test_url => {
+ let ifr = content.document.createElement("iframe");
+
+ content.document.body.appendChild(ifr);
+ ifr.src = test_url;
+ }
+ );
+ await contentBlockingEventPromise;
+
+ // Click the tracker category again.
+ viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ // Ensure both the email tracker and the tracker are listed on the tracker
+ // list.
+ listItems = Array.from(
+ trackersView.querySelectorAll(".protections-popup-list-item")
+ );
+ is(listItems.length, 2, "We have 2 trackers in the list");
+
+ listItem = listItems.find(
+ item =>
+ item.querySelector("label").value == "https://email-tracking.example.org"
+ );
+ ok(listItem, "Has an item for email-tracking.example.org");
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+
+ listItem = listItems.find(
+ item => item.querySelector("label").value == "https://itisatracker.org"
+ );
+ ok(listItem, "Has an item for itisatracker.org");
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TP_PREF);
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+add_task(async function testTrackersSubView() {
+ info("Testing trackers subview with TP disabled.");
+ Services.prefs.setBoolPref(EMAIL_TP_PREF, false);
+ await assertSitesListed(false);
+ info("Testing trackers subview with TP enabled.");
+ Services.prefs.setBoolPref(EMAIL_TP_PREF, true);
+ await assertSitesListed(true);
+ info("Testing trackers subview with TP enabled and a CB exception.");
+ let uri = Services.io.newURI("https://www.example.com");
+ PermissionTestUtils.add(
+ uri,
+ "trackingprotection",
+ Services.perms.ALLOW_ACTION
+ );
+ await assertSitesListed(false);
+ info("Testing trackers subview with TP enabled and a CB exception removed.");
+ PermissionTestUtils.remove(uri, "trackingprotection");
+ await assertSitesListed(true);
+
+ Services.prefs.clearUserPref(EMAIL_TP_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js
new file mode 100644
index 0000000000..639d8982fc
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js
@@ -0,0 +1,39 @@
+const URL =
+ "http://mochi.test:8888/browser/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html";
+
+add_task(async function test_fetch() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.trackingprotection.enabled", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (newTabBrowser) {
+ let contentBlockingEvent = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(newTabBrowser, [], async function () {
+ await content.wrappedJSObject
+ .test_fetch()
+ .then(response => Assert.ok(false, "should have denied the request"))
+ .catch(e => Assert.ok(true, `Caught exception: ${e}`));
+ });
+ await contentBlockingEvent;
+
+ let gProtectionsHandler = newTabBrowser.ownerGlobal.gProtectionsHandler;
+ ok(gProtectionsHandler, "got CB object");
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "has detected content blocking"
+ );
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("active"),
+ "icon box is active"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ gNavigatorBundle.getString("trackingProtection.icon.activeTooltip2"),
+ "correct tooltip"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js
new file mode 100644
index 0000000000..aaa6745628
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js
@@ -0,0 +1,303 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const FP_PROTECTION_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+let fpHistogram;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "urlclassifier.features.fingerprinting.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ [
+ "urlclassifier.features.fingerprinting.annotate.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", false],
+ ["privacy.trackingprotection.cryptomining.enabled", false],
+ ["urlclassifier.features.cryptomining.annotate.blacklistHosts", ""],
+ ["urlclassifier.features.cryptomining.annotate.blacklistTables", ""],
+ ],
+ });
+ fpHistogram = Services.telemetry.getHistogramById(
+ "FINGERPRINTERS_BLOCKED_COUNT"
+ );
+ registerCleanupFunction(() => {
+ fpHistogram.clear();
+ });
+});
+
+async function testIdentityState(hasException) {
+ fpHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ await openProtectionsPanel();
+
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "fingerprinters are not detected"
+ );
+ ok(
+ !BrowserTestUtils.is_hidden(gProtectionsHandler.iconBox),
+ "icon box is visible regardless the exception"
+ );
+
+ promise = waitForContentBlockingEvent();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("fingerprinting", "*");
+ });
+
+ await promise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ hasException,
+ "Shows an exception when appropriate"
+ );
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ let loads = hasException ? 3 : 1;
+ testTelemetry(loads, 1, hasException);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testCategoryItem() {
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, false);
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ await openProtectionsPanel();
+ let categoryItem = document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, true);
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, false);
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ await closeProtectionsPanel();
+
+ promise = waitForContentBlockingEvent();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("fingerprinting", "*");
+ });
+
+ await promise;
+
+ await openProtectionsPanel();
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, true);
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, false);
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ await closeProtectionsPanel();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testSubview(hasException) {
+ fpHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("fingerprinting", "*");
+ });
+ await promise;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ // We have to wait until the ContentBlockingLog gets updated in the content.
+ // Unfortunately, we need to use the setTimeout here since we don't have an
+ // easy to know whether the log is updated in the content. This should be
+ // removed after the log been removed in the content (Bug 1599046).
+ await new Promise(resolve => {
+ setTimeout(resolve, 500);
+ });
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let subview = document.getElementById("protections-popup-fingerprintersView");
+ let viewShown = BrowserTestUtils.waitForEvent(subview, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ let trackersViewShimHint = document.getElementById(
+ "protections-popup-fingerprintersView-shim-allow-hint"
+ );
+ ok(trackersViewShimHint.hidden, "Shim hint is hidden");
+
+ let listItems = subview.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 item in the list");
+ let listItem = listItems[0];
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.querySelector("label").value,
+ "https://fingerprinting.example.com",
+ "Has the correct host"
+ );
+ is(
+ listItem.classList.contains("allowed"),
+ hasException,
+ "Indicates the fingerprinter was blocked or allowed"
+ );
+
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = subview.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ let loads = hasException ? 3 : 1;
+ testTelemetry(loads, 1, hasException);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+function testTelemetry(pagesVisited, pagesWithBlockableContent, hasException) {
+ let results = fpHistogram.snapshot();
+ Assert.equal(
+ results.values[0],
+ pagesVisited,
+ "The correct number of page loads have been recorded"
+ );
+ let expectedValue = hasException ? 2 : 1;
+ Assert.equal(
+ results.values[expectedValue],
+ pagesWithBlockableContent,
+ "The correct number of fingerprinters have been recorded as blocked or allowed."
+ );
+}
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, true);
+
+ await testIdentityState(false);
+ await testIdentityState(true);
+
+ await testSubview(false);
+ await testSubview(true);
+
+ await testCategoryItem();
+
+ Services.prefs.clearUserPref(FP_PROTECTION_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_icon_state.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_icon_state.js
new file mode 100644
index 0000000000..187a777850
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_icon_state.js
@@ -0,0 +1,223 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/*
+ * Test that the Content Blocking icon state is properly updated in the identity
+ * block when loading tabs and switching between tabs.
+ * See also Bug 1175858.
+ */
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TP_PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+const NCB_PREF = "network.cookie.cookieBehavior";
+const BENIGN_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const COOKIE_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+
+registerCleanupFunction(function () {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.prefs.clearUserPref(TP_PREF);
+ Services.prefs.clearUserPref(TP_PB_PREF);
+ Services.prefs.clearUserPref(NCB_PREF);
+ Services.prefs.clearUserPref(DTSCBN_PREF);
+});
+
+async function testTrackingProtectionIconState(tabbrowser) {
+ Services.prefs.setBoolPref(DTSCBN_PREF, true);
+
+ info("Load a test page not containing tracking elements");
+ let benignTab = await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ BENIGN_PAGE
+ );
+ let gProtectionsHandler = tabbrowser.ownerGlobal.gProtectionsHandler;
+
+ ok(!gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox not active");
+
+ info("Load a test page containing tracking elements");
+ let trackingTab = await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ TRACKING_PAGE
+ );
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Load a test page containing tracking cookies");
+ let trackingCookiesTab = await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ COOKIE_PAGE
+ );
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Switch from tracking cookie -> benign tab");
+ let securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ tabbrowser.selectedTab = benignTab;
+ await securityChanged;
+
+ ok(!gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox not active");
+
+ info("Switch from benign -> tracking tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ tabbrowser.selectedTab = trackingTab;
+ await securityChanged;
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Switch from tracking -> tracking cookies tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ tabbrowser.selectedTab = trackingCookiesTab;
+ await securityChanged;
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Reload tracking cookies tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ let contentBlockingEvent = waitForContentBlockingEvent(
+ 2,
+ tabbrowser.ownerGlobal
+ );
+ tabbrowser.reload();
+ await Promise.all([securityChanged, contentBlockingEvent]);
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Reload tracking tab");
+ securityChanged = waitForSecurityChange(2, tabbrowser.ownerGlobal);
+ contentBlockingEvent = waitForContentBlockingEvent(3, tabbrowser.ownerGlobal);
+ tabbrowser.selectedTab = trackingTab;
+ tabbrowser.reload();
+ await Promise.all([securityChanged, contentBlockingEvent]);
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Inject tracking cookie inside tracking tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
+ await SpecialPowers.spawn(tabbrowser.selectedBrowser, [], function () {
+ content.postMessage("cookie", "*");
+ });
+ let result = await Promise.race([securityChanged, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Inject tracking element inside tracking tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
+ await SpecialPowers.spawn(tabbrowser.selectedBrowser, [], function () {
+ content.postMessage("tracking", "*");
+ });
+ result = await Promise.race([securityChanged, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ tabbrowser.selectedTab = trackingCookiesTab;
+
+ info("Inject tracking cookie inside tracking cookies tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
+ await SpecialPowers.spawn(tabbrowser.selectedBrowser, [], function () {
+ content.postMessage("cookie", "*");
+ });
+ result = await Promise.race([securityChanged, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ info("Inject tracking element inside tracking cookies tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
+ await SpecialPowers.spawn(tabbrowser.selectedBrowser, [], function () {
+ content.postMessage("tracking", "*");
+ });
+ result = await Promise.race([securityChanged, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+
+ while (tabbrowser.tabs.length > 1) {
+ tabbrowser.removeCurrentTab();
+ }
+}
+
+add_task(async function testNormalBrowsing() {
+ await SpecialPowers.pushPrefEnv({ set: [[APS_PREF, false]] });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let gProtectionsHandler = gBrowser.ownerGlobal.gProtectionsHandler;
+ ok(
+ gProtectionsHandler,
+ "gProtectionsHandler is attached to the browser window"
+ );
+
+ let { TrackingProtection } =
+ gBrowser.ownerGlobal.gProtectionsHandler.blockers;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ let { ThirdPartyCookies } = gBrowser.ownerGlobal.gProtectionsHandler.blockers;
+ ok(ThirdPartyCookies, "TPC is attached to the browser window");
+
+ Services.prefs.setBoolPref(TP_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+ Services.prefs.setIntPref(
+ NCB_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ ok(
+ ThirdPartyCookies.enabled,
+ "ThirdPartyCookies is enabled after setting the pref"
+ );
+
+ await testTrackingProtectionIconState(gBrowser);
+});
+
+add_task(async function testPrivateBrowsing() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [APS_PREF, false],
+ ["dom.security.https_first_pbm", false],
+ ],
+ });
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let tabbrowser = privateWin.gBrowser;
+
+ let gProtectionsHandler = tabbrowser.ownerGlobal.gProtectionsHandler;
+ ok(
+ gProtectionsHandler,
+ "gProtectionsHandler is attached to the private window"
+ );
+ let { TrackingProtection } =
+ tabbrowser.ownerGlobal.gProtectionsHandler.blockers;
+ ok(TrackingProtection, "TP is attached to the private window");
+ let { ThirdPartyCookies } =
+ tabbrowser.ownerGlobal.gProtectionsHandler.blockers;
+ ok(ThirdPartyCookies, "TPC is attached to the browser window");
+
+ Services.prefs.setBoolPref(TP_PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+ Services.prefs.setIntPref(
+ NCB_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ ok(
+ ThirdPartyCookies.enabled,
+ "ThirdPartyCookies is enabled after setting the pref"
+ );
+
+ await testTrackingProtectionIconState(tabbrowser);
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js
new file mode 100644
index 0000000000..9909e5b876
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Hide protections cards so as not to trigger more async messaging
+ // when landing on the page.
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["browser.contentblocking.cfr-milestone.update-interval", 0],
+ ],
+ });
+});
+
+add_task(async function doTest() {
+ // This also ensures that the DB tables have been initialized.
+ await TrackingDBService.clearAll();
+
+ let milestones = JSON.parse(
+ Services.prefs.getStringPref(
+ "browser.contentblocking.cfr-milestone.milestones"
+ )
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ for (let milestone of milestones) {
+ Services.telemetry.clearEvents();
+ // Trigger the milestone feature.
+ Services.prefs.setIntPref(
+ "browser.contentblocking.cfr-milestone.milestone-achieved",
+ milestone
+ );
+ await TestUtils.waitForCondition(
+ () => gProtectionsHandler._milestoneTextSet
+ );
+ // We set the shown-time pref to pretend that the CFR has been
+ // shown, so that we can test the panel.
+ // TODO: Full integration test for robustness.
+ Services.prefs.setStringPref(
+ "browser.contentblocking.cfr-milestone.milestone-shown-time",
+ Date.now().toString()
+ );
+ await openProtectionsPanel();
+
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupMilestonesText
+ ),
+ "Milestones section should be visible in the panel."
+ );
+
+ await closeProtectionsPanel();
+ await openProtectionsPanel();
+
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupMilestonesText
+ ),
+ "Milestones section should still be visible in the panel."
+ );
+
+ let newTabPromise = waitForAboutProtectionsTab();
+ await EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("protections-popup-milestones-content"),
+ {}
+ );
+ let protectionsTab = await newTabPromise;
+
+ ok(true, "about:protections has been opened as expected.");
+
+ BrowserTestUtils.removeTab(protectionsTab);
+
+ await openProtectionsPanel();
+
+ ok(
+ !BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupMilestonesText
+ ),
+ "Milestones section should no longer be visible in the panel."
+ );
+
+ checkClickTelemetry("milestone_message");
+
+ await closeProtectionsPanel();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await TrackingDBService.clearAll();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js
new file mode 100644
index 0000000000..c50e93b9d2
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TPC_PREF = "network.cookie.cookieBehavior";
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const COOKIE_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.com/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+
+async function waitAndAssertPreferencesShown(_spotlight) {
+ await BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ await TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#privacy",
+ "Should open about:preferences."
+ );
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [_spotlight],
+ async spotlight => {
+ let doc = content.document;
+ let section = await ContentTaskUtils.waitForCondition(
+ () => doc.querySelector(".spotlight"),
+ "The spotlight should appear."
+ );
+ Assert.equal(
+ section.getAttribute("data-subcategory"),
+ spotlight,
+ "The correct section is spotlighted."
+ );
+ }
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_setup(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+// Tests that pressing the preferences button in the trackers subview
+// links to about:preferences
+add_task(async function testOpenPreferencesFromTrackersSubview() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+
+ // Wait for 2 content blocking events - one for the load and one for the tracker.
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(2)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-trackers"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let trackersView = document.getElementById("protections-popup-trackersView");
+ let viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Trackers view was shown");
+
+ let preferencesButton = document.getElementById(
+ "protections-popup-trackersView-settings-button"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(preferencesButton),
+ "The preferences button is shown."
+ );
+
+ let shown = waitAndAssertPreferencesShown("trackingprotection");
+ preferencesButton.click();
+ await shown;
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+// Tests that pressing the preferences button in the cookies subview
+// links to about:preferences
+add_task(async function testOpenPreferencesFromCookiesSubview() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+
+ // Wait for 2 content blocking events - one for the load and one for the tracker.
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(2)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let preferencesButton = document.getElementById(
+ "protections-popup-cookiesView-settings-button"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(preferencesButton),
+ "The preferences button is shown."
+ );
+
+ let shown = waitAndAssertPreferencesShown("trackingprotection");
+ preferencesButton.click();
+ await shown;
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js
new file mode 100644
index 0000000000..ebe67ea0c0
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that sites added to the Tracking Protection whitelist in private
+// browsing mode don't persist once the private browsing window closes.
+
+const TP_PB_PREF = "privacy.trackingprotection.enabled";
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+var TrackingProtection = null;
+var gProtectionsHandler = null;
+var browser = null;
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(TP_PB_PREF);
+ Services.prefs.clearUserPref(DTSCBN_PREF);
+ gProtectionsHandler = TrackingProtection = browser = null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+function hidden(sel) {
+ let win = browser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ let display = win.getComputedStyle(el).getPropertyValue("display", null);
+ return display === "none";
+}
+
+function protectionsPopupState() {
+ let win = browser.ownerGlobal;
+ return win.document.getElementById("protections-popup")?.state || "closed";
+}
+
+function clickButton(sel) {
+ let win = browser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ el.doCommand();
+}
+
+function testTrackingPage() {
+ info("Tracking content must be blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(!gProtectionsHandler.hasException, "content shows no exception");
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "icon box shows no exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ gNavigatorBundle.getString("trackingProtection.icon.activeTooltip2"),
+ "correct tooltip"
+ );
+}
+
+function testTrackingPageUnblocked() {
+ info("Tracking content must be allowlisted and not blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(gProtectionsHandler.hasException, "content shows exception");
+
+ ok(!gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "shield shows exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ gNavigatorBundle.getString("trackingProtection.icon.disabledTooltip2"),
+ "correct tooltip"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+}
+
+add_task(async function testExceptionAddition() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first_pbm", false]],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+ Services.prefs.setBoolPref(DTSCBN_PREF, true);
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ browser = privateWin.gBrowser;
+ let tab = await BrowserTestUtils.openNewForegroundTab(browser);
+
+ gProtectionsHandler = browser.ownerGlobal.gProtectionsHandler;
+ ok(gProtectionsHandler, "CB is attached to the private window");
+
+ TrackingProtection =
+ browser.ownerGlobal.gProtectionsHandler.blockers.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+
+ Services.prefs.setBoolPref(TP_PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ info("Load a test page containing tracking elements");
+ await Promise.all([
+ promiseTabLoadEvent(tab, TRACKING_PAGE),
+ waitForContentBlockingEvent(2, tab.ownerGlobal),
+ ]);
+
+ testTrackingPage(tab.ownerGlobal);
+
+ info("Disable TP for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ gProtectionsHandler.disableForCurrentPage();
+ is(protectionsPopupState(), "closed", "protections popup is closed");
+
+ await tabReloadPromise;
+ testTrackingPageUnblocked();
+
+ info(
+ "Test that the exception is remembered across tabs in the same private window"
+ );
+ tab = browser.selectedTab = BrowserTestUtils.addTab(browser);
+
+ info("Load a test page containing tracking elements");
+ await promiseTabLoadEvent(tab, TRACKING_PAGE);
+ testTrackingPageUnblocked();
+
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function testExceptionPersistence() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first_pbm", false]],
+ });
+
+ info("Open another private browsing window");
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ browser = privateWin.gBrowser;
+ let tab = await BrowserTestUtils.openNewForegroundTab(browser);
+
+ gProtectionsHandler = browser.ownerGlobal.gProtectionsHandler;
+ ok(gProtectionsHandler, "CB is attached to the private window");
+ TrackingProtection =
+ browser.ownerGlobal.gProtectionsHandler.blockers.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+
+ ok(TrackingProtection.enabled, "TP is still enabled");
+
+ info("Load a test page containing tracking elements");
+ await Promise.all([
+ promiseTabLoadEvent(tab, TRACKING_PAGE),
+ waitForContentBlockingEvent(2, tab.ownerGlobal),
+ ]);
+
+ testTrackingPage(tab.ownerGlobal);
+
+ info("Disable TP for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ gProtectionsHandler.disableForCurrentPage();
+ is(protectionsPopupState(), "closed", "protections popup is closed");
+
+ await Promise.all([
+ tabReloadPromise,
+ waitForContentBlockingEvent(2, tab.ownerGlobal),
+ ]);
+ testTrackingPageUnblocked();
+
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js
new file mode 100644
index 0000000000..b5400954db
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js
@@ -0,0 +1,404 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const BENIGN_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const COOKIE_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+
+const CM_PREF = "privacy.trackingprotection.cryptomining.enabled";
+const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+const TP_PREF = "privacy.trackingprotection.enabled";
+const CB_PREF = "network.cookie.cookieBehavior";
+
+const PREF_REPORT_BREAKAGE_URL = "browser.contentblocking.reportBreakage.url";
+
+let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+let { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+let { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+add_setup(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ // Clear prefs that are touched in this test again for sanity.
+ Services.prefs.clearUserPref(TP_PREF);
+ Services.prefs.clearUserPref(CB_PREF);
+ Services.prefs.clearUserPref(FP_PREF);
+ Services.prefs.clearUserPref(CM_PREF);
+ Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_URL);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "urlclassifier.features.fingerprinting.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ [
+ "urlclassifier.features.fingerprinting.annotate.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ ["privacy.trackingprotection.cryptomining.enabled", true],
+ [
+ "urlclassifier.features.cryptomining.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ [
+ "urlclassifier.features.cryptomining.annotate.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ ],
+ });
+});
+
+add_task(async function testReportBreakageCancel() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function () {
+ await openProtectionsPanel();
+ await TestUtils.waitForCondition(() =>
+ gProtectionsHandler._protectionsPopup.hasAttribute("blocking")
+ );
+
+ let siteNotWorkingButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-link"
+ );
+ ok(
+ BrowserTestUtils.is_visible(siteNotWorkingButton),
+ "site not working button is visible"
+ );
+ let siteNotWorkingView = document.getElementById(
+ "protections-popup-siteNotWorkingView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ siteNotWorkingView,
+ "ViewShown"
+ );
+ siteNotWorkingButton.click();
+ await viewShown;
+
+ let sendReportButton = document.getElementById(
+ "protections-popup-siteNotWorkingView-sendReport"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ sendReportButton.click();
+ await viewShown;
+
+ ok(true, "Report breakage view was shown");
+
+ viewShown = BrowserTestUtils.waitForEvent(siteNotWorkingView, "ViewShown");
+ let cancelButton = document.getElementById(
+ "protections-popup-sendReportView-cancel"
+ );
+ cancelButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testReportBreakageSiteException() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+
+ await BrowserTestUtils.withNewTab(url, async browser => {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false);
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+
+ await openProtectionsPanel();
+
+ let siteFixedButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-fixed-link"
+ );
+ ok(
+ BrowserTestUtils.is_visible(siteFixedButton),
+ "site fixed button is visible"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ siteFixedButton.click();
+ await viewShown;
+
+ ok(true, "Report breakage view was shown");
+
+ await testReportBreakageSubmit(
+ TRACKING_PAGE,
+ "trackingprotection",
+ false,
+ true
+ );
+
+ // Pass false for shouldReload - there's no need since the tab is going away.
+ gProtectionsHandler.enableForCurrentPage(false);
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testNoTracking() {
+ await BrowserTestUtils.withNewTab(BENIGN_PAGE, async function () {
+ await openProtectionsPanel();
+
+ let siteNotWorkingButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-link"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(siteNotWorkingButton),
+ "site not working button is not visible"
+ );
+ });
+});
+
+add_task(async function testReportBreakageError() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function () {
+ await openAndTestReportBreakage(TRACKING_PAGE, "trackingprotection", true);
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testTP() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function () {
+ await openAndTestReportBreakage(TRACKING_PAGE, "trackingprotection");
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testCR() {
+ Services.prefs.setIntPref(
+ CB_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ // Make sure that we correctly strip the query.
+ let url = COOKIE_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function () {
+ await openAndTestReportBreakage(COOKIE_PAGE, "cookierestrictions");
+ });
+
+ Services.prefs.clearUserPref(CB_PREF);
+});
+
+add_task(async function testFP() {
+ Services.prefs.setIntPref(CB_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ Services.prefs.setBoolPref(FP_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(browser, [], function () {
+ content.postMessage("fingerprinting", "*");
+ });
+ await promise;
+
+ await openAndTestReportBreakage(TRACKING_PAGE, "fingerprinting", true);
+ });
+
+ Services.prefs.clearUserPref(FP_PREF);
+ Services.prefs.clearUserPref(CB_PREF);
+});
+
+add_task(async function testCM() {
+ Services.prefs.setIntPref(CB_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ Services.prefs.setBoolPref(CM_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(browser, [], function () {
+ content.postMessage("cryptomining", "*");
+ });
+ await promise;
+
+ await openAndTestReportBreakage(TRACKING_PAGE, "cryptomining", true);
+ });
+
+ Services.prefs.clearUserPref(CM_PREF);
+ Services.prefs.clearUserPref(CB_PREF);
+});
+
+async function openAndTestReportBreakage(url, tags, error = false) {
+ await openProtectionsPanel();
+
+ let siteNotWorkingButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-link"
+ );
+ ok(
+ BrowserTestUtils.is_visible(siteNotWorkingButton),
+ "site not working button is visible"
+ );
+ let siteNotWorkingView = document.getElementById(
+ "protections-popup-siteNotWorkingView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ siteNotWorkingView,
+ "ViewShown"
+ );
+ siteNotWorkingButton.click();
+ await viewShown;
+
+ let sendReportButton = document.getElementById(
+ "protections-popup-siteNotWorkingView-sendReport"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ sendReportButton.click();
+ await viewShown;
+
+ ok(true, "Report breakage view was shown");
+
+ await testReportBreakageSubmit(url, tags, error, false);
+}
+
+// This function assumes that the breakage report view is ready.
+async function testReportBreakageSubmit(url, tags, error, hasException) {
+ // Setup a mock server for receiving breakage reports.
+ let server = new HttpServer();
+ server.start(-1);
+ let i = server.identity;
+ let path =
+ i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort + "/";
+
+ Services.prefs.setStringPref(PREF_REPORT_BREAKAGE_URL, path);
+
+ let comments = document.getElementById(
+ "protections-popup-sendReportView-collection-comments"
+ );
+ is(comments.value, "", "Comments textarea should initially be empty");
+
+ let submitButton = document.getElementById(
+ "protections-popup-sendReportView-submit"
+ );
+ let reportURL = document.getElementById(
+ "protections-popup-sendReportView-collection-url"
+ ).value;
+
+ is(reportURL, url, "Shows the correct URL in the report UI.");
+
+ // Make sure that sending the report closes the identity popup.
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+
+ // Check that we're receiving a good report.
+ await new Promise(resolve => {
+ server.registerPathHandler("/", async (request, response) => {
+ is(request.method, "POST", "request was a post");
+
+ // Extract and "parse" the form data in the request body.
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let boundary = request
+ .getHeader("Content-Type")
+ .match(/boundary=-+([^-]*)/i)[1];
+ let regex = new RegExp("-+" + boundary + "-*\\s+");
+ let sections = body.split(regex);
+
+ let prefs = [
+ "privacy.trackingprotection.enabled",
+ "privacy.trackingprotection.pbmode.enabled",
+ "urlclassifier.trackingTable",
+ "network.http.referer.defaultPolicy",
+ "network.http.referer.defaultPolicy.pbmode",
+ "network.cookie.cookieBehavior",
+ "privacy.annotate_channels.strict_list.enabled",
+ "privacy.restrict3rdpartystorage.expiration",
+ "privacy.trackingprotection.fingerprinting.enabled",
+ "privacy.trackingprotection.cryptomining.enabled",
+ ];
+ let prefsBody = "";
+
+ for (let pref of prefs) {
+ prefsBody += `${pref}: ${Preferences.get(pref)}\r\n`;
+ }
+
+ Assert.deepEqual(
+ sections,
+ [
+ "",
+ `Content-Disposition: form-data; name=\"title\"\r\n\r\n${
+ Services.io.newURI(reportURL).host
+ }\r\n`,
+ 'Content-Disposition: form-data; name="body"\r\n\r\n' +
+ `Full URL: ${reportURL + "?"}\r\n` +
+ `userAgent: ${navigator.userAgent}\r\n\r\n` +
+ "**Preferences**\r\n" +
+ `${prefsBody}\r\n` +
+ `hasException: ${hasException}\r\n\r\n` +
+ "**Comments**\r\n" +
+ "This is a comment\r\n",
+ 'Content-Disposition: form-data; name="labels"\r\n\r\n' +
+ `${hasException ? "" : tags}\r\n`,
+ "",
+ ],
+ "Should send the correct form data"
+ );
+
+ if (error) {
+ response.setStatusLine(request.httpVersion, 500, "Request failed");
+ } else {
+ response.setStatusLine(request.httpVersion, 201, "Entry created");
+ }
+
+ resolve();
+ });
+
+ comments.value = "This is a comment";
+ submitButton.click();
+ });
+
+ let errorMessage = document.getElementById(
+ "protections-popup-sendReportView-report-error"
+ );
+ if (error) {
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(errorMessage)
+ );
+ is(
+ comments.value,
+ "This is a comment",
+ "Comment not cleared in case of an error"
+ );
+ gProtectionsHandler._protectionsPopup.hidePopup();
+ } else {
+ ok(BrowserTestUtils.is_hidden(errorMessage), "Error message not shown");
+ }
+
+ await popuphidden;
+
+ // Stop the server.
+ await new Promise(r => server.stop(r));
+
+ Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_URL);
+}
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js
new file mode 100644
index 0000000000..539ac077ac
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test checks pages of different URL variants (mostly differing in scheme)
+ * and verifies that the shield is only shown when content blocking can deal
+ * with the specific variant. */
+
+const TEST_CASES = [
+ {
+ type: "http",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ testURL: "http://example.com",
+ hidden: false,
+ },
+ {
+ type: "https",
+ testURL: "https://example.com",
+ hidden: false,
+ },
+ {
+ type: "chrome page",
+ testURL: "chrome://global/skin/in-content/info-pages.css",
+ hidden: true,
+ },
+ {
+ type: "content-privileged about page",
+ testURL: "about:robots",
+ hidden: true,
+ },
+ {
+ type: "non-chrome about page",
+ testURL: "about:about",
+ hidden: true,
+ },
+ {
+ type: "chrome about page",
+ testURL: "about:preferences",
+ hidden: true,
+ },
+ {
+ type: "file",
+ testURL: "benignPage.html",
+ hidden: true,
+ },
+ {
+ type: "certificateError",
+ testURL: "https://self-signed.example.com",
+ hidden: true,
+ },
+ {
+ type: "localhost",
+ testURL: "http://127.0.0.1",
+ hidden: false,
+ },
+ {
+ type: "data URI",
+ testURL: "data:text/html,<div>",
+ hidden: true,
+ },
+ {
+ type: "view-source HTTP",
+ testURL: "view-source:http://example.com/",
+ hidden: true,
+ },
+ {
+ type: "view-source HTTPS",
+ testURL: "view-source:https://example.com/",
+ hidden: true,
+ },
+ {
+ type: "top level sandbox",
+ testURL:
+ "https://example.com/browser/browser/base/content/test/protectionsUI/sandboxed.html",
+ hidden: false,
+ },
+];
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // By default, proxies don't apply to 127.0.0.1. We need them to for this test, though:
+ ["network.proxy.allow_hijacking_localhost", true],
+ ],
+ });
+
+ for (let testData of TEST_CASES) {
+ info(`Testing for ${testData.type}`);
+ let testURL = testData.testURL;
+
+ // Overwrite the url if it is testing the file url.
+ if (testData.type === "file") {
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append(testURL);
+ dir.normalize();
+ testURL = Services.io.newFileURI(dir).spec;
+ }
+
+ let pageLoaded;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, testURL);
+ let browser = gBrowser.selectedBrowser;
+ if (testData.type === "certificateError") {
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ } else {
+ pageLoaded = BrowserTestUtils.browserLoaded(browser, true);
+ }
+ },
+ false
+ );
+ await pageLoaded;
+
+ is(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._trackingProtectionIconContainer
+ ),
+ testData.hidden,
+ "tracking protection icon container is correctly displayed"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js
new file mode 100644
index 0000000000..8506016067
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js
@@ -0,0 +1,321 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+const ST_PROTECTION_PREF = "privacy.trackingprotection.socialtracking.enabled";
+const ST_BLOCK_COOKIES_PREF = "privacy.socialtracking.block_cookies.enabled";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ST_BLOCK_COOKIES_PREF, true],
+ [
+ "urlclassifier.features.socialtracking.blacklistHosts",
+ "social-tracking.example.org",
+ ],
+ [
+ "urlclassifier.features.socialtracking.annotate.blacklistHosts",
+ "social-tracking.example.org",
+ ],
+ // Whitelist trackertest.org loaded by default in trackingPage.html
+ ["urlclassifier.trackingSkipURLs", "trackertest.org"],
+ ["urlclassifier.trackingAnnotationSkipURLs", "trackertest.org"],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+});
+
+async function testIdentityState(hasException) {
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ await openProtectionsPanel();
+ let categoryItem = document.getElementById(
+ "protections-popup-category-socialblock"
+ );
+
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "socialtrackings are not detected"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible regardless the exception"
+ );
+ await closeProtectionsPanel();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("socialtracking", "*");
+ });
+ await openProtectionsPanel();
+
+ await TestUtils.waitForCondition(() => {
+ return !categoryItem.classList.contains("notFound");
+ });
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are detected"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "social trackers are detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ hasException,
+ "Shows an exception when appropriate"
+ );
+ await closeProtectionsPanel();
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testSubview(hasException) {
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("socialtracking", "*");
+ });
+ await promise;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-socialblock"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "STP category item is visible");
+ ok(
+ categoryItem.classList.contains("blocked"),
+ "STP category item is blocked"
+ );
+
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ // We have to wait until the ContentBlockingLog gets updated in the content.
+ // Unfortunately, we need to use the setTimeout here since we don't have an
+ // easy to know whether the log is updated in the content. This should be
+ // removed after the log been removed in the content (Bug 1599046).
+ await new Promise(resolve => {
+ setTimeout(resolve, 500);
+ });
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let subview = document.getElementById("protections-popup-socialblockView");
+ let viewShown = BrowserTestUtils.waitForEvent(subview, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ let trackersViewShimHint = document.getElementById(
+ "protections-popup-socialblockView-shim-allow-hint"
+ );
+ ok(trackersViewShimHint.hidden, "Shim hint is hidden");
+
+ let listItems = subview.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 item in the list");
+ let listItem = listItems[0];
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.querySelector("label").value,
+ "https://social-tracking.example.org",
+ "Has the correct host"
+ );
+
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = subview.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testCategoryItem(blockLoads) {
+ if (blockLoads) {
+ Services.prefs.setBoolPref(ST_PROTECTION_PREF, true);
+ }
+
+ Services.prefs.setBoolPref(ST_BLOCK_COOKIES_PREF, false);
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-socialblock"
+ );
+
+ let noTrackersDetectedDesc = document.getElementById(
+ "protections-popup-no-trackers-found-description"
+ );
+
+ ok(categoryItem.hasAttribute("uidisabled"), "Category should be uidisabled");
+
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(!BrowserTestUtils.is_visible(categoryItem), "Item should be hidden");
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("socialtracking", "*");
+ });
+
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(!BrowserTestUtils.is_visible(categoryItem), "Item should be hidden");
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(noTrackersDetectedDesc),
+ "No Trackers detected should be shown"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.setBoolPref(ST_BLOCK_COOKIES_PREF, true);
+
+ promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ await openProtectionsPanel();
+
+ ok(!categoryItem.hasAttribute("uidisabled"), "Item shouldn't be uidisabled");
+
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ // At this point we should still be showing "No Trackers Detected"
+ ok(!BrowserTestUtils.is_visible(categoryItem), "Item should not be visible");
+ ok(
+ BrowserTestUtils.is_visible(noTrackersDetectedDesc),
+ "No Trackers detected should be shown"
+ );
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("socialtracking", "*");
+ });
+
+ await TestUtils.waitForCondition(() => {
+ return !categoryItem.classList.contains("notFound");
+ });
+
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ ok(BrowserTestUtils.is_visible(categoryItem), "Item should be visible");
+ ok(
+ !BrowserTestUtils.is_visible(noTrackersDetectedDesc),
+ "No Trackers detected should be hidden"
+ );
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(ST_PROTECTION_PREF);
+}
+
+add_task(async function testIdentityUI() {
+ requestLongerTimeout(2);
+
+ await testIdentityState(false);
+ await testIdentityState(true);
+
+ await testSubview(false);
+ await testSubview(true);
+
+ await testCategoryItem(false);
+ await testCategoryItem(true);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_state.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_state.js
new file mode 100644
index 0000000000..b524d2d7c7
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_state.js
@@ -0,0 +1,405 @@
+/*
+ * Test that the Tracking Protection section is visible in the Control Center
+ * and has the correct state for the cases when:
+ *
+ * In a normal window as well as a private window,
+ * With TP enabled
+ * 1) A page with no tracking elements is loaded.
+ * 2) A page with tracking elements is loaded and they are blocked.
+ * 3) A page with tracking elements is loaded and they are not blocked.
+ * With TP disabled
+ * 1) A page with no tracking elements is loaded.
+ * 2) A page with tracking elements is loaded.
+ *
+ * See also Bugs 1175327, 1043801, 1178985
+ */
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TP_PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+const TPC_PREF = "network.cookie.cookieBehavior";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+const BENIGN_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const COOKIE_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+var gProtectionsHandler = null;
+var TrackingProtection = null;
+var ThirdPartyCookies = null;
+var tabbrowser = null;
+var gTrackingPageURL = TRACKING_PAGE;
+
+const sBrandBundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+);
+const sNoTrackerIconTooltip = gNavigatorBundle.getFormattedString(
+ "trackingProtection.icon.noTrackersDetectedTooltip",
+ [sBrandBundle.GetStringFromName("brandShortName")]
+);
+const sActiveIconTooltip = gNavigatorBundle.getString(
+ "trackingProtection.icon.activeTooltip2"
+);
+const sDisabledIconTooltip = gNavigatorBundle.getString(
+ "trackingProtection.icon.disabledTooltip2"
+);
+
+registerCleanupFunction(function () {
+ TrackingProtection =
+ gProtectionsHandler =
+ ThirdPartyCookies =
+ tabbrowser =
+ null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.prefs.clearUserPref(TP_PREF);
+ Services.prefs.clearUserPref(TP_PB_PREF);
+ Services.prefs.clearUserPref(TPC_PREF);
+ Services.prefs.clearUserPref(DTSCBN_PREF);
+});
+
+function notFound(id) {
+ let doc = tabbrowser.ownerGlobal.document;
+ return doc.getElementById(id).classList.contains("notFound");
+}
+
+async function testBenignPage() {
+ info("Non-tracking content must not be blocked");
+ ok(!gProtectionsHandler.anyDetected, "no trackers are detected");
+ ok(!gProtectionsHandler.hasException, "content shows no exception");
+
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "icon box shows no exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ sNoTrackerIconTooltip,
+ "correct tooltip"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+
+ let win = tabbrowser.ownerGlobal;
+ await openProtectionsPanel(false, win);
+ ok(
+ notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is not found"
+ );
+ ok(
+ notFound("protections-popup-category-trackers"),
+ "Trackers category is not found"
+ );
+ await closeProtectionsPanel(win);
+}
+
+async function testBenignPageWithException() {
+ info("Non-tracking content must not be blocked");
+ ok(!gProtectionsHandler.anyDetected, "no trackers are detected");
+ ok(gProtectionsHandler.hasException, "content shows exception");
+
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "shield shows exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ sDisabledIconTooltip,
+ "correct tooltip"
+ );
+
+ ok(
+ !BrowserTestUtils.is_hidden(gProtectionsHandler.iconBox),
+ "icon box is not hidden"
+ );
+
+ let win = tabbrowser.ownerGlobal;
+ await openProtectionsPanel(false, win);
+ ok(
+ notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is not found"
+ );
+ ok(
+ notFound("protections-popup-category-trackers"),
+ "Trackers category is not found"
+ );
+ await closeProtectionsPanel(win);
+}
+
+function areTrackersBlocked(isPrivateBrowsing) {
+ let blockedByTP = Services.prefs.getBoolPref(
+ isPrivateBrowsing ? TP_PB_PREF : TP_PREF
+ );
+ let blockedByTPC = [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ].includes(Services.prefs.getIntPref(TPC_PREF));
+ return blockedByTP || blockedByTPC;
+}
+
+async function testTrackingPage(window) {
+ info("Tracking content must be blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(!gProtectionsHandler.hasException, "content shows no exception");
+
+ let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+ let blockedByTP = areTrackersBlocked(isWindowPrivate);
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is always visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("active"),
+ blockedByTP,
+ "shield is" + (blockedByTP ? "" : " not") + " active"
+ );
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "icon box shows no exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ blockedByTP ? sActiveIconTooltip : sNoTrackerIconTooltip,
+ "correct tooltip"
+ );
+
+ await openProtectionsPanel(false, window);
+ ok(
+ !notFound("protections-popup-category-trackers"),
+ "Trackers category is detected"
+ );
+ if (gTrackingPageURL == COOKIE_PAGE) {
+ ok(
+ !notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is detected"
+ );
+ } else {
+ ok(
+ notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is not found"
+ );
+ }
+ await closeProtectionsPanel(window);
+}
+
+async function testTrackingPageUnblocked(blockedByTP, window) {
+ info("Tracking content must be in the exception list and not blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(gProtectionsHandler.hasException, "content shows exception");
+
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "shield shows exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ sDisabledIconTooltip,
+ "correct tooltip"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+
+ await openProtectionsPanel(false, window);
+ ok(
+ !notFound("protections-popup-category-trackers"),
+ "Trackers category is detected"
+ );
+ if (gTrackingPageURL == COOKIE_PAGE) {
+ ok(
+ !notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is detected"
+ );
+ } else {
+ ok(
+ notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is not found"
+ );
+ }
+ await closeProtectionsPanel(window);
+}
+
+async function testContentBlocking(tab) {
+ info("Testing with Tracking Protection ENABLED.");
+
+ info("Load a test page not containing tracking elements");
+ await promiseTabLoadEvent(tab, BENIGN_PAGE);
+ await testBenignPage();
+
+ info(
+ "Load a test page not containing tracking elements which has an exception."
+ );
+
+ await promiseTabLoadEvent(tab, "https://example.org/?round=1");
+
+ ContentBlockingAllowList.add(tab.linkedBrowser);
+ // Load another page from the same origin to ensure there is an onlocationchange
+ // notification which would trigger an oncontentblocking notification for us.
+ await promiseTabLoadEvent(tab, "https://example.org/?round=2");
+
+ await testBenignPageWithException();
+
+ ContentBlockingAllowList.remove(tab.linkedBrowser);
+
+ info("Load a test page containing tracking elements");
+ await promiseTabLoadEvent(tab, gTrackingPageURL);
+ await testTrackingPage(tab.ownerGlobal);
+
+ info("Disable CB for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ tab.ownerGlobal.gProtectionsHandler.disableForCurrentPage();
+ await tabReloadPromise;
+ let isPrivateBrowsing = PrivateBrowsingUtils.isWindowPrivate(tab.ownerGlobal);
+ let blockedByTP = areTrackersBlocked(isPrivateBrowsing);
+ await testTrackingPageUnblocked(blockedByTP, tab.ownerGlobal);
+
+ info("Re-enable TP for the page (which reloads the page)");
+ tabReloadPromise = promiseTabLoadEvent(tab);
+ tab.ownerGlobal.gProtectionsHandler.enableForCurrentPage();
+ await tabReloadPromise;
+ await testTrackingPage(tab.ownerGlobal);
+}
+
+add_task(async function testNormalBrowsing() {
+ await SpecialPowers.pushPrefEnv({ set: [[APS_PREF, false]] });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ Services.prefs.setBoolPref(DTSCBN_PREF, true);
+
+ tabbrowser = gBrowser;
+ let tab = (tabbrowser.selectedTab = BrowserTestUtils.addTab(tabbrowser));
+
+ gProtectionsHandler = gBrowser.ownerGlobal.gProtectionsHandler;
+ ok(
+ gProtectionsHandler,
+ "gProtectionsHandler is attached to the browser window"
+ );
+
+ TrackingProtection =
+ gBrowser.ownerGlobal.gProtectionsHandler.blockers.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+ is(
+ TrackingProtection.enabled,
+ Services.prefs.getBoolPref(TP_PREF),
+ "TP.enabled is based on the original pref value"
+ );
+
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+
+ await testContentBlocking(tab);
+
+ Services.prefs.setBoolPref(TP_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ await testContentBlocking(tab);
+
+ gBrowser.removeCurrentTab();
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testPrivateBrowsing() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_first_pbm", false],
+ [APS_PREF, false],
+ ],
+ });
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ tabbrowser = privateWin.gBrowser;
+ let tab = (tabbrowser.selectedTab = BrowserTestUtils.addTab(tabbrowser));
+
+ // Set the normal mode pref to false to check the pbmode pref.
+ Services.prefs.setBoolPref(TP_PREF, false);
+
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+
+ gProtectionsHandler = tabbrowser.ownerGlobal.gProtectionsHandler;
+ ok(
+ gProtectionsHandler,
+ "gProtectionsHandler is attached to the private window"
+ );
+
+ TrackingProtection =
+ tabbrowser.ownerGlobal.gProtectionsHandler.blockers.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+ is(
+ TrackingProtection.enabled,
+ Services.prefs.getBoolPref(TP_PB_PREF),
+ "TP.enabled is based on the pb pref value"
+ );
+
+ await testContentBlocking(tab);
+
+ Services.prefs.setBoolPref(TP_PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ await testContentBlocking(tab);
+
+ privateWin.close();
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testThirdPartyCookies() {
+ await SpecialPowers.pushPrefEnv({ set: [[APS_PREF, false]] });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+ gTrackingPageURL = COOKIE_PAGE;
+
+ tabbrowser = gBrowser;
+ let tab = (tabbrowser.selectedTab = BrowserTestUtils.addTab(tabbrowser));
+
+ gProtectionsHandler = gBrowser.ownerGlobal.gProtectionsHandler;
+ ok(
+ gProtectionsHandler,
+ "gProtectionsHandler is attached to the browser window"
+ );
+ ThirdPartyCookies =
+ gBrowser.ownerGlobal.gProtectionsHandler.blockers.ThirdPartyCookies;
+ ok(ThirdPartyCookies, "TP is attached to the browser window");
+ is(
+ ThirdPartyCookies.enabled,
+ [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ].includes(Services.prefs.getIntPref(TPC_PREF)),
+ "TPC.enabled is based on the original pref value"
+ );
+
+ await testContentBlocking(tab);
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ ok(ThirdPartyCookies.enabled, "TPC is enabled after setting the pref");
+
+ await testContentBlocking(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_state_reset.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_state_reset.js
new file mode 100644
index 0000000000..020733cc72
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_state_reset.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const BENIGN_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const ABOUT_PAGE = "about:preferences";
+
+/* This asserts that the content blocking event state is correctly reset
+ * when navigating to a new location, and that the user is correctly
+ * reset when switching between tabs. */
+
+add_task(async function testResetOnLocationChange() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BENIGN_PAGE);
+ let browser = tab.linkedBrowser;
+
+ is(
+ browser.getContentBlockingEvents(),
+ 0,
+ "Benign page has no content blocking event"
+ );
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+
+ await Promise.all([
+ promiseTabLoadEvent(tab, TRACKING_PAGE),
+ waitForContentBlockingEvent(2),
+ ]);
+
+ is(
+ browser.getContentBlockingEvents(),
+ Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+ "Tracking page has a content blocking event"
+ );
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+
+ await promiseTabLoadEvent(tab, BENIGN_PAGE);
+
+ is(
+ browser.getContentBlockingEvents(),
+ 0,
+ "Benign page has no content blocking event"
+ );
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+
+ let contentBlockingEvent = waitForContentBlockingEvent(3);
+ let trackingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TRACKING_PAGE
+ );
+ await contentBlockingEvent;
+
+ is(
+ trackingTab.linkedBrowser.getContentBlockingEvents(),
+ Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+ "Tracking page has a content blocking event"
+ );
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+
+ gBrowser.selectedTab = tab;
+ is(
+ browser.getContentBlockingEvents(),
+ 0,
+ "Benign page has no content blocking event"
+ );
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+
+ gBrowser.removeTab(trackingTab);
+ gBrowser.removeTab(tab);
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+/* Test that the content blocking icon is correctly reset
+ * when changing tabs or navigating to an about: page */
+add_task(async function testResetOnTabChange() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, ABOUT_PAGE);
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+
+ await Promise.all([
+ promiseTabLoadEvent(tab, TRACKING_PAGE),
+ waitForContentBlockingEvent(3),
+ ]);
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+
+ await promiseTabLoadEvent(tab, ABOUT_PAGE);
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+
+ let contentBlockingEvent = waitForContentBlockingEvent(3);
+ let trackingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TRACKING_PAGE
+ );
+ await contentBlockingEvent;
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+
+ gBrowser.selectedTab = tab;
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+
+ gBrowser.removeTab(trackingTab);
+ gBrowser.removeTab(tab);
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_subview_shim.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_subview_shim.js
new file mode 100644
index 0000000000..cf120a33da
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_subview_shim.js
@@ -0,0 +1,403 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the warning and list indicators that are shown in the protections panel
+ * subview when a tracking channel is allowed via the
+ * "urlclassifier-before-block-channel" event.
+ */
+
+// Choose origin so that all tracking origins used are third-parties.
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.net/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.enabled", true],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ["privacy.trackingprotection.cryptomining.enabled", true],
+ ["privacy.trackingprotection.socialtracking.enabled", true],
+ ["privacy.trackingprotection.fingerprinting.enabled", true],
+ ["privacy.socialtracking.block_cookies.enabled", true],
+ // Allowlist trackertest.org loaded by default in trackingPage.html
+ ["urlclassifier.trackingSkipURLs", "trackertest.org"],
+ ["urlclassifier.trackingAnnotationSkipURLs", "trackertest.org"],
+ // Additional denylisted hosts.
+ [
+ "urlclassifier.trackingAnnotationTable.testEntries",
+ "tracking.example.com",
+ ],
+ [
+ "urlclassifier.features.cryptomining.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ [
+ "urlclassifier.features.cryptomining.annotate.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ [
+ "urlclassifier.features.fingerprinting.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ [
+ "urlclassifier.features.fingerprinting.annotate.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+ registerCleanupFunction(() => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+async function assertSubViewState(category, expectedState) {
+ await openProtectionsPanel();
+
+ // Sort the expected state by origin and transform it into an array.
+ let expectedStateSorted = Object.keys(expectedState)
+ .sort()
+ .reduce((stateArr, key) => {
+ let obj = expectedState[key];
+ obj.origin = key;
+ stateArr.push(obj);
+ return stateArr;
+ }, []);
+
+ if (!expectedStateSorted.length) {
+ ok(
+ BrowserTestUtils.is_visible(
+ document.getElementById(
+ "protections-popup-no-trackers-found-description"
+ )
+ ),
+ "No Trackers detected should be shown"
+ );
+ return;
+ }
+
+ let categoryItem = document.getElementById(
+ `protections-popup-category-${category}`
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(
+ BrowserTestUtils.is_visible(categoryItem),
+ `${category} category item is visible`
+ );
+
+ ok(!categoryItem.disabled, `${category} category item is enabled`);
+
+ let subView = document.getElementById(`protections-popup-${category}View`);
+ let viewShown = BrowserTestUtils.waitForEvent(subView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, `${category} subView was shown`);
+
+ info("Testing tracker list");
+
+ // Get the listed trackers in the UI and sort them by origin.
+ let items = Array.from(
+ subView.querySelectorAll(
+ `#protections-popup-${category}View-list .protections-popup-list-item`
+ )
+ ).sort((a, b) => {
+ let originA = a.querySelector("label").value;
+ let originB = b.querySelector("label").value;
+ return originA.localeCompare(originB);
+ });
+
+ is(
+ items.length,
+ expectedStateSorted.length,
+ "List has expected amount of entries"
+ );
+
+ for (let i = 0; i < expectedStateSorted.length; i += 1) {
+ let expected = expectedStateSorted[i];
+ let item = items[i];
+
+ let label = item.querySelector(".protections-popup-list-host-label");
+ ok(label, "Item has label.");
+ is(label.tooltipText, expected.origin, "Label has correct tooltip.");
+ is(label.value, expected.origin, "Label has correct text.");
+
+ is(
+ item.classList.contains("allowed"),
+ !expected.block,
+ "Item has allowed class if tracker is not blocked"
+ );
+
+ let shimAllowIndicator = item.querySelector(
+ ".protections-popup-list-host-shim-allow-indicator"
+ );
+
+ if (expected.shimAllow) {
+ is(item.childNodes.length, 2, "Item has two childNodes.");
+ ok(shimAllowIndicator, "Item has shim allow indicator icon.");
+ ok(
+ shimAllowIndicator.tooltipText,
+ "Shim allow indicator icon has tooltip text"
+ );
+ } else {
+ is(item.childNodes.length, 1, "Item has one childNode.");
+ ok(!shimAllowIndicator, "Item does not have shim allow indicator icon.");
+ }
+ }
+
+ let shimAllowSection = document.getElementById(
+ `protections-popup-${category}View-shim-allow-hint`
+ );
+ ok(shimAllowSection, `Category ${category} has shim-allow hint.`);
+
+ if (Object.values(expectedState).some(entry => entry.shimAllow)) {
+ BrowserTestUtils.is_visible(
+ shimAllowSection,
+ "Shim allow hint is visible."
+ );
+ } else {
+ BrowserTestUtils.is_hidden(shimAllowSection, "Shim allow hint is hidden.");
+ }
+
+ await closeProtectionsPanel();
+}
+
+async function runTestForCategoryAndState(category, action) {
+ // Maps the protection categories to the test tracking origins defined in
+ // ./trackingAPI.js and the UI class identifiers to look for in the
+ // protections UI.
+ let categoryToTestData = {
+ tracking: {
+ apiMessage: "more-tracking",
+ origin: "https://itisatracker.org",
+ elementId: "trackers",
+ },
+ socialtracking: {
+ origin: "https://social-tracking.example.org",
+ elementId: "socialblock",
+ },
+ cryptomining: {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ origin: "http://cryptomining.example.com",
+ elementId: "cryptominers",
+ },
+ fingerprinting: {
+ origin: "https://fingerprinting.example.com",
+ elementId: "fingerprinters",
+ },
+ };
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ // Wait for the tab to load and the initial blocking events from the
+ // classifier.
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ let {
+ origin: trackingOrigin,
+ elementId: categoryElementId,
+ apiMessage,
+ } = categoryToTestData[category];
+ if (!apiMessage) {
+ apiMessage = category;
+ }
+
+ // For allow or replace actions we need to hook into before-block-channel.
+ // If we don't hook into the event, the tracking channel will be blocked.
+ let beforeBlockChannelPromise;
+ if (action != "block") {
+ beforeBlockChannelPromise = UrlClassifierTestUtils.handleBeforeBlockChannel(
+ {
+ filterOrigin: trackingOrigin,
+ action,
+ }
+ );
+ }
+ // Load the test tracker matching the category.
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ apiMessage }],
+ function (args) {
+ content.postMessage(args.apiMessage, "*");
+ }
+ );
+ await beforeBlockChannelPromise;
+
+ // Next, test if the UI state is correct for the given category and action.
+ let expectedState = {};
+ expectedState[trackingOrigin] = {
+ block: action == "block",
+ shimAllow: action == "allow",
+ };
+
+ await assertSubViewState(categoryElementId, expectedState);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+/**
+ * Test mixed allow/block/replace states for the tracking protection category.
+ * @param {Object} options - States to test.
+ * @param {boolean} options.block - Test tracker block state.
+ * @param {boolean} options.allow - Test tracker allow state.
+ * @param {boolean} options.replace - Test tracker replace state.
+ */
+async function runTestMixed({ block, allow, replace }) {
+ const ORIGIN_BLOCK = "https://trackertest.org";
+ const ORIGIN_ALLOW = "https://itisatracker.org";
+ const ORIGIN_REPLACE = "https://tracking.example.com";
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (block) {
+ // Temporarily remove trackertest.org from the allowlist.
+ await SpecialPowers.pushPrefEnv({
+ clear: [
+ ["urlclassifier.trackingSkipURLs"],
+ ["urlclassifier.trackingAnnotationSkipURLs"],
+ ],
+ });
+ let blockEventPromise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("tracking", "*");
+ });
+ await blockEventPromise;
+ await SpecialPowers.popPrefEnv();
+ }
+
+ if (allow) {
+ let promiseEvent = waitForContentBlockingEvent();
+ let promiseAllow = UrlClassifierTestUtils.handleBeforeBlockChannel({
+ filterOrigin: ORIGIN_ALLOW,
+ action: "allow",
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("more-tracking", "*");
+ });
+
+ await promiseAllow;
+ await promiseEvent;
+ }
+
+ if (replace) {
+ let promiseReplace = UrlClassifierTestUtils.handleBeforeBlockChannel({
+ filterOrigin: ORIGIN_REPLACE,
+ action: "replace",
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("more-tracking-2", "*");
+ });
+
+ await promiseReplace;
+ }
+
+ let expectedState = {};
+
+ if (block) {
+ expectedState[ORIGIN_BLOCK] = {
+ shimAllow: false,
+ block: true,
+ };
+ }
+
+ if (replace) {
+ expectedState[ORIGIN_REPLACE] = {
+ shimAllow: false,
+ block: false,
+ };
+ }
+
+ if (allow) {
+ expectedState[ORIGIN_ALLOW] = {
+ shimAllow: true,
+ block: false,
+ };
+ }
+
+ // Check the protection categories subview with the block list.
+ await assertSubViewState("trackers", expectedState);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testNoShim() {
+ await runTestMixed({
+ allow: false,
+ replace: false,
+ block: false,
+ });
+ await runTestMixed({
+ allow: false,
+ replace: false,
+ block: true,
+ });
+});
+
+add_task(async function testShimAllow() {
+ await runTestMixed({
+ allow: true,
+ replace: false,
+ block: false,
+ });
+ await runTestMixed({
+ allow: true,
+ replace: false,
+ block: true,
+ });
+});
+
+add_task(async function testShimReplace() {
+ await runTestMixed({
+ allow: false,
+ replace: true,
+ block: false,
+ });
+ await runTestMixed({
+ allow: false,
+ replace: true,
+ block: true,
+ });
+});
+
+add_task(async function testShimMixed() {
+ await runTestMixed({
+ allow: true,
+ replace: true,
+ block: true,
+ });
+});
+
+add_task(async function testShimCategorySubviews() {
+ let categories = [
+ "tracking",
+ "socialtracking",
+ "cryptomining",
+ "fingerprinting",
+ ];
+ for (let category of categories) {
+ for (let action of ["block", "allow", "replace"]) {
+ info(`Test category subview. category: ${category}, action: ${action}`);
+ await runTestForCategoryAndState(category, action);
+ }
+ }
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js
new file mode 100644
index 0000000000..6bac0ce9b6
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js
@@ -0,0 +1,89 @@
+/*
+ * Test telemetry for Tracking Protection
+ */
+
+const PREF = "privacy.trackingprotection.enabled";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+const BENIGN_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+/**
+ * Enable local telemetry recording for the duration of the tests.
+ */
+var oldCanRecord = Services.telemetry.canRecordExtended;
+Services.telemetry.canRecordExtended = true;
+registerCleanupFunction(function () {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.prefs.clearUserPref(PREF);
+ Services.prefs.clearUserPref(DTSCBN_PREF);
+});
+
+function getShieldHistogram() {
+ return Services.telemetry.getHistogramById("TRACKING_PROTECTION_SHIELD");
+}
+
+function getShieldCounts() {
+ return getShieldHistogram().snapshot().values;
+}
+
+add_setup(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+ Services.prefs.setBoolPref(DTSCBN_PREF, true);
+
+ let TrackingProtection =
+ gBrowser.ownerGlobal.gProtectionsHandler.blockers.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+ ok(!TrackingProtection.enabled, "TP is not enabled");
+
+ let enabledCounts = Services.telemetry
+ .getHistogramById("TRACKING_PROTECTION_ENABLED")
+ .snapshot().values;
+ is(enabledCounts[0], 1, "TP was not enabled on start up");
+});
+
+add_task(async function testShieldHistogram() {
+ Services.prefs.setBoolPref(PREF, true);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Reset these to make counting easier
+ getShieldHistogram().clear();
+
+ await promiseTabLoadEvent(tab, BENIGN_PAGE);
+ is(getShieldCounts()[0], 1, "Page loads without tracking");
+
+ await promiseTabLoadEvent(tab, TRACKING_PAGE);
+ is(getShieldCounts()[0], 2, "Adds one more page load");
+ is(getShieldCounts()[2], 1, "Counts one instance of the shield being shown");
+
+ info("Disable TP for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ gProtectionsHandler.disableForCurrentPage();
+ await tabReloadPromise;
+ is(getShieldCounts()[0], 3, "Adds one more page load");
+ is(
+ getShieldCounts()[1],
+ 1,
+ "Counts one instance of the shield being crossed out"
+ );
+
+ info("Re-enable TP for the page (which reloads the page)");
+ tabReloadPromise = promiseTabLoadEvent(tab);
+ gProtectionsHandler.enableForCurrentPage();
+ await tabReloadPromise;
+ is(getShieldCounts()[0], 4, "Adds one more page load");
+ is(
+ getShieldCounts()[2],
+ 2,
+ "Adds one more instance of the shield being shown"
+ );
+
+ gBrowser.removeCurrentTab();
+
+ // Reset these to make counting easier for the next test
+ getShieldHistogram().clear();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js
new file mode 100644
index 0000000000..7fe52065ea
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js
@@ -0,0 +1,134 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const TRACKING_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+
+add_setup(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+async function assertSitesListed(blocked) {
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+
+ // Wait for 2 content blocking events - one for the load and one for the tracker.
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(2)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-trackers"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let trackersView = document.getElementById("protections-popup-trackersView");
+ let viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Trackers view was shown");
+
+ let trackersViewShimHint = document.getElementById(
+ "protections-popup-trackersView-shim-allow-hint"
+ );
+ ok(trackersViewShimHint.hidden, "Shim hint is hidden");
+ let listItems = trackersView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 tracker in the list");
+
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = trackersView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ let change = waitForSecurityChange(1);
+ let timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.postMessage("more-tracking", "*");
+ });
+
+ let result = await Promise.race([change, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Trackers view was shown");
+
+ listItems = Array.from(
+ trackersView.querySelectorAll(".protections-popup-list-item")
+ );
+ is(listItems.length, 2, "We have 2 trackers in the list");
+
+ let listItem = listItems.find(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ item => item.querySelector("label").value == "http://trackertest.org"
+ );
+ ok(listItem, "Has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.classList.contains("allowed"),
+ !blocked,
+ "Indicates whether the tracker was blocked or allowed"
+ );
+
+ listItem = listItems.find(
+ item => item.querySelector("label").value == "https://itisatracker.org"
+ );
+ ok(listItem, "Has an item for itisatracker.org");
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.classList.contains("allowed"),
+ !blocked,
+ "Indicates whether the tracker was blocked or allowed"
+ );
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testTrackersSubView() {
+ info("Testing trackers subview with TP disabled.");
+ Services.prefs.setBoolPref(TP_PREF, false);
+ await assertSitesListed(false);
+ info("Testing trackers subview with TP enabled.");
+ Services.prefs.setBoolPref(TP_PREF, true);
+ await assertSitesListed(true);
+ info("Testing trackers subview with TP enabled and a CB exception.");
+ let uri = Services.io.newURI("https://tracking.example.org");
+ PermissionTestUtils.add(
+ uri,
+ "trackingprotection",
+ Services.perms.ALLOW_ACTION
+ );
+ await assertSitesListed(false);
+ info("Testing trackers subview with TP enabled and a CB exception removed.");
+ PermissionTestUtils.remove(uri, "trackingprotection");
+ await assertSitesListed(true);
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/containerPage.html b/browser/base/content/test/protectionsUI/containerPage.html
new file mode 100644
index 0000000000..f68f7325c1
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/containerPage.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <iframe src="http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/embeddedPage.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/cookiePage.html b/browser/base/content/test/protectionsUI/cookiePage.html
new file mode 100644
index 0000000000..e7ef2aafa1
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/cookiePage.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <script src="trackingAPI.js" type="text/javascript"></script>
+ </head>
+ <body>
+ <iframe src="http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/cookieServer.sjs b/browser/base/content/test/protectionsUI/cookieServer.sjs
new file mode 100644
index 0000000000..44341b9a71
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/cookieServer.sjs
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+const IMAGE = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+);
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200);
+ if (
+ request.queryString &&
+ request.queryString.includes("type=image-no-cookie")
+ ) {
+ response.setHeader("Content-Type", "image/png", false);
+ response.write(IMAGE);
+ } else {
+ response.setHeader("Set-Cookie", "foopy=1");
+ response.write("cookie served");
+ }
+}
diff --git a/browser/base/content/test/protectionsUI/cookieSetterPage.html b/browser/base/content/test/protectionsUI/cookieSetterPage.html
new file mode 100644
index 0000000000..aab18e0aff
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/cookieSetterPage.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <script> document.cookie = "foo=bar"; </script>
+</body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/emailTrackingPage.html b/browser/base/content/test/protectionsUI/emailTrackingPage.html
new file mode 100644
index 0000000000..85b48cbbcb
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/emailTrackingPage.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <iframe src="https://email-tracking.example.org/"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/embeddedPage.html b/browser/base/content/test/protectionsUI/embeddedPage.html
new file mode 100644
index 0000000000..6003d49300
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/embeddedPage.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <iframe src="http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieSetterPage.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html
new file mode 100644
index 0000000000..4a7f1a1682
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Testing the shield from fetch and XHR</title>
+</head>
+<body>
+ <p>Hello there!</p>
+ <script type="application/javascript">
+ function test_fetch() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let url = "http://trackertest.org/browser/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js";
+ return fetch(url);
+ }
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js
new file mode 100644
index 0000000000..f7ac687cfc
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js
@@ -0,0 +1,2 @@
+/* Some code goes here! */
+void 0;
diff --git a/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/browser/base/content/test/protectionsUI/head.js b/browser/base/content/test/protectionsUI/head.js
new file mode 100644
index 0000000000..28395ad732
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/head.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { Sqlite } = ChromeUtils.importESModule(
+ "resource://gre/modules/Sqlite.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+XPCOMUtils.defineLazyGetter(this, "TRACK_DB_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, "protections.sqlite");
+});
+
+ChromeUtils.defineESModuleGetters(this, {
+ ContentBlockingAllowList:
+ "resource://gre/modules/ContentBlockingAllowList.sys.mjs",
+});
+
+var { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+
+async function openProtectionsPanel(toast, win = window) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ win,
+ "popupshown",
+ true,
+ e => e.target.id == "protections-popup"
+ );
+ let shieldIconContainer = win.document.getElementById(
+ "tracking-protection-icon-container"
+ );
+
+ // Move out than move over the shield icon to trigger the hover event in
+ // order to fetch tracker count.
+ EventUtils.synthesizeMouseAtCenter(
+ win.gURLBar.textbox,
+ {
+ type: "mousemove",
+ },
+ win
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ shieldIconContainer,
+ {
+ type: "mousemove",
+ },
+ win
+ );
+
+ if (!toast) {
+ EventUtils.synthesizeMouseAtCenter(shieldIconContainer, {}, win);
+ } else {
+ win.gProtectionsHandler.showProtectionsPopup({ toast });
+ }
+
+ await popupShownPromise;
+}
+
+async function openProtectionsPanelWithKeyNav() {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ e => e.target.id == "protections-popup"
+ );
+
+ gURLBar.focus();
+
+ // This will trigger the focus event for the shield icon for pre-fetching
+ // the tracker count.
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_Enter", {});
+
+ await popupShownPromise;
+}
+
+async function closeProtectionsPanel(win = window) {
+ let protectionsPopup = win.document.getElementById("protections-popup");
+ if (!protectionsPopup) {
+ return;
+ }
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ protectionsPopup,
+ "popuphidden"
+ );
+
+ PanelMultiView.hidePopup(protectionsPopup);
+ await popuphiddenPromise;
+}
+
+function checkClickTelemetry(objectName, value, source = "protectionspopup") {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ let buttonEvents = events.filter(
+ e =>
+ e[1] == `security.ui.${source}` &&
+ e[2] == "click" &&
+ e[3] == objectName &&
+ e[4] === value
+ );
+ is(buttonEvents.length, 1, `recorded ${objectName} telemetry event`);
+}
+
+async function addTrackerDataIntoDB(count) {
+ const insertSQL =
+ "INSERT INTO events (type, count, timestamp)" +
+ "VALUES (:type, :count, date(:timestamp));";
+
+ let db = await Sqlite.openConnection({ path: TRACK_DB_PATH });
+ let date = new Date().toISOString();
+
+ await db.execute(insertSQL, {
+ type: TrackingDBService.TRACKERS_ID,
+ count,
+ timestamp: date,
+ });
+
+ await db.close();
+}
+
+async function waitForAboutProtectionsTab() {
+ let tab = await BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:protections",
+ true
+ );
+
+ // When the graph is built it means the messaging has finished,
+ // we can close the tab.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let bars = content.document.querySelectorAll(".graph-bar");
+ return bars.length;
+ }, "The graph has been built");
+ });
+
+ return tab;
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url) {
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+function waitForSecurityChange(numChanges = 1, win = null) {
+ if (!win) {
+ win = window;
+ }
+ return new Promise(resolve => {
+ let n = 0;
+ let listener = {
+ onSecurityChange() {
+ n = n + 1;
+ info("Received onSecurityChange event " + n + " of " + numChanges);
+ if (n >= numChanges) {
+ win.gBrowser.removeProgressListener(listener);
+ resolve(n);
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(listener);
+ });
+}
+
+function waitForContentBlockingEvent(numChanges = 1, win = null) {
+ if (!win) {
+ win = window;
+ }
+ return new Promise(resolve => {
+ let n = 0;
+ let listener = {
+ onContentBlockingEvent(webProgress, request, event) {
+ n = n + 1;
+ info(
+ `Received onContentBlockingEvent event: ${event} (${n} of ${numChanges})`
+ );
+ if (n >= numChanges) {
+ win.gBrowser.removeProgressListener(listener);
+ resolve(n);
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(listener);
+ });
+}
diff --git a/browser/base/content/test/protectionsUI/sandboxed.html b/browser/base/content/test/protectionsUI/sandboxed.html
new file mode 100644
index 0000000000..661fb0b8e2
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/sandboxed.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <iframe src="http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/sandboxed.html^headers^ b/browser/base/content/test/protectionsUI/sandboxed.html^headers^
new file mode 100644
index 0000000000..4705ce9ded
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/sandboxed.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts;
diff --git a/browser/base/content/test/protectionsUI/trackingAPI.js b/browser/base/content/test/protectionsUI/trackingAPI.js
new file mode 100644
index 0000000000..de5479a70f
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/trackingAPI.js
@@ -0,0 +1,77 @@
+function createIframe(src) {
+ let ifr = document.createElement("iframe");
+ ifr.src = src;
+ document.body.appendChild(ifr);
+}
+
+function createImage(src) {
+ let img = document.createElement("img");
+ img.src = src;
+ img.onload = () => {
+ parent.postMessage("done", "*");
+ };
+ document.body.appendChild(img);
+}
+
+onmessage = event => {
+ switch (event.data) {
+ case "tracking":
+ createIframe("https://trackertest.org/");
+ break;
+ case "socialtracking":
+ createIframe(
+ "https://social-tracking.example.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
+ );
+ break;
+ case "cryptomining":
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ createIframe("http://cryptomining.example.com/");
+ break;
+ case "fingerprinting":
+ createIframe("https://fingerprinting.example.com/");
+ break;
+ case "more-tracking":
+ createIframe("https://itisatracker.org/");
+ break;
+ case "more-tracking-2":
+ createIframe("https://tracking.example.com/");
+ break;
+ case "cookie":
+ createIframe(
+ "https://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
+ );
+ break;
+ case "first-party-cookie":
+ // Since the content blocking log doesn't seem to get updated for
+ // top-level cookies right now, we just create an iframe with the
+ // first party domain...
+ createIframe(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
+ );
+ break;
+ case "third-party-cookie":
+ createIframe(
+ "https://test1.example.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
+ );
+ break;
+ case "image":
+ createImage(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs?type=image-no-cookie"
+ );
+ break;
+ case "window-open":
+ window.win = window.open(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs",
+ "_blank",
+ "width=100,height=100"
+ );
+ break;
+ case "window-close":
+ window.win.close();
+ window.win = null;
+ break;
+ }
+};
diff --git a/browser/base/content/test/protectionsUI/trackingPage.html b/browser/base/content/test/protectionsUI/trackingPage.html
new file mode 100644
index 0000000000..60ee20203b
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/trackingPage.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <script src="trackingAPI.js" type="text/javascript"></script>
+ </head>
+ <body>
+ <iframe src="http://trackertest.org/"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/referrer/browser.ini b/browser/base/content/test/referrer/browser.ini
new file mode 100644
index 0000000000..cfd1638771
--- /dev/null
+++ b/browser/base/content/test/referrer/browser.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+support-files =
+ file_referrer_policyserver.sjs
+ file_referrer_policyserver_attr.sjs
+ file_referrer_testserver.sjs
+ head.js
+
+[browser_referrer_click_pinned_tab.js]
+https_first_disabled = true
+[browser_referrer_middle_click.js]
+https_first_disabled = true
+[browser_referrer_middle_click_in_container.js]
+https_first_disabled = true
+[browser_referrer_open_link_in_container_tab.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_container_tab2.js]
+https_first_disabled = true
+skip-if =
+ os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_container_tab3.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_private.js]
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_open_link_in_tab.js]
+https_first_disabled = true
+skip-if =
+ os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_window.js]
+https_first_disabled = true
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_open_link_in_window_in_container.js]
+https_first_disabled = true
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_simple_click.js]
+https_first_disabled = true
diff --git a/browser/base/content/test/referrer/browser_referrer_click_pinned_tab.js b/browser/base/content/test/referrer/browser_referrer_click_pinned_tab.js
new file mode 100644
index 0000000000..2c0d14d687
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_click_pinned_tab.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 will open a new tab if clicking on a cross domain link in pinned tab
+// So, override the tests data in head.js, adding "cross: true".
+
+_referrerTests = [
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ fromScheme: "http://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ cross: true,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ result: "http://test1.example.com/", // origin
+ },
+ {
+ fromScheme: "https://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ cross: true,
+ result: "", // no referrer when downgrade
+ },
+ {
+ fromScheme: "https://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ policy: "origin",
+ cross: true,
+ result: "https://test1.example.com/", // origin, even on downgrade
+ },
+ {
+ fromScheme: "https://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ policy: "origin",
+ rel: "noreferrer",
+ cross: true,
+ result: "", // rel=noreferrer trumps meta-referrer
+ },
+ {
+ fromScheme: "https://",
+ toScheme: "https://",
+ policy: "no-referrer",
+ cross: true,
+ result: "", // same origin https://test1.example.com/browser
+ },
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ fromScheme: "http://",
+ toScheme: "https://",
+ policy: "no-referrer",
+ cross: true,
+ result: "", // cross origin http://test1.example.com
+ },
+];
+
+async function startClickPinnedTabTestCase(aTestNumber) {
+ info(
+ "browser_referrer_click_pinned_tab: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ let browser = gTestWindow.gBrowser;
+
+ browser.pinTab(browser.selectedTab);
+ someTabLoaded(gTestWindow).then(function (aNewTab) {
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ aNewTab,
+ startClickPinnedTabTestCase
+ );
+ });
+
+ clickTheLink(gTestWindow, "testlink", {});
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startClickPinnedTabTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_middle_click.js b/browser/base/content/test/referrer/browser_referrer_middle_click.js
new file mode 100644
index 0000000000..7686e461d0
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_middle_click.js
@@ -0,0 +1,25 @@
+// Tests referrer on middle-click navigation.
+// Middle-clicks on the link, which opens it in a new tab.
+
+function startMiddleClickTestCase(aTestNumber) {
+ info(
+ "browser_referrer_middle_click: " + getReferrerTestDescription(aTestNumber)
+ );
+ someTabLoaded(gTestWindow).then(function (aNewTab) {
+ BrowserTestUtils.switchTab(gTestWindow.gBrowser, aNewTab).then(() => {
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ aNewTab,
+ startMiddleClickTestCase
+ );
+ });
+ });
+
+ clickTheLink(gTestWindow, "testlink", { button: 1 });
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startMiddleClickTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js b/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js
new file mode 100644
index 0000000000..ec61a99804
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js
@@ -0,0 +1,33 @@
+// Tests referrer on middle-click navigation.
+// Middle-clicks on the link, which opens it in a new tab, same container.
+
+function startMiddleClickTestCase(aTestNumber) {
+ info(
+ "browser_referrer_middle_click: " + getReferrerTestDescription(aTestNumber)
+ );
+ someTabLoaded(gTestWindow).then(function (aNewTab) {
+ BrowserTestUtils.switchTab(gTestWindow.gBrowser, aNewTab).then(() => {
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ aNewTab,
+ startMiddleClickTestCase,
+ { userContextId: 3 }
+ );
+ });
+ });
+
+ clickTheLink(gTestWindow, "testlink", { button: 1 });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.userContext.enabled", true]] },
+ function () {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startMiddleClickTestCase, { userContextId: 3 });
+ }
+ );
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js
new file mode 100644
index 0000000000..3fe5df53c4
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js
@@ -0,0 +1,80 @@
+// Tests referrer on context menu navigation - open link in new container tab.
+// Selects "open link in new container tab" from the context menu.
+
+function getReferrerTest(aTestNumber) {
+ let testCase = _referrerTests[aTestNumber];
+ if (testCase) {
+ // We want all the referrer tests to fail!
+ testCase.result = "";
+ }
+
+ return testCase;
+}
+
+function startNewTabTestCase(aTestNumber) {
+ info(
+ "browser_referrer_open_link_in_container_tab: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ contextMenuOpened(gTestWindow, "testlink").then(function (aContextMenu) {
+ someTabLoaded(gTestWindow).then(function (aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ aNewTab,
+ startNewTabTestCase
+ );
+ });
+
+ let menu = gTestWindow.document.getElementById(
+ "context-openlinkinusercontext-menu"
+ );
+
+ let menupopup = menu.menupopup;
+ menu.addEventListener(
+ "popupshown",
+ function () {
+ is(menupopup.nodeType, Node.ELEMENT_NODE, "We have a menupopup.");
+ ok(menupopup.firstElementChild, "We have a first container entry.");
+
+ let firstContext = menupopup.firstElementChild;
+ is(
+ firstContext.nodeType,
+ Node.ELEMENT_NODE,
+ "We have a first container entry."
+ );
+ ok(
+ firstContext.hasAttribute("data-usercontextid"),
+ "We have a usercontextid value."
+ );
+
+ aContextMenu.addEventListener(
+ "popuphidden",
+ function () {
+ firstContext.doCommand();
+ },
+ { once: true }
+ );
+
+ aContextMenu.hidePopup();
+ },
+ { once: true }
+ );
+
+ menu.openMenu(true);
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.userContext.enabled", true]] },
+ function () {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase);
+ }
+ );
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js
new file mode 100644
index 0000000000..660b946e03
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js
@@ -0,0 +1,43 @@
+// Tests referrer on context menu navigation - open link in new container tab.
+// Selects "open link in new container tab" from the context menu.
+
+// The test runs from a container ID 1.
+// Output: we have the correct referrer policy applied.
+
+function startNewTabTestCase(aTestNumber) {
+ info(
+ "browser_referrer_open_link_in_container_tab: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ contextMenuOpened(gTestWindow, "testlink").then(function (aContextMenu) {
+ someTabLoaded(gTestWindow).then(function (aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ aNewTab,
+ startNewTabTestCase,
+ { userContextId: 1 }
+ );
+ });
+
+ doContextMenuCommand(
+ gTestWindow,
+ aContextMenu,
+ "context-openlinkincontainertab"
+ );
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.userContext.enabled", true]] },
+ function () {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase, { userContextId: 1 });
+ }
+ );
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js
new file mode 100644
index 0000000000..9233d7cf7a
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js
@@ -0,0 +1,81 @@
+// Tests referrer on context menu navigation - open link in new container tab.
+// Selects "open link in new container tab" from the context menu.
+
+// The test runs from a container ID 2.
+// Output: we have no referrer.
+
+getReferrerTest = getRemovedReferrerTest;
+
+function startNewTabTestCase(aTestNumber) {
+ info(
+ "browser_referrer_open_link_in_container_tab: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ contextMenuOpened(gTestWindow, "testlink").then(function (aContextMenu) {
+ someTabLoaded(gTestWindow).then(function (aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ aNewTab,
+ startNewTabTestCase,
+ { userContextId: 2 }
+ );
+ });
+
+ let menu = gTestWindow.document.getElementById(
+ "context-openlinkinusercontext-menu"
+ );
+
+ let menupopup = menu.menupopup;
+ menu.addEventListener(
+ "popupshown",
+ function () {
+ is(menupopup.nodeType, Node.ELEMENT_NODE, "We have a menupopup.");
+ ok(menupopup.firstElementChild, "We have a first container entry.");
+
+ let firstContext = menupopup.firstElementChild;
+ is(
+ firstContext.nodeType,
+ Node.ELEMENT_NODE,
+ "We have a first container entry."
+ );
+ ok(
+ firstContext.hasAttribute("data-usercontextid"),
+ "We have a usercontextid value."
+ );
+ is(
+ "0",
+ firstContext.getAttribute("data-usercontextid"),
+ "We have the right usercontextid value."
+ );
+
+ aContextMenu.addEventListener(
+ "popuphidden",
+ function () {
+ firstContext.doCommand();
+ },
+ { once: true }
+ );
+
+ aContextMenu.hidePopup();
+ },
+ { once: true }
+ );
+
+ menu.openMenu(true);
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.userContext.enabled", true]] },
+ function () {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase, { userContextId: 2 });
+ }
+ );
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js
new file mode 100644
index 0000000000..486b505565
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js
@@ -0,0 +1,33 @@
+// Tests referrer on context menu navigation - open link in new private window.
+// Selects "open link in new private window" from the context menu.
+
+// The test runs from a regular browsing window.
+// Output: we have no referrer.
+
+getReferrerTest = getRemovedReferrerTest;
+
+function startNewPrivateWindowTestCase(aTestNumber) {
+ info(
+ "browser_referrer_open_link_in_private: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ contextMenuOpened(gTestWindow, "testlink").then(function (aContextMenu) {
+ newWindowOpened().then(function (aNewWindow) {
+ BrowserTestUtils.firstBrowserLoaded(aNewWindow, false).then(function () {
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ aNewWindow,
+ null,
+ startNewPrivateWindowTestCase
+ );
+ });
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlinkprivate");
+ });
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewPrivateWindowTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js
new file mode 100644
index 0000000000..d790bd371b
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js
@@ -0,0 +1,27 @@
+// Tests referrer on context menu navigation - open link in new tab.
+// Selects "open link in new tab" from the context menu.
+
+function startNewTabTestCase(aTestNumber) {
+ info(
+ "browser_referrer_open_link_in_tab: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ contextMenuOpened(gTestWindow, "testlink").then(function (aContextMenu) {
+ someTabLoaded(gTestWindow).then(function (aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ aNewTab,
+ startNewTabTestCase
+ );
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlinkintab");
+ });
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js
new file mode 100644
index 0000000000..5c36470ded
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js
@@ -0,0 +1,28 @@
+// Tests referrer on context menu navigation - open link in new window.
+// Selects "open link in new window" from the context menu.
+
+function startNewWindowTestCase(aTestNumber) {
+ info(
+ "browser_referrer_open_link_in_window: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ contextMenuOpened(gTestWindow, "testlink").then(function (aContextMenu) {
+ newWindowOpened().then(function (aNewWindow) {
+ BrowserTestUtils.firstBrowserLoaded(aNewWindow, false).then(function () {
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ aNewWindow,
+ null,
+ startNewWindowTestCase
+ );
+ });
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlink");
+ });
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewWindowTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js
new file mode 100644
index 0000000000..2ba17cd449
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js
@@ -0,0 +1,39 @@
+// Tests referrer on context menu navigation - open link in new window.
+// Selects "open link in new window" from the context menu.
+
+// This test runs from a container tab. The new tab/window will be loaded in
+// the same container.
+
+function startNewWindowTestCase(aTestNumber) {
+ info(
+ "browser_referrer_open_link_in_window: " +
+ getReferrerTestDescription(aTestNumber)
+ );
+ contextMenuOpened(gTestWindow, "testlink").then(function (aContextMenu) {
+ newWindowOpened().then(function (aNewWindow) {
+ BrowserTestUtils.firstBrowserLoaded(aNewWindow, false).then(function () {
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ aNewWindow,
+ null,
+ startNewWindowTestCase,
+ { userContextId: 1 }
+ );
+ });
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlink");
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.userContext.enabled", true]] },
+ function () {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewWindowTestCase, { userContextId: 1 });
+ }
+ );
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_simple_click.js b/browser/base/content/test/referrer/browser_referrer_simple_click.js
new file mode 100644
index 0000000000..a9c3cb8d6f
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_simple_click.js
@@ -0,0 +1,27 @@
+// Tests referrer on simple click navigation.
+// Clicks on the link, which opens it in the same tab.
+
+function startSimpleClickTestCase(aTestNumber) {
+ info(
+ "browser_referrer_simple_click: " + getReferrerTestDescription(aTestNumber)
+ );
+ BrowserTestUtils.browserLoaded(
+ gTestWindow.gBrowser.selectedBrowser,
+ false,
+ url => url.endsWith("file_referrer_testserver.sjs")
+ ).then(function () {
+ checkReferrerAndStartNextTest(
+ aTestNumber,
+ null,
+ null,
+ startSimpleClickTestCase
+ );
+ });
+
+ clickTheLink(gTestWindow, "testlink", {});
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startSimpleClickTestCase);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_policyserver.sjs b/browser/base/content/test/referrer/file_referrer_policyserver.sjs
new file mode 100644
index 0000000000..6695d417f4
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver.sjs
@@ -0,0 +1,41 @@
+/**
+ * Renders a link with the provided referrer policy.
+ * Used in browser_referrer_*.js, bug 1113431.
+ * Arguments: ?scheme=http://&policy=origin&rel=noreferrer
+ */
+function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let scheme = query.get("scheme");
+ let policy = query.get("policy");
+ let rel = query.get("rel");
+ let cross = query.get("cross");
+
+ let host = cross ? "example.com" : "test1.example.com";
+ let linkUrl =
+ scheme +
+ host +
+ "/browser/browser/base/content/test/referrer/" +
+ "file_referrer_testserver.sjs";
+ let metaReferrerTag = policy
+ ? `<meta name='referrer' content='${policy}'>`
+ : "";
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ ${metaReferrerTag}
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <a id='testlink' href='${linkUrl}' ${rel ? ` rel='${rel}'` : ""}>
+ referrer test link</a>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs b/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs
new file mode 100644
index 0000000000..b0104f292e
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs
@@ -0,0 +1,41 @@
+/**
+ * Renders a link with the provided referrer policy.
+ * Used in browser_referrer_*.js, bug 1113431.
+ * Arguments: ?scheme=http://&policy=origin&rel=noreferrer
+ */
+function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let scheme = query.get("scheme");
+ let policy = query.get("policy");
+ let rel = query.get("rel");
+ let cross = query.get("cross");
+
+ let host = cross ? "example.com" : "test1.example.com";
+ let linkUrl =
+ scheme +
+ host +
+ "/browser/browser/base/content/test/referrer/" +
+ "file_referrer_testserver.sjs";
+
+ let referrerPolicy = policy ? `referrerpolicy="${policy}"` : "";
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <a id='testlink' href='${linkUrl}' ${referrerPolicy} ${
+ rel ? ` rel='${rel}'` : ""
+ }>
+ referrer test link</a>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_testserver.sjs b/browser/base/content/test/referrer/file_referrer_testserver.sjs
new file mode 100644
index 0000000000..3dac7811b1
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_testserver.sjs
@@ -0,0 +1,30 @@
+/**
+ * Renders the HTTP Referer header up to the second path slash.
+ * Used in browser_referrer_*.js, bug 1113431.
+ */
+function handleRequest(request, response) {
+ let referrer = "";
+ try {
+ referrer = request.getHeader("referer");
+ } catch (e) {
+ referrer = "";
+ }
+
+ // Strip it past the first path slash. Makes tests easier to read.
+ referrer = referrer.split("/").slice(0, 4).join("/");
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <div id='testdiv'>${referrer}</div>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/head.js b/browser/base/content/test/referrer/head.js
new file mode 100644
index 0000000000..c812d73e80
--- /dev/null
+++ b/browser/base/content/test/referrer/head.js
@@ -0,0 +1,311 @@
+const REFERRER_URL_BASE = "/browser/browser/base/content/test/referrer/";
+const REFERRER_POLICYSERVER_URL =
+ "test1.example.com" + REFERRER_URL_BASE + "file_referrer_policyserver.sjs";
+const REFERRER_POLICYSERVER_URL_ATTRIBUTE =
+ "test1.example.com" +
+ REFERRER_URL_BASE +
+ "file_referrer_policyserver_attr.sjs";
+
+var gTestWindow = null;
+var rounds = 0;
+
+// We test that the UI code propagates three pieces of state - the referrer URI
+// itself, the referrer policy, and the triggering principal. After that, we
+// trust nsIWebNavigation to do the right thing with the info it's given, which
+// is covered more exhaustively by dom/base/test/test_bug704320.html (which is
+// a faster content-only test). So, here, we limit ourselves to cases that
+// would break when the UI code drops either of these pieces; we don't try to
+// duplicate the entire cross-product test in bug 704320 - that would be slow,
+// especially when we're opening a new window for each case.
+var _referrerTests = [
+ // 1. Normal cases - no referrer policy, no special attributes.
+ // We expect a full referrer normally, and no referrer on downgrade.
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ fromScheme: "http://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ result: "http://test1.example.com/browser", // full referrer
+ },
+ {
+ fromScheme: "https://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ result: "", // no referrer when downgrade
+ },
+ // 2. Origin referrer policy - we expect an origin referrer,
+ // even on downgrade. But rel=noreferrer trumps this.
+ {
+ fromScheme: "https://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ policy: "origin",
+ result: "https://test1.example.com/", // origin, even on downgrade
+ },
+ {
+ fromScheme: "https://",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ toScheme: "http://",
+ policy: "origin",
+ rel: "noreferrer",
+ result: "", // rel=noreferrer trumps meta-referrer
+ },
+ // 3. XXX: using no-referrer here until we support all attribute values (bug 1178337)
+ // Origin-when-cross-origin policy - this depends on the triggering
+ // principal. We expect full referrer for same-origin requests,
+ // and origin referrer for cross-origin requests.
+ {
+ fromScheme: "https://",
+ toScheme: "https://",
+ policy: "no-referrer",
+ result: "", // same origin https://test1.example.com/browser
+ },
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ fromScheme: "http://",
+ toScheme: "https://",
+ policy: "no-referrer",
+ result: "", // cross origin http://test1.example.com
+ },
+];
+
+/**
+ * Returns the test object for a given test number.
+ * @param aTestNumber The test number - 0, 1, 2, ...
+ * @return The test object, or undefined if the number is out of range.
+ */
+function getReferrerTest(aTestNumber) {
+ return _referrerTests[aTestNumber];
+}
+
+/**
+ * Returns shimmed test object for a given test number.
+ *
+ * @param aTestNumber The test number - 0, 1, 2, ...
+ * @return The test object with result hard-coded to "",
+ * or undefined if the number is out of range.
+ */
+function getRemovedReferrerTest(aTestNumber) {
+ let testCase = _referrerTests[aTestNumber];
+ if (testCase) {
+ // We want all the referrer tests to fail!
+ testCase.result = "";
+ }
+
+ return testCase;
+}
+
+/**
+ * Returns a brief summary of the test, for logging.
+ * @param aTestNumber The test number - 0, 1, 2...
+ * @return The test description.
+ */
+function getReferrerTestDescription(aTestNumber) {
+ let test = getReferrerTest(aTestNumber);
+ return (
+ "policy=[" +
+ test.policy +
+ "] " +
+ "rel=[" +
+ test.rel +
+ "] " +
+ test.fromScheme +
+ " -> " +
+ test.toScheme
+ );
+}
+
+/**
+ * Clicks the link.
+ * @param aWindow The window to click the link in.
+ * @param aLinkId The id of the link element.
+ * @param aOptions The options for synthesizeMouseAtCenter.
+ */
+function clickTheLink(aWindow, aLinkId, aOptions) {
+ return BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + aLinkId,
+ aOptions,
+ aWindow.gBrowser.selectedBrowser
+ );
+}
+
+/**
+ * Extracts the referrer result from the target window.
+ * @param aWindow The window where the referrer target has loaded.
+ * @return {Promise}
+ * @resolves When extacted, with the text of the (trimmed) referrer.
+ */
+function referrerResultExtracted(aWindow) {
+ return SpecialPowers.spawn(aWindow.gBrowser.selectedBrowser, [], function () {
+ return content.document.getElementById("testdiv").textContent;
+ });
+}
+
+/**
+ * Waits for browser delayed startup to finish.
+ * @param aWindow The window to wait for.
+ * @return {Promise}
+ * @resolves When the window is loaded.
+ */
+function delayedStartupFinished(aWindow) {
+ return new Promise(function (resolve) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ resolve();
+ }
+ }, "browser-delayed-startup-finished");
+ });
+}
+
+/**
+ * Waits for some (any) tab to load. The caller triggers the load.
+ * @param aWindow The window where to wait for a tab to load.
+ * @return {Promise}
+ * @resolves With the tab once it's loaded.
+ */
+function someTabLoaded(aWindow) {
+ return BrowserTestUtils.waitForNewTab(gTestWindow.gBrowser, null, true);
+}
+
+/**
+ * Waits for a new window to open and load. The caller triggers the open.
+ * @return {Promise}
+ * @resolves With the new window once it's open and loaded.
+ */
+function newWindowOpened() {
+ return TestUtils.topicObserved("browser-delayed-startup-finished").then(
+ ([win]) => win
+ );
+}
+
+/**
+ * Opens the context menu.
+ * @param aWindow The window to open the context menu in.
+ * @param aLinkId The id of the link to open the context menu on.
+ * @return {Promise}
+ * @resolves With the menu popup when the context menu is open.
+ */
+function contextMenuOpened(aWindow, aLinkId) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ aWindow.document,
+ "popupshown"
+ );
+ // Simulate right-click.
+ clickTheLink(aWindow, aLinkId, { type: "contextmenu", button: 2 });
+ return popupShownPromise.then(e => e.target);
+}
+
+/**
+ * Performs a context menu command.
+ * @param aWindow The window with the already open context menu.
+ * @param aMenu The menu popup to hide.
+ * @param aItemId The id of the menu item to activate.
+ */
+function doContextMenuCommand(aWindow, aMenu, aItemId) {
+ let command = aWindow.document.getElementById(aItemId);
+ command.doCommand();
+ aMenu.hidePopup();
+}
+
+/**
+ * Loads a single test case, i.e., a source url into gTestWindow.
+ * @param aTestNumber The test case number - 0, 1, 2...
+ * @return {Promise}
+ * @resolves When the source url for this test case is loaded.
+ */
+function referrerTestCaseLoaded(aTestNumber, aParams) {
+ let test = getReferrerTest(aTestNumber);
+ let server =
+ rounds == 0
+ ? REFERRER_POLICYSERVER_URL
+ : REFERRER_POLICYSERVER_URL_ATTRIBUTE;
+ let url =
+ test.fromScheme +
+ server +
+ "?scheme=" +
+ escape(test.toScheme) +
+ "&policy=" +
+ escape(test.policy || "") +
+ "&rel=" +
+ escape(test.rel || "") +
+ "&cross=" +
+ escape(test.cross || "");
+ let browser = gTestWindow.gBrowser;
+ return BrowserTestUtils.openNewForegroundTab(
+ browser,
+ () => {
+ browser.selectedTab = BrowserTestUtils.addTab(browser, url, aParams);
+ },
+ false,
+ true
+ );
+}
+
+/**
+ * Checks the result of the referrer test, and moves on to the next test.
+ * @param aTestNumber The test number - 0, 1, 2, ...
+ * @param aNewWindow The new window where the referrer target opened, or null.
+ * @param aNewTab The new tab where the referrer target opened, or null.
+ * @param aStartTestCase The callback to start the next test, aTestNumber + 1.
+ */
+function checkReferrerAndStartNextTest(
+ aTestNumber,
+ aNewWindow,
+ aNewTab,
+ aStartTestCase,
+ aParams = {}
+) {
+ referrerResultExtracted(aNewWindow || gTestWindow).then(function (result) {
+ // Compare the actual result against the expected one.
+ let test = getReferrerTest(aTestNumber);
+ let desc = getReferrerTestDescription(aTestNumber);
+ is(result, test.result, desc);
+
+ // Clean up - close new tab / window, and then the source tab.
+ aNewTab && (aNewWindow || gTestWindow).gBrowser.removeTab(aNewTab);
+ aNewWindow && aNewWindow.close();
+ is(gTestWindow.gBrowser.tabs.length, 2, "two tabs open");
+ gTestWindow.gBrowser.removeTab(gTestWindow.gBrowser.tabs[1]);
+
+ // Move on to the next test. Or finish if we're done.
+ var nextTestNumber = aTestNumber + 1;
+ if (getReferrerTest(nextTestNumber)) {
+ referrerTestCaseLoaded(nextTestNumber, aParams).then(function () {
+ aStartTestCase(nextTestNumber);
+ });
+ } else if (rounds == 0) {
+ nextTestNumber = 0;
+ rounds = 1;
+ referrerTestCaseLoaded(nextTestNumber, aParams).then(function () {
+ aStartTestCase(nextTestNumber);
+ });
+ } else {
+ finish();
+ }
+ });
+}
+
+/**
+ * Fires up the complete referrer test.
+ * @param aStartTestCase The callback to start a single test case, called with
+ * the test number - 0, 1, 2... Needs to trigger the navigation from the source
+ * page, and call checkReferrerAndStartNextTest() when the target is loaded.
+ */
+function startReferrerTest(aStartTestCase, params = {}) {
+ waitForExplicitFinish();
+
+ // Open the window where we'll load the source URLs.
+ gTestWindow = openDialog(location, "", "chrome,all,dialog=no", "about:blank");
+ registerCleanupFunction(function () {
+ gTestWindow && gTestWindow.close();
+ });
+
+ // Load and start the first test.
+ delayedStartupFinished(gTestWindow).then(function () {
+ referrerTestCaseLoaded(0, params).then(function () {
+ aStartTestCase(0);
+ });
+ });
+}
diff --git a/browser/base/content/test/sanitize/browser.ini b/browser/base/content/test/sanitize/browser.ini
new file mode 100644
index 0000000000..2a0a77a288
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files=
+ head.js
+ dummy.js
+ dummy_page.html
+
+[browser_cookiePermission.js]
+[browser_cookiePermission_aboutURL.js]
+[browser_cookiePermission_containers.js]
+[browser_cookiePermission_subDomains.js]
+[browser_purgehistory_clears_sh.js]
+[browser_sanitize-cookie-exceptions.js]
+[browser_sanitize-formhistory.js]
+[browser_sanitize-history.js]
+[browser_sanitize-offlineData.js]
+[browser_sanitize-passwordDisabledHosts.js]
+[browser_sanitize-sitepermissions.js]
+[browser_sanitize-timespans.js]
+[browser_sanitizeDialog.js]
diff --git a/browser/base/content/test/sanitize/browser_cookiePermission.js b/browser/base/content/test/sanitize/browser_cookiePermission.js
new file mode 100644
index 0000000000..9fadfa91db
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission.js
@@ -0,0 +1 @@
+runAllCookiePermissionTests({ name: "default", oa: {} });
diff --git a/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js b/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js
new file mode 100644
index 0000000000..7ae8ec158e
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js
@@ -0,0 +1,101 @@
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+function checkDataForAboutURL() {
+ return new Promise(resolve => {
+ let data = true;
+ let uri = Services.io.newURI("about:newtab");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
+ request.onupgradeneeded = function (e) {
+ data = false;
+ };
+ request.onsuccess = function (e) {
+ resolve(data);
+ };
+ });
+}
+
+add_task(async function deleteStorageInAboutURL() {
+ info("Test about:newtab");
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sanitizer.loglevel", "All"]],
+ });
+
+ // Let's create a tab with some data.
+ await SiteDataTestUtils.addToIndexedDB("about:newtab", "foo", "bar", {});
+
+ ok(await checkDataForAboutURL(), "We have data for about:newtab");
+
+ // Cleaning up.
+ await Sanitizer.runSanitizeOnShutdown();
+
+ ok(await checkDataForAboutURL(), "about:newtab data is not deleted.");
+
+ // Clean up.
+ await Sanitizer.sanitize(["cookies", "offlineApps"]);
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:newtab"
+ );
+ await new Promise(aResolve => {
+ let req = Services.qms.clearStoragesForPrincipal(principal);
+ req.callback = () => {
+ aResolve();
+ };
+ });
+});
+
+add_task(async function deleteStorageOnlyCustomPermissionInAboutURL() {
+ info("Test about:newtab + permissions");
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sanitizer.loglevel", "All"]],
+ });
+
+ // Custom permission without considering OriginAttributes
+ let uri = Services.io.newURI("about:newtab");
+ PermissionTestUtils.add(uri, "cookie", Ci.nsICookiePermission.ACCESS_SESSION);
+
+ // Let's create a tab with some data.
+ await SiteDataTestUtils.addToIndexedDB("about:newtab", "foo", "bar", {});
+
+ ok(await checkDataForAboutURL(), "We have data for about:newtab");
+
+ // Cleaning up.
+ await Sanitizer.runSanitizeOnShutdown();
+
+ ok(await checkDataForAboutURL(), "about:newtab data is not deleted.");
+
+ // Clean up.
+ await Sanitizer.sanitize(["cookies", "offlineApps"]);
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:newtab"
+ );
+ await new Promise(aResolve => {
+ let req = Services.qms.clearStoragesForPrincipal(principal);
+ req.callback = () => {
+ aResolve();
+ };
+ });
+
+ PermissionTestUtils.remove(uri, "cookie");
+});
diff --git a/browser/base/content/test/sanitize/browser_cookiePermission_containers.js b/browser/base/content/test/sanitize/browser_cookiePermission_containers.js
new file mode 100644
index 0000000000..236c0913e8
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_containers.js
@@ -0,0 +1 @@
+runAllCookiePermissionTests({ name: "container", oa: { userContextId: 1 } });
diff --git a/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js
new file mode 100644
index 0000000000..b4b62be110
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js
@@ -0,0 +1,290 @@
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ["privacy.clearOnShutdown.cookies", true],
+ ["privacy.clearOnShutdown.offlineApps", true],
+ ["privacy.clearOnShutdown.cache", false],
+ ["privacy.clearOnShutdown.sessions", false],
+ ["privacy.clearOnShutdown.history", false],
+ ["privacy.clearOnShutdown.formdata", false],
+ ["privacy.clearOnShutdown.downloads", false],
+ ["privacy.clearOnShutdown.siteSettings", false],
+ ["browser.sanitizer.loglevel", "All"],
+ ],
+ });
+});
+// 2 domains: www.mozilla.org (session-only) mozilla.org (allowed) - after the
+// cleanp, mozilla.org must have data.
+add_task(async function subDomains1() {
+ info("Test subdomains and custom setting");
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ // Domains and data
+ let originA = "https://www.mozilla.org";
+ PermissionTestUtils.add(
+ originA,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originA });
+ await SiteDataTestUtils.addToIndexedDB(originA);
+
+ let originB = "https://mozilla.org";
+ PermissionTestUtils.add(
+ originB,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originB });
+ await SiteDataTestUtils.addToIndexedDB(originB);
+
+ // Check
+ ok(SiteDataTestUtils.hasCookies(originA), "We have cookies for " + originA);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originA),
+ "We have IDB for " + originA
+ );
+ ok(SiteDataTestUtils.hasCookies(originB), "We have cookies for " + originB);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originB),
+ "We have IDB for " + originB
+ );
+
+ // Cleaning up
+ await Sanitizer.runSanitizeOnShutdown();
+
+ // Check again
+ ok(
+ !SiteDataTestUtils.hasCookies(originA),
+ "We should not have cookies for " + originA
+ );
+ ok(
+ !(await SiteDataTestUtils.hasIndexedDB(originA)),
+ "We should not have IDB for " + originA
+ );
+ ok(SiteDataTestUtils.hasCookies(originB), "We have cookies for " + originB);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originB),
+ "We have IDB for " + originB
+ );
+
+ // Cleaning up permissions
+ PermissionTestUtils.remove(originA, "cookie");
+ PermissionTestUtils.remove(originB, "cookie");
+});
+
+// session only cookie life-time, 2 domains (sub.mozilla.org, www.mozilla.org),
+// only the former has a cookie permission.
+add_task(async function subDomains2() {
+ info("Test subdomains and custom setting with cookieBehavior == 2");
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ // Domains and data
+ let originA = "https://sub.mozilla.org";
+ PermissionTestUtils.add(
+ originA,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originA });
+ await SiteDataTestUtils.addToIndexedDB(originA);
+
+ let originB = "https://www.mozilla.org";
+
+ SiteDataTestUtils.addToCookies({ origin: originB });
+ await SiteDataTestUtils.addToIndexedDB(originB);
+
+ // Check
+ ok(SiteDataTestUtils.hasCookies(originA), "We have cookies for " + originA);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originA),
+ "We have IDB for " + originA
+ );
+ ok(SiteDataTestUtils.hasCookies(originB), "We have cookies for " + originB);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originB),
+ "We have IDB for " + originB
+ );
+
+ // Cleaning up
+ await Sanitizer.runSanitizeOnShutdown();
+
+ // Check again
+ ok(SiteDataTestUtils.hasCookies(originA), "We have cookies for " + originA);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originA),
+ "We have IDB for " + originA
+ );
+ ok(
+ !SiteDataTestUtils.hasCookies(originB),
+ "We should not have cookies for " + originB
+ );
+ ok(
+ !(await SiteDataTestUtils.hasIndexedDB(originB)),
+ "We should not have IDB for " + originB
+ );
+
+ // Cleaning up permissions
+ PermissionTestUtils.remove(originA, "cookie");
+});
+
+// session only cookie life-time, 3 domains (sub.mozilla.org, www.mozilla.org, mozilla.org),
+// only the former has a cookie permission. Both sub.mozilla.org and mozilla.org should
+// be sustained.
+add_task(async function subDomains3() {
+ info(
+ "Test base domain and subdomains and custom setting with cookieBehavior == 2"
+ );
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ // Domains and data
+ let originA = "https://sub.mozilla.org";
+ PermissionTestUtils.add(
+ originA,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+ SiteDataTestUtils.addToCookies({ origin: originA });
+ await SiteDataTestUtils.addToIndexedDB(originA);
+
+ let originB = "https://mozilla.org";
+ SiteDataTestUtils.addToCookies({ origin: originB });
+ await SiteDataTestUtils.addToIndexedDB(originB);
+
+ let originC = "https://www.mozilla.org";
+ SiteDataTestUtils.addToCookies({ origin: originC });
+ await SiteDataTestUtils.addToIndexedDB(originC);
+
+ // Check
+ ok(SiteDataTestUtils.hasCookies(originA), "We have cookies for " + originA);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originA),
+ "We have IDB for " + originA
+ );
+ ok(SiteDataTestUtils.hasCookies(originB), "We have cookies for " + originB);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originB),
+ "We have IDB for " + originB
+ );
+ ok(SiteDataTestUtils.hasCookies(originC), "We have cookies for " + originC);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originC),
+ "We have IDB for " + originC
+ );
+
+ // Cleaning up
+ await Sanitizer.runSanitizeOnShutdown();
+
+ // Check again
+ ok(SiteDataTestUtils.hasCookies(originA), "We have cookies for " + originA);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originA),
+ "We have IDB for " + originA
+ );
+ ok(SiteDataTestUtils.hasCookies(originB), "We have cookies for " + originB);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originB),
+ "We have IDB for " + originB
+ );
+ ok(
+ !SiteDataTestUtils.hasCookies(originC),
+ "We should not have cookies for " + originC
+ );
+ ok(
+ !(await SiteDataTestUtils.hasIndexedDB(originC)),
+ "We should not have IDB for " + originC
+ );
+
+ // Cleaning up permissions
+ PermissionTestUtils.remove(originA, "cookie");
+});
+
+// clear on shutdown, 3 domains (sub.sub.mozilla.org, sub.mozilla.org, mozilla.org),
+// only the former has a cookie permission. Both sub.mozilla.org and mozilla.org should
+// be sustained due to Permission of sub.sub.mozilla.org
+add_task(async function subDomains4() {
+ info("Test subdomain cookie permission inheritance with two subdomains");
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ // Domains and data
+ let originA = "https://sub.sub.mozilla.org";
+ PermissionTestUtils.add(
+ originA,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+ SiteDataTestUtils.addToCookies({ origin: originA });
+ await SiteDataTestUtils.addToIndexedDB(originA);
+
+ let originB = "https://sub.mozilla.org";
+ SiteDataTestUtils.addToCookies({ origin: originB });
+ await SiteDataTestUtils.addToIndexedDB(originB);
+
+ let originC = "https://mozilla.org";
+ SiteDataTestUtils.addToCookies({ origin: originC });
+ await SiteDataTestUtils.addToIndexedDB(originC);
+
+ // Check
+ ok(SiteDataTestUtils.hasCookies(originA), "We have cookies for " + originA);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originA),
+ "We have IDB for " + originA
+ );
+ ok(SiteDataTestUtils.hasCookies(originB), "We have cookies for " + originB);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originB),
+ "We have IDB for " + originB
+ );
+ ok(SiteDataTestUtils.hasCookies(originC), "We have cookies for " + originC);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originC),
+ "We have IDB for " + originC
+ );
+
+ // Cleaning up
+ await Sanitizer.runSanitizeOnShutdown();
+
+ // Check again
+ ok(SiteDataTestUtils.hasCookies(originA), "We have cookies for " + originA);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originA),
+ "We have IDB for " + originA
+ );
+ ok(SiteDataTestUtils.hasCookies(originB), "We have cookies for " + originB);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originB),
+ "We have IDB for " + originB
+ );
+ ok(SiteDataTestUtils.hasCookies(originC), "We have cookies for " + originC);
+ ok(
+ await SiteDataTestUtils.hasIndexedDB(originC),
+ "We have IDB for " + originC
+ );
+
+ // Cleaning up permissions
+ PermissionTestUtils.remove(originA, "cookie");
+});
diff --git a/browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js b/browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js
new file mode 100644
index 0000000000..abf11017dd
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const url =
+ "https://example.org/browser/browser/base/content/test/sanitize/dummy_page.html";
+
+add_task(async function purgeHistoryTest() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function purgeHistoryTestInner(browser) {
+ let backButton = browser.ownerDocument.getElementById("Browser:Back");
+ let forwardButton =
+ browser.ownerDocument.getElementById("Browser:Forward");
+
+ ok(
+ !browser.webNavigation.canGoBack,
+ "Initial value for webNavigation.canGoBack"
+ );
+ ok(
+ !browser.webNavigation.canGoForward,
+ "Initial value for webNavigation.canGoBack"
+ );
+ ok(backButton.hasAttribute("disabled"), "Back button is disabled");
+ ok(forwardButton.hasAttribute("disabled"), "Forward button is disabled");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let startHistory = content.history.length;
+ content.history.pushState({}, "");
+ content.history.pushState({}, "");
+ content.history.back();
+ await new Promise(function (r) {
+ content.onpopstate = r;
+ });
+ let newHistory = content.history.length;
+ Assert.equal(startHistory, 1, "Initial SHistory size");
+ Assert.equal(newHistory, 3, "New SHistory size");
+ });
+
+ ok(
+ browser.webNavigation.canGoBack,
+ "New value for webNavigation.canGoBack"
+ );
+ ok(
+ browser.webNavigation.canGoForward,
+ "New value for webNavigation.canGoForward"
+ );
+ ok(!backButton.hasAttribute("disabled"), "Back button was enabled");
+ ok(!forwardButton.hasAttribute("disabled"), "Forward button was enabled");
+
+ await Sanitizer.sanitize(["history"]);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ Assert.equal(content.history.length, 1, "SHistory correctly cleared");
+ });
+
+ ok(
+ !browser.webNavigation.canGoBack,
+ "webNavigation.canGoBack correctly cleared"
+ );
+ ok(
+ !browser.webNavigation.canGoForward,
+ "webNavigation.canGoForward correctly cleared"
+ );
+ ok(backButton.hasAttribute("disabled"), "Back button was disabled");
+ ok(forwardButton.hasAttribute("disabled"), "Forward button was disabled");
+ }
+ );
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-cookie-exceptions.js b/browser/base/content/test/sanitize/browser_sanitize-cookie-exceptions.js
new file mode 100644
index 0000000000..a3ab8aa0f3
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-cookie-exceptions.js
@@ -0,0 +1,274 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const oneHour = 3600000000;
+
+add_task(async function sanitizeWithExceptionsOnShutdown() {
+ info(
+ "Test that cookies that are marked as allowed from the user do not get \
+ cleared when cleaning on shutdown is done"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sanitizer.loglevel", "All"],
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ],
+ });
+
+ // Clean up before start
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ let originALLOW = "https://mozilla.org";
+ PermissionTestUtils.add(
+ originALLOW,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ let originDENY = "https://example123.com";
+ PermissionTestUtils.add(
+ originDENY,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_DENY
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originALLOW });
+ ok(
+ SiteDataTestUtils.hasCookies(originALLOW),
+ "We have cookies for " + originALLOW
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originDENY });
+ ok(
+ SiteDataTestUtils.hasCookies(originDENY),
+ "We have cookies for " + originDENY
+ );
+
+ await Sanitizer.runSanitizeOnShutdown();
+
+ ok(
+ SiteDataTestUtils.hasCookies(originALLOW),
+ "We should have cookies for " + originALLOW
+ );
+
+ ok(
+ !SiteDataTestUtils.hasCookies(originDENY),
+ "We should not have cookies for " + originDENY
+ );
+});
+
+add_task(async function sanitizeNoExceptionsInTimeRange() {
+ info(
+ "Test that no exceptions are made when not clearing on shutdown, e.g. clearing within a range"
+ );
+
+ // Clean up before start
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ let originALLOW = "https://mozilla.org";
+ PermissionTestUtils.add(
+ originALLOW,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ let originDENY = "https://bar123.com";
+ PermissionTestUtils.add(
+ originDENY,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_DENY
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originALLOW });
+ ok(
+ SiteDataTestUtils.hasCookies(originALLOW),
+ "We have cookies for " + originALLOW
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originDENY });
+ ok(
+ SiteDataTestUtils.hasCookies(originDENY),
+ "We have cookies for " + originDENY
+ );
+
+ let to = Date.now() * 1000;
+ let from = to - oneHour;
+ await Sanitizer.sanitize(["cookies"], { range: [from, to] });
+
+ ok(
+ !SiteDataTestUtils.hasCookies(originALLOW),
+ "We should not have cookies for " + originALLOW
+ );
+
+ ok(
+ !SiteDataTestUtils.hasCookies(originDENY),
+ "We should not have cookies for " + originDENY
+ );
+});
+
+add_task(async function sanitizeWithExceptionsOnStartup() {
+ info(
+ "Test that cookies that are marked as allowed from the user do not get \
+ cleared when cleaning on startup is done, for example after a crash"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sanitizer.loglevel", "All"],
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ],
+ });
+
+ // Clean up before start
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ let originALLOW = "https://mozilla.org";
+ PermissionTestUtils.add(
+ originALLOW,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ let originDENY = "https://example123.com";
+ PermissionTestUtils.add(
+ originDENY,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_DENY
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originALLOW });
+ ok(
+ SiteDataTestUtils.hasCookies(originALLOW),
+ "We have cookies for " + originALLOW
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originDENY });
+ ok(
+ SiteDataTestUtils.hasCookies(originDENY),
+ "We have cookies for " + originDENY
+ );
+
+ let pendingSanitizations = [
+ {
+ id: "shutdown",
+ itemsToClear: ["cookies"],
+ options: {},
+ },
+ ];
+ Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
+ Services.prefs.setStringPref(
+ Sanitizer.PREF_PENDING_SANITIZATIONS,
+ JSON.stringify(pendingSanitizations)
+ );
+
+ await Sanitizer.onStartup();
+
+ ok(
+ SiteDataTestUtils.hasCookies(originALLOW),
+ "We should have cookies for " + originALLOW
+ );
+
+ ok(
+ !SiteDataTestUtils.hasCookies(originDENY),
+ "We should not have cookies for " + originDENY
+ );
+});
+
+add_task(async function sanitizeWithSessionExceptionsOnShutdown() {
+ info(
+ "Test that cookies that are marked as allowed on session is cleared when sanitizeOnShutdown is false"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sanitizer.loglevel", "All"],
+ ["privacy.sanitize.sanitizeOnShutdown", false],
+ ],
+ });
+
+ // Clean up before start
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ let originAllowSession = "https://mozilla.org";
+ PermissionTestUtils.add(
+ originAllowSession,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: originAllowSession });
+ ok(
+ SiteDataTestUtils.hasCookies(originAllowSession),
+ "We have cookies for " + originAllowSession
+ );
+
+ await Sanitizer.runSanitizeOnShutdown();
+
+ ok(
+ !SiteDataTestUtils.hasCookies(originAllowSession),
+ "We should not have cookies for " + originAllowSession
+ );
+});
+
+add_task(async function sanitizeWithManySessionExceptionsOnShutdown() {
+ info(
+ "Test that lots of allowed on session exceptions are cleared when sanitizeOnShutdown is false"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.sanitize.sanitizeOnShutdown", false],
+ ["dom.quotaManager.backgroundTask.enabled", true],
+ ],
+ });
+
+ // Clean up before start
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ info("Setting cookies");
+
+ const origins = new Array(300)
+ .fill(0)
+ .map((v, i) => `https://mozilla${i}.org`);
+
+ for (const origin of origins) {
+ PermissionTestUtils.add(
+ origin,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+ SiteDataTestUtils.addToCookies({ origin });
+ }
+
+ ok(
+ origins.every(origin => SiteDataTestUtils.hasCookies(origin)),
+ "All origins have cookies"
+ );
+
+ info("Running sanitization");
+
+ await Sanitizer.runSanitizeOnShutdown();
+
+ ok(
+ origins.every(origin => !SiteDataTestUtils.hasCookies(origin)),
+ "All origins lost cookies"
+ );
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-formhistory.js b/browser/base/content/test/sanitize/browser_sanitize-formhistory.js
new file mode 100644
index 0000000000..5547a88d64
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-formhistory.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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() {
+ // This test relies on the form history being empty to start with delete
+ // all the items first.
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ await FormHistory.update({ op: "remove" });
+
+ // Sanitize now so we can test the baseline point.
+ await Sanitizer.sanitize(["formdata"]);
+ await gFindBarPromise;
+ ok(!gFindBar.hasTransactions, "pre-test baseline for sanitizer");
+
+ gFindBar.getElement("findbar-textbox").value = "m";
+ ok(gFindBar.hasTransactions, "formdata can be cleared after input");
+
+ await Sanitizer.sanitize(["formdata"]);
+ is(
+ gFindBar.getElement("findbar-textbox").value,
+ "",
+ "findBar textbox should be empty after sanitize"
+ );
+ ok(!gFindBar.hasTransactions, "No transactions after sanitize");
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-history.js b/browser/base/content/test/sanitize/browser_sanitize-history.js
new file mode 100644
index 0000000000..5ca2843174
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-history.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that sanitizing history will clear storage access permissions
+// for sites without cookies or site data.
+add_task(async function sanitizeStorageAccessPermissions() {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ await SiteDataTestUtils.addToIndexedDB("https://sub.example.org");
+ await SiteDataTestUtils.addToCookies({ origin: "https://example.com" });
+
+ PermissionTestUtils.add(
+ "https://example.org",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://example.com",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ "http://mochi.test",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Add some time in between taking the snapshot of the timestamp
+ // to avoid flakyness.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 100));
+ let timestamp = Date.now();
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 100));
+
+ PermissionTestUtils.add(
+ "http://example.net",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await Sanitizer.sanitize(["history"], {
+ // Sanitizer and ClearDataService work with time range in PRTime (microseconds)
+ range: [timestamp * 1000, Date.now() * 1000],
+ });
+
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://example.net",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://mochi.test",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.com",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.org",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await Sanitizer.sanitize(["history"]);
+
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://mochi.test",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://example.net",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.com",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.org",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await Sanitizer.sanitize(["history", "siteSettings"]);
+
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://mochi.test",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.com",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.org",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-offlineData.js b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
new file mode 100644
index 0000000000..64604684b1
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 380852 - Delete permission manager entries in Clear Recent History
+
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "sas",
+ "@mozilla.org/storage/activity-service;1",
+ "nsIStorageActivityService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "swm",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager"
+);
+
+const oneHour = 3600000000;
+const fiveHours = oneHour * 5;
+
+const itemsToClear = ["cookies", "offlineApps"];
+
+function waitForUnregister(host) {
+ return new Promise(resolve => {
+ let listener = {
+ onUnregister: registration => {
+ if (registration.principal.host != host) {
+ return;
+ }
+ swm.removeListener(listener);
+ resolve(registration);
+ },
+ };
+ swm.addListener(listener);
+ });
+}
+
+async function createData(host) {
+ let origin = "https://" + host;
+ let dummySWURL =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ "dummy.js";
+
+ await SiteDataTestUtils.addToIndexedDB(origin);
+ await SiteDataTestUtils.addServiceWorker(dummySWURL);
+}
+
+function moveOriginInTime(principals, endDate, host) {
+ for (let i = 0; i < principals.length; ++i) {
+ let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
+ if (principal.host == host) {
+ sas.moveOriginInTime(principal, endDate - fiveHours);
+ return true;
+ }
+ }
+ return false;
+}
+
+add_task(async function testWithRange() {
+ // We have intermittent occurrences of NS_ERROR_ABORT being
+ // thrown at closing database instances when using Santizer.sanitize().
+ // This does not seem to impact cleanup, since our tests run fine anyway.
+ PromiseTestUtils.allowMatchingRejectionsGlobally(/NS_ERROR_ABORT/);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ // The service may have picked up activity from prior tests in this run.
+ // Clear it.
+ sas.testOnlyReset();
+
+ let endDate = Date.now() * 1000;
+ let principals = sas.getActiveOrigins(endDate - oneHour, endDate);
+ is(principals.length, 0, "starting from clear activity state");
+
+ info("sanitize: " + itemsToClear.join(", "));
+ await Sanitizer.sanitize(itemsToClear, { ignoreTimespan: false });
+
+ await createData("example.org");
+ await createData("example.com");
+
+ endDate = Date.now() * 1000;
+ principals = sas.getActiveOrigins(endDate - oneHour, endDate);
+ ok(!!principals, "We have an active origin.");
+ ok(principals.length >= 2, "We have an active origin.");
+
+ let found = 0;
+ for (let i = 0; i < principals.length; ++i) {
+ let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
+ if (principal.host == "example.org" || principal.host == "example.com") {
+ found++;
+ }
+ }
+
+ is(found, 2, "Our origins are active.");
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.org"),
+ "We have indexedDB data for example.org"
+ );
+ ok(
+ SiteDataTestUtils.hasServiceWorkers("https://example.org"),
+ "We have serviceWorker data for example.org"
+ );
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.com"),
+ "We have indexedDB data for example.com"
+ );
+ ok(
+ SiteDataTestUtils.hasServiceWorkers("https://example.com"),
+ "We have serviceWorker data for example.com"
+ );
+
+ // Let's move example.com in the past.
+ ok(
+ moveOriginInTime(principals, endDate, "example.com"),
+ "Operation completed!"
+ );
+
+ let p = waitForUnregister("example.org");
+
+ // Clear it
+ info("sanitize: " + itemsToClear.join(", "));
+ await Sanitizer.sanitize(itemsToClear, { ignoreTimespan: false });
+ await p;
+
+ ok(
+ !(await SiteDataTestUtils.hasIndexedDB("https://example.org")),
+ "We don't have indexedDB data for example.org"
+ );
+ ok(
+ !SiteDataTestUtils.hasServiceWorkers("https://example.org"),
+ "We don't have serviceWorker data for example.org"
+ );
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.com"),
+ "We still have indexedDB data for example.com"
+ );
+ ok(
+ SiteDataTestUtils.hasServiceWorkers("https://example.com"),
+ "We still have serviceWorker data for example.com"
+ );
+
+ // We have to move example.com in the past because how we check IDB triggers
+ // a storage activity.
+ ok(
+ moveOriginInTime(principals, endDate, "example.com"),
+ "Operation completed!"
+ );
+
+ // Let's call the clean up again.
+ info("sanitize again to ensure clearing doesn't expand the activity scope");
+ await Sanitizer.sanitize(itemsToClear, { ignoreTimespan: false });
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.com"),
+ "We still have indexedDB data for example.com"
+ );
+ ok(
+ SiteDataTestUtils.hasServiceWorkers("https://example.com"),
+ "We still have serviceWorker data for example.com"
+ );
+
+ ok(
+ !(await SiteDataTestUtils.hasIndexedDB("https://example.org")),
+ "We don't have indexedDB data for example.org"
+ );
+ ok(
+ !SiteDataTestUtils.hasServiceWorkers("https://example.org"),
+ "We don't have serviceWorker data for example.org"
+ );
+
+ sas.testOnlyReset();
+
+ // Clean up.
+ await SiteDataTestUtils.clear();
+});
+
+add_task(async function testExceptionsOnShutdown() {
+ await createData("example.org");
+ await createData("example.com");
+
+ // Set exception for example.org to not get cleaned
+ let originALLOW = "https://example.org";
+ PermissionTestUtils.add(
+ originALLOW,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.org"),
+ "We have indexedDB data for example.org"
+ );
+ ok(
+ SiteDataTestUtils.hasServiceWorkers("https://example.org"),
+ "We have serviceWorker data for example.org"
+ );
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.com"),
+ "We have indexedDB data for example.com"
+ );
+ ok(
+ SiteDataTestUtils.hasServiceWorkers("https://example.com"),
+ "We have serviceWorker data for example.com"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sanitizer.loglevel", "All"],
+ ["privacy.clearOnShutdown.offlineApps", true],
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ],
+ });
+ // Clear it
+ await Sanitizer.runSanitizeOnShutdown();
+ // Data for example.org should not have been cleared
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.org"),
+ "We still have indexedDB data for example.org"
+ );
+ ok(
+ SiteDataTestUtils.hasServiceWorkers("https://example.org"),
+ "We still have serviceWorker data for example.org"
+ );
+ // Data for example.com should be cleared
+ ok(
+ !(await SiteDataTestUtils.hasIndexedDB("https://example.com")),
+ "We don't have indexedDB data for example.com"
+ );
+ ok(
+ !SiteDataTestUtils.hasServiceWorkers("https://example.com"),
+ "We don't have serviceWorker data for example.com"
+ );
+
+ // Clean up
+ await SiteDataTestUtils.clear();
+ Services.perms.removeAll();
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js b/browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js
new file mode 100644
index 0000000000..305fe37e7e
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js
@@ -0,0 +1,28 @@
+// Bug 474792 - Clear "Never remember passwords for this site" when
+// clearing site-specific settings in Clear Recent History dialog
+
+add_task(async function () {
+ // getLoginSavingEnabled always returns false if password capture is disabled.
+ await SpecialPowers.pushPrefEnv({ set: [["signon.rememberSignons", true]] });
+
+ // Add a disabled host
+ Services.logins.setLoginSavingEnabled("https://example.com", false);
+ // Sanity check
+ is(
+ Services.logins.getLoginSavingEnabled("https://example.com"),
+ false,
+ "example.com should be disabled for password saving since we haven't cleared that yet."
+ );
+
+ // Clear it
+ await Sanitizer.sanitize(["siteSettings"], { ignoreTimespan: false });
+
+ // Make sure it's gone
+ is(
+ Services.logins.getLoginSavingEnabled("https://example.com"),
+ true,
+ "example.com should be enabled for password saving again now that we've cleared."
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js b/browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js
new file mode 100644
index 0000000000..034727852a
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js
@@ -0,0 +1,37 @@
+// Bug 380852 - Delete permission manager entries in Clear Recent History
+
+function countPermissions() {
+ return Services.perms.all.length;
+}
+
+add_task(async function test() {
+ // sanitize before we start so we have a good baseline.
+ await Sanitizer.sanitize(["siteSettings"], { ignoreTimespan: false });
+
+ // Count how many permissions we start with - some are defaults that
+ // will not be sanitized.
+ let numAtStart = countPermissions();
+
+ // Add a permission entry
+ PermissionTestUtils.add(
+ "https://example.com",
+ "testing",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Sanity check
+ ok(
+ !!Services.perms.all.length,
+ "Permission manager should have elements, since we just added one"
+ );
+
+ // Clear it
+ await Sanitizer.sanitize(["siteSettings"], { ignoreTimespan: false });
+
+ // Make sure it's gone
+ is(
+ numAtStart,
+ countPermissions(),
+ "Permission manager should have the same count it started with"
+ );
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-timespans.js b/browser/base/content/test/sanitize/browser_sanitize-timespans.js
new file mode 100644
index 0000000000..30ccb90666
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-timespans.js
@@ -0,0 +1,1194 @@
+requestLongerTimeout(2);
+
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+// Bug 453440 - Test the timespan-based logic of the sanitizer code
+var now_mSec = Date.now();
+var now_uSec = now_mSec * 1000;
+
+const kMsecPerMin = 60 * 1000;
+const kUsecPerMin = 60 * 1000000;
+
+function promiseFormHistoryRemoved() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function onfh() {
+ Services.obs.removeObserver(onfh, "satchel-storage-changed");
+ resolve();
+ }, "satchel-storage-changed");
+ });
+}
+
+function promiseDownloadRemoved(list) {
+ return new Promise(resolve => {
+ let view = {
+ onDownloadRemoved(download) {
+ list.removeView(view);
+ resolve();
+ },
+ };
+
+ list.addView(view);
+ });
+}
+
+add_task(async function test() {
+ await setupDownloads();
+ await setupFormHistory();
+ await setupHistory();
+ await onHistoryReady();
+});
+
+async function countEntries(name, message, check) {
+ var obj = {};
+ if (name !== null) {
+ obj.fieldname = name;
+ }
+ let count = await FormHistory.count(obj);
+ check(count, message);
+}
+
+async function onHistoryReady() {
+ var hoursSinceMidnight = new Date().getHours();
+ var minutesSinceMidnight = hoursSinceMidnight * 60 + new Date().getMinutes();
+
+ // Should test cookies here, but nsICookieManager/nsICookieService
+ // doesn't let us fake creation times. bug 463127
+
+ var itemPrefs = Services.prefs.getBranch("privacy.cpd.");
+ itemPrefs.setBoolPref("history", true);
+ itemPrefs.setBoolPref("downloads", true);
+ itemPrefs.setBoolPref("cache", false);
+ itemPrefs.setBoolPref("cookies", false);
+ itemPrefs.setBoolPref("formdata", true);
+ itemPrefs.setBoolPref("offlineApps", false);
+ itemPrefs.setBoolPref("passwords", false);
+ itemPrefs.setBoolPref("sessions", false);
+ itemPrefs.setBoolPref("siteSettings", false);
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloadPromise = promiseDownloadRemoved(publicList);
+ let formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 10 minutes ago
+ let range = [now_uSec - 10 * 60 * 1000000, now_uSec];
+ await Sanitizer.sanitize(null, { range, ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://10minutes.com")),
+ "Pretend visit to 10minutes.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://1hour.com"),
+ "Pretend visit to 1hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://1hour10minutes.com"),
+ "Pretend visit to 1hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://2hour.com"),
+ "Pretend visit to 2hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (minutesSinceMidnight > 10) {
+ ok(
+ await PlacesUtils.history.hasVisits("https://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+
+ let checkZero = function (num, message) {
+ is(num, 0, message);
+ };
+ let checkOne = function (num, message) {
+ is(num, 1, message);
+ };
+
+ await countEntries(
+ "10minutes",
+ "10minutes form entry should be deleted",
+ checkZero
+ );
+ await countEntries("1hour", "1hour form entry should still exist", checkOne);
+ await countEntries(
+ "1hour10minutes",
+ "1hour10minutes form entry should still exist",
+ checkOne
+ );
+ await countEntries("2hour", "2hour form entry should still exist", checkOne);
+ await countEntries(
+ "2hour10minutes",
+ "2hour10minutes form entry should still exist",
+ checkOne
+ );
+ await countEntries("4hour", "4hour form entry should still exist", checkOne);
+ await countEntries(
+ "4hour10minutes",
+ "4hour10minutes form entry should still exist",
+ checkOne
+ );
+ if (minutesSinceMidnight > 10) {
+ await countEntries(
+ "today",
+ "today form entry should still exist",
+ checkOne
+ );
+ }
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-10-minutes")),
+ "10 minute download should now be deleted"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-1-hour"),
+ "<1 hour download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-1-hour-10-minutes"),
+ "1 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour"),
+ "<2 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour-10-minutes"),
+ "2 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour"),
+ "<4 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour-10-minutes"),
+ "4 hour 10 minute download should still be present"
+ );
+
+ if (minutesSinceMidnight > 10) {
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "'Today' download should still be present"
+ );
+ }
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 1 hour
+ Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 1);
+ await Sanitizer.sanitize(null, { ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://1hour.com")),
+ "Pretend visit to 1hour.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://1hour10minutes.com"),
+ "Pretend visit to 1hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://2hour.com"),
+ "Pretend visit to 2hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (hoursSinceMidnight > 1) {
+ ok(
+ await PlacesUtils.history.hasVisits("https://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+
+ await countEntries("1hour", "1hour form entry should be deleted", checkZero);
+ await countEntries(
+ "1hour10minutes",
+ "1hour10minutes form entry should still exist",
+ checkOne
+ );
+ await countEntries("2hour", "2hour form entry should still exist", checkOne);
+ await countEntries(
+ "2hour10minutes",
+ "2hour10minutes form entry should still exist",
+ checkOne
+ );
+ await countEntries("4hour", "4hour form entry should still exist", checkOne);
+ await countEntries(
+ "4hour10minutes",
+ "4hour10minutes form entry should still exist",
+ checkOne
+ );
+ if (hoursSinceMidnight > 1) {
+ await countEntries(
+ "today",
+ "today form entry should still exist",
+ checkOne
+ );
+ }
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-1-hour")),
+ "<1 hour download should now be deleted"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-1-hour-10-minutes"),
+ "1 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour"),
+ "<2 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour-10-minutes"),
+ "2 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour"),
+ "<4 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour-10-minutes"),
+ "4 hour 10 minute download should still be present"
+ );
+
+ if (hoursSinceMidnight > 1) {
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "'Today' download should still be present"
+ );
+ }
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 1 hour 10 minutes
+ range = [now_uSec - 70 * 60 * 1000000, now_uSec];
+ await Sanitizer.sanitize(null, { range, ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://1hour10minutes.com")),
+ "Pretend visit to 1hour10minutes.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://2hour.com"),
+ "Pretend visit to 2hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (minutesSinceMidnight > 70) {
+ ok(
+ await PlacesUtils.history.hasVisits("https://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+
+ await countEntries(
+ "1hour10minutes",
+ "1hour10minutes form entry should be deleted",
+ checkZero
+ );
+ await countEntries("2hour", "2hour form entry should still exist", checkOne);
+ await countEntries(
+ "2hour10minutes",
+ "2hour10minutes form entry should still exist",
+ checkOne
+ );
+ await countEntries("4hour", "4hour form entry should still exist", checkOne);
+ await countEntries(
+ "4hour10minutes",
+ "4hour10minutes form entry should still exist",
+ checkOne
+ );
+ if (minutesSinceMidnight > 70) {
+ await countEntries(
+ "today",
+ "today form entry should still exist",
+ checkOne
+ );
+ }
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-1-hour-10-minutes")),
+ "1 hour 10 minute old download should now be deleted"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour"),
+ "<2 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour-10-minutes"),
+ "2 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour"),
+ "<4 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour-10-minutes"),
+ "4 hour 10 minute download should still be present"
+ );
+ if (minutesSinceMidnight > 70) {
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "'Today' download should still be present"
+ );
+ }
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 2 hours
+ Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 2);
+ await Sanitizer.sanitize(null, { ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://2hour.com")),
+ "Pretend visit to 2hour.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (hoursSinceMidnight > 2) {
+ ok(
+ await PlacesUtils.history.hasVisits("https://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+
+ await countEntries("2hour", "2hour form entry should be deleted", checkZero);
+ await countEntries(
+ "2hour10minutes",
+ "2hour10minutes form entry should still exist",
+ checkOne
+ );
+ await countEntries("4hour", "4hour form entry should still exist", checkOne);
+ await countEntries(
+ "4hour10minutes",
+ "4hour10minutes form entry should still exist",
+ checkOne
+ );
+ if (hoursSinceMidnight > 2) {
+ await countEntries(
+ "today",
+ "today form entry should still exist",
+ checkOne
+ );
+ }
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-2-hour")),
+ "<2 hour old download should now be deleted"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour-10-minutes"),
+ "2 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour"),
+ "<4 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour-10-minutes"),
+ "4 hour 10 minute download should still be present"
+ );
+ if (hoursSinceMidnight > 2) {
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "'Today' download should still be present"
+ );
+ }
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 2 hours 10 minutes
+ range = [now_uSec - 130 * 60 * 1000000, now_uSec];
+ await Sanitizer.sanitize(null, { range, ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://2hour10minutes.com")),
+ "Pretend visit to 2hour10minutes.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (minutesSinceMidnight > 130) {
+ ok(
+ await PlacesUtils.history.hasVisits("https://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+
+ await countEntries(
+ "2hour10minutes",
+ "2hour10minutes form entry should be deleted",
+ checkZero
+ );
+ await countEntries("4hour", "4hour form entry should still exist", checkOne);
+ await countEntries(
+ "4hour10minutes",
+ "4hour10minutes form entry should still exist",
+ checkOne
+ );
+ if (minutesSinceMidnight > 130) {
+ await countEntries(
+ "today",
+ "today form entry should still exist",
+ checkOne
+ );
+ }
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-2-hour-10-minutes")),
+ "2 hour 10 minute old download should now be deleted"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour"),
+ "<4 hour old download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour-10-minutes"),
+ "4 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+ if (minutesSinceMidnight > 130) {
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "'Today' download should still be present"
+ );
+ }
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 4 hours
+ Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 3);
+ await Sanitizer.sanitize(null, { ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://4hour.com")),
+ "Pretend visit to 4hour.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("https://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (hoursSinceMidnight > 4) {
+ ok(
+ await PlacesUtils.history.hasVisits("https://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+
+ await countEntries("4hour", "4hour form entry should be deleted", checkZero);
+ await countEntries(
+ "4hour10minutes",
+ "4hour10minutes form entry should still exist",
+ checkOne
+ );
+ if (hoursSinceMidnight > 4) {
+ await countEntries(
+ "today",
+ "today form entry should still exist",
+ checkOne
+ );
+ }
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-4-hour")),
+ "<4 hour old download should now be deleted"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour-10-minutes"),
+ "4 hour 10 minute download should still be present"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+ if (hoursSinceMidnight > 4) {
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "'Today' download should still be present"
+ );
+ }
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 4 hours 10 minutes
+ range = [now_uSec - 250 * 60 * 1000000, now_uSec];
+ await Sanitizer.sanitize(null, { range, ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://4hour10minutes.com")),
+ "Pretend visit to 4hour10minutes.com should now be deleted"
+ );
+ if (minutesSinceMidnight > 250) {
+ ok(
+ await PlacesUtils.history.hasVisits("https://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+
+ await countEntries(
+ "4hour10minutes",
+ "4hour10minutes form entry should be deleted",
+ checkZero
+ );
+ if (minutesSinceMidnight > 250) {
+ await countEntries(
+ "today",
+ "today form entry should still exist",
+ checkOne
+ );
+ }
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-4-hour-10-minutes")),
+ "4 hour 10 minute download should now be deleted"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+ if (minutesSinceMidnight > 250) {
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "'Today' download should still be present"
+ );
+ }
+
+ // The 'Today' download might have been already deleted, in which case we
+ // should not wait for a download removal notification.
+ if (minutesSinceMidnight > 250) {
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+ } else {
+ downloadPromise = formHistoryPromise = Promise.resolve();
+ }
+
+ // Clear Today
+ Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 4);
+ let progress = await Sanitizer.sanitize(null, { ignoreTimespan: false });
+ Assert.deepEqual(progress, {
+ history: "cleared",
+ formdata: "cleared",
+ downloads: "cleared",
+ });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ // Be careful. If we add our objectss just before midnight, and sanitize
+ // runs immediately after, they won't be expired. This is expected, but
+ // we should not test in that case. We cannot just test for opposite
+ // condition because we could cross midnight just one moment after we
+ // cache our time, then we would have an even worse random failure.
+ var today = isToday(new Date(now_mSec));
+ if (today) {
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://today.com")),
+ "Pretend visit to today.com should now be deleted"
+ );
+
+ await countEntries(
+ "today",
+ "today form entry should be deleted",
+ checkZero
+ );
+ ok(
+ !(await downloadExists(publicList, "fakefile-today")),
+ "'Today' download should now be deleted"
+ );
+ }
+
+ ok(
+ await PlacesUtils.history.hasVisits("https://before-today.com"),
+ "Pretend visit to before-today.com should still exist"
+ );
+ await countEntries(
+ "b4today",
+ "b4today form entry should still exist",
+ checkOne
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Year old download should still be present"
+ );
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Choose everything
+ Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 0);
+ await Sanitizer.sanitize(null, { ignoreTimespan: false });
+
+ await formHistoryPromise;
+ await downloadPromise;
+
+ ok(
+ !(await PlacesUtils.history.hasVisits("https://before-today.com")),
+ "Pretend visit to before-today.com should now be deleted"
+ );
+
+ await countEntries(
+ "b4today",
+ "b4today form entry should be deleted",
+ checkZero
+ );
+
+ ok(
+ !(await downloadExists(publicList, "fakefile-old")),
+ "Year old download should now be deleted"
+ );
+}
+
+async function setupHistory() {
+ let places = [];
+
+ function addPlace(aURI, aTitle, aVisitDate) {
+ places.push({
+ uri: aURI,
+ title: aTitle,
+ visitDate: aVisitDate,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK,
+ });
+ }
+
+ addPlace(
+ "https://10minutes.com/",
+ "10 minutes ago",
+ now_uSec - 10 * kUsecPerMin
+ );
+ addPlace(
+ "https://1hour.com/",
+ "Less than 1 hour ago",
+ now_uSec - 45 * kUsecPerMin
+ );
+ addPlace(
+ "https://1hour10minutes.com/",
+ "1 hour 10 minutes ago",
+ now_uSec - 70 * kUsecPerMin
+ );
+ addPlace(
+ "https://2hour.com/",
+ "Less than 2 hours ago",
+ now_uSec - 90 * kUsecPerMin
+ );
+ addPlace(
+ "https://2hour10minutes.com/",
+ "2 hours 10 minutes ago",
+ now_uSec - 130 * kUsecPerMin
+ );
+ addPlace(
+ "https://4hour.com/",
+ "Less than 4 hours ago",
+ now_uSec - 180 * kUsecPerMin
+ );
+ addPlace(
+ "https://4hour10minutes.com/",
+ "4 hours 10 minutesago",
+ now_uSec - 250 * kUsecPerMin
+ );
+
+ let today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ today.setMilliseconds(1);
+ addPlace("https://today.com/", "Today", today.getTime() * 1000);
+
+ let lastYear = new Date();
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
+ addPlace(
+ "https://before-today.com/",
+ "Before Today",
+ lastYear.getTime() * 1000
+ );
+ await PlacesTestUtils.addVisits(places);
+}
+
+async function setupFormHistory() {
+ function searchEntries(terms, params) {
+ return FormHistory.search(terms, params);
+ }
+
+ // Make sure we've got a clean DB to start with, then add the entries we'll be testing.
+ await FormHistory.update([
+ {
+ op: "remove",
+ },
+ {
+ op: "add",
+ fieldname: "10minutes",
+ value: "10m",
+ },
+ {
+ op: "add",
+ fieldname: "1hour",
+ value: "1h",
+ },
+ {
+ op: "add",
+ fieldname: "1hour10minutes",
+ value: "1h10m",
+ },
+ {
+ op: "add",
+ fieldname: "2hour",
+ value: "2h",
+ },
+ {
+ op: "add",
+ fieldname: "2hour10minutes",
+ value: "2h10m",
+ },
+ {
+ op: "add",
+ fieldname: "4hour",
+ value: "4h",
+ },
+ {
+ op: "add",
+ fieldname: "4hour10minutes",
+ value: "4h10m",
+ },
+ {
+ op: "add",
+ fieldname: "today",
+ value: "1d",
+ },
+ {
+ op: "add",
+ fieldname: "b4today",
+ value: "1y",
+ },
+ ]);
+
+ // Artifically age the entries to the proper vintage.
+ let timestamp = now_uSec - 10 * kUsecPerMin;
+ let results = await searchEntries(["guid"], { fieldname: "10minutes" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ timestamp = now_uSec - 45 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "1hour" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ timestamp = now_uSec - 70 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "1hour10minutes" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ timestamp = now_uSec - 90 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "2hour" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ timestamp = now_uSec - 130 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "2hour10minutes" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ timestamp = now_uSec - 180 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "4hour" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ timestamp = now_uSec - 250 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "4hour10minutes" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ let today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ today.setMilliseconds(1);
+ timestamp = today.getTime() * 1000;
+ results = await searchEntries(["guid"], { fieldname: "today" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ let lastYear = new Date();
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
+ timestamp = lastYear.getTime() * 1000;
+ results = await searchEntries(["guid"], { fieldname: "b4today" });
+ await FormHistory.update({
+ op: "update",
+ firstUsed: timestamp,
+ guid: results[0].guid,
+ });
+
+ var checks = 0;
+ let checkOne = function (num, message) {
+ is(num, 1, message);
+ checks++;
+ };
+
+ // Sanity check.
+ await countEntries(
+ "10minutes",
+ "Checking for 10minutes form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "1hour",
+ "Checking for 1hour form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "1hour10minutes",
+ "Checking for 1hour10minutes form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "2hour",
+ "Checking for 2hour form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "2hour10minutes",
+ "Checking for 2hour10minutes form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "4hour",
+ "Checking for 4hour form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "4hour10minutes",
+ "Checking for 4hour10minutes form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "today",
+ "Checking for today form history entry creation",
+ checkOne
+ );
+ await countEntries(
+ "b4today",
+ "Checking for b4today form history entry creation",
+ checkOne
+ );
+ is(checks, 9, "9 checks made");
+}
+
+async function setupDownloads() {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+
+ let download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-10-minutes",
+ });
+ download.startTime = new Date(now_mSec - 10 * kMsecPerMin); // 10 minutes ago
+ download.canceled = true;
+ await publicList.add(download);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-1-hour",
+ });
+ download.startTime = new Date(now_mSec - 45 * kMsecPerMin); // 45 minutes ago
+ download.canceled = true;
+ await publicList.add(download);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-1-hour-10-minutes",
+ });
+ download.startTime = new Date(now_mSec - 70 * kMsecPerMin); // 70 minutes ago
+ download.canceled = true;
+ await publicList.add(download);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-2-hour",
+ });
+ download.startTime = new Date(now_mSec - 90 * kMsecPerMin); // 90 minutes ago
+ download.canceled = true;
+ await publicList.add(download);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-2-hour-10-minutes",
+ });
+ download.startTime = new Date(now_mSec - 130 * kMsecPerMin); // 130 minutes ago
+ download.canceled = true;
+ await publicList.add(download);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-4-hour",
+ });
+ download.startTime = new Date(now_mSec - 180 * kMsecPerMin); // 180 minutes ago
+ download.canceled = true;
+ await publicList.add(download);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-4-hour-10-minutes",
+ });
+ download.startTime = new Date(now_mSec - 250 * kMsecPerMin); // 250 minutes ago
+ download.canceled = true;
+ await publicList.add(download);
+
+ // Add "today" download
+ let today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ today.setMilliseconds(1);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-today",
+ });
+ download.startTime = today; // 12:00:01 AM this morning
+ download.canceled = true;
+ await publicList.add(download);
+
+ // Add "before today" download
+ let lastYear = new Date();
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
+
+ download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-old",
+ });
+ download.startTime = lastYear;
+ download.canceled = true;
+ await publicList.add(download);
+
+ // Confirm everything worked
+ let downloads = await publicList.getAll();
+ is(downloads.length, 9, "9 Pretend downloads added");
+
+ ok(
+ await downloadExists(publicList, "fakefile-old"),
+ "Pretend download for everything case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-10-minutes"),
+ "Pretend download for 10-minutes case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-1-hour"),
+ "Pretend download for 1-hour case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-1-hour-10-minutes"),
+ "Pretend download for 1-hour-10-minutes case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour"),
+ "Pretend download for 2-hour case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-2-hour-10-minutes"),
+ "Pretend download for 2-hour-10-minutes case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour"),
+ "Pretend download for 4-hour case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-4-hour-10-minutes"),
+ "Pretend download for 4-hour-10-minutes case should exist"
+ );
+ ok(
+ await downloadExists(publicList, "fakefile-today"),
+ "Pretend download for Today case should exist"
+ );
+}
+
+/**
+ * Checks to see if the downloads with the specified id exists.
+ *
+ * @param aID
+ * The ids of the downloads to check.
+ */
+let downloadExists = async function (list, path) {
+ let listArray = await list.getAll();
+ return listArray.some(i => i.target.path == path);
+};
+
+function isToday(aDate) {
+ return aDate.getDate() == new Date().getDate();
+}
diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog.js b/browser/base/content/test/sanitize/browser_sanitizeDialog.js
new file mode 100644
index 0000000000..aece90f16e
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitizeDialog.js
@@ -0,0 +1,833 @@
+/* -*- 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 the sanitize dialog (a.k.a. the clear recent history dialog).
+ * See bug 480169.
+ *
+ * The purpose of this test is not to fully flex the sanitize timespan code;
+ * browser/base/content/test/sanitize/browser_sanitize-timespans.js does that. This
+ * test checks the UI of the dialog and makes sure it's correctly connected to
+ * the sanitize timespan code.
+ *
+ * Some of this code, especially the history creation parts, was taken from
+ * browser/base/content/test/sanitize/browser_sanitize-timespans.js.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ Timer: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const kMsecPerMin = 60 * 1000;
+const kUsecPerMin = 60 * 1000000;
+
+/**
+ * Ensures that the specified URIs are either cleared or not.
+ *
+ * @param aURIs
+ * Array of page URIs
+ * @param aShouldBeCleared
+ * True if each visit to the URI should be cleared, false otherwise
+ */
+async function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
+ for (let uri of aURIs) {
+ let visited = await PlacesUtils.history.hasVisits(uri);
+ Assert.equal(
+ visited,
+ !aShouldBeCleared,
+ `history visit ${uri.spec} should ${
+ aShouldBeCleared ? "no longer" : "still"
+ } exist`
+ );
+ }
+}
+
+add_setup(async function () {
+ requestLongerTimeout(3);
+ await blankSlate();
+ registerCleanupFunction(async function () {
+ await blankSlate();
+ await PlacesTestUtils.promiseAsyncUpdates();
+ });
+});
+
+/**
+ * Initializes the dialog to its default state.
+ */
+add_task(async function default_state() {
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ // Select "Last Hour"
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.acceptDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Cancels the dialog, makes sure history not cleared.
+ */
+add_task(async function test_cancel() {
+ // Add history (within the past hour)
+ let uris = [];
+ let places = [];
+ let pURI;
+ for (let i = 0; i < 30; i++) {
+ pURI = makeURI("https://" + i + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) });
+ uris.push(pURI);
+ }
+ await PlacesTestUtils.addVisits(places);
+
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.checkPrefCheckbox("history", false);
+ this.cancelDialog();
+ };
+ dh.onunload = async function () {
+ await promiseHistoryClearedState(uris, false);
+ await blankSlate();
+ await promiseHistoryClearedState(uris, true);
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Ensures that the combined history-downloads checkbox clears both history
+ * visits and downloads when checked; the dialog respects simple timespan.
+ */
+add_task(async function test_history_downloads_checked() {
+ // Add downloads (within the past hour).
+ let downloadIDs = [];
+ for (let i = 0; i < 5; i++) {
+ await addDownloadWithMinutesAgo(downloadIDs, i);
+ }
+ // Add downloads (over an hour ago).
+ let olderDownloadIDs = [];
+ for (let i = 0; i < 5; i++) {
+ await addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i);
+ }
+
+ // Add history (within the past hour).
+ let uris = [];
+ let places = [];
+ let pURI;
+ for (let i = 0; i < 30; i++) {
+ pURI = makeURI("https://" + i + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) });
+ uris.push(pURI);
+ }
+ // Add history (over an hour ago).
+ let olderURIs = [];
+ for (let i = 0; i < 5; i++) {
+ pURI = makeURI("https://" + (61 + i) + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i) });
+ olderURIs.push(pURI);
+ }
+ let promiseSanitized = promiseSanitizationComplete();
+
+ await PlacesTestUtils.addVisits(places);
+
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.checkPrefCheckbox("history", true);
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ intPrefIs(
+ "sanitize.timeSpan",
+ Sanitizer.TIMESPAN_HOUR,
+ "timeSpan pref should be hour after accepting dialog with " +
+ "hour selected"
+ );
+ boolPrefIs(
+ "cpd.history",
+ true,
+ "history pref should be true after accepting dialog with " +
+ "history checkbox checked"
+ );
+ boolPrefIs(
+ "cpd.downloads",
+ true,
+ "downloads pref should be true after accepting dialog with " +
+ "history checkbox checked"
+ );
+
+ await promiseSanitized;
+
+ // History visits and downloads within one hour should be cleared.
+ await promiseHistoryClearedState(uris, true);
+ await ensureDownloadsClearedState(downloadIDs, true);
+
+ // Visits and downloads > 1 hour should still exist.
+ await promiseHistoryClearedState(olderURIs, false);
+ await ensureDownloadsClearedState(olderDownloadIDs, false);
+
+ // OK, done, cleanup after ourselves.
+ await blankSlate();
+ await promiseHistoryClearedState(olderURIs, true);
+ await ensureDownloadsClearedState(olderDownloadIDs, true);
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Ensures that the combined history-downloads checkbox removes neither
+ * history visits nor downloads when not checked.
+ */
+add_task(async function test_history_downloads_unchecked() {
+ // Add form entries
+ let formEntries = [];
+
+ for (let i = 0; i < 5; i++) {
+ formEntries.push(await promiseAddFormEntryWithMinutesAgo(i));
+ }
+
+ // Add downloads (within the past hour).
+ let downloadIDs = [];
+ for (let i = 0; i < 5; i++) {
+ await addDownloadWithMinutesAgo(downloadIDs, i);
+ }
+
+ // Add history, downloads, form entries (within the past hour).
+ let uris = [];
+ let places = [];
+ let pURI;
+ for (let i = 0; i < 5; i++) {
+ pURI = makeURI("https://" + i + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) });
+ uris.push(pURI);
+ }
+
+ await PlacesTestUtils.addVisits(places);
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ is(
+ this.isWarningPanelVisible(),
+ false,
+ "Warning panel should be hidden after previously accepting dialog " +
+ "with a predefined timespan"
+ );
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+
+ // Remove only form entries, leave history (including downloads).
+ this.checkPrefCheckbox("history", false);
+ this.checkPrefCheckbox("formdata", true);
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ intPrefIs(
+ "sanitize.timeSpan",
+ Sanitizer.TIMESPAN_HOUR,
+ "timeSpan pref should be hour after accepting dialog with " +
+ "hour selected"
+ );
+ boolPrefIs(
+ "cpd.history",
+ false,
+ "history pref should be false after accepting dialog with " +
+ "history checkbox unchecked"
+ );
+ boolPrefIs(
+ "cpd.downloads",
+ false,
+ "downloads pref should be false after accepting dialog with " +
+ "history checkbox unchecked"
+ );
+
+ // Of the three only form entries should be cleared.
+ await promiseHistoryClearedState(uris, false);
+ await ensureDownloadsClearedState(downloadIDs, false);
+
+ for (let entry of formEntries) {
+ let exists = await formNameExists(entry);
+ ok(!exists, "form entry " + entry + " should no longer exist");
+ }
+
+ // OK, done, cleanup after ourselves.
+ await blankSlate();
+ await promiseHistoryClearedState(uris, true);
+ await ensureDownloadsClearedState(downloadIDs, true);
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Ensures that the "Everything" duration option works.
+ */
+add_task(async function test_everything() {
+ // Add history.
+ let uris = [];
+ let places = [];
+ let pURI;
+ // within past hour, within past two hours, within past four hours and
+ // outside past four hours
+ [10, 70, 130, 250].forEach(function (aValue) {
+ pURI = makeURI("https://" + aValue + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) });
+ uris.push(pURI);
+ });
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ await PlacesTestUtils.addVisits(places);
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ is(
+ this.isWarningPanelVisible(),
+ false,
+ "Warning panel should be hidden after previously accepting dialog " +
+ "with a predefined timespan"
+ );
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ this.checkPrefCheckbox("history", true);
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ await promiseSanitized;
+ intPrefIs(
+ "sanitize.timeSpan",
+ Sanitizer.TIMESPAN_EVERYTHING,
+ "timeSpan pref should be everything after accepting dialog " +
+ "with everything selected"
+ );
+
+ await promiseHistoryClearedState(uris, true);
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Ensures that the "Everything" warning is visible on dialog open after
+ * the previous test.
+ */
+add_task(async function test_everything_warning() {
+ // Add history.
+ let uris = [];
+ let places = [];
+ let pURI;
+ // within past hour, within past two hours, within past four hours and
+ // outside past four hours
+ [10, 70, 130, 250].forEach(function (aValue) {
+ pURI = makeURI("https://" + aValue + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) });
+ uris.push(pURI);
+ });
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ await PlacesTestUtils.addVisits(places);
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ is(
+ this.isWarningPanelVisible(),
+ true,
+ "Warning panel should be visible after previously accepting dialog " +
+ "with clearing everything"
+ );
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ this.checkPrefCheckbox("history", true);
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ intPrefIs(
+ "sanitize.timeSpan",
+ Sanitizer.TIMESPAN_EVERYTHING,
+ "timeSpan pref should be everything after accepting dialog " +
+ "with everything selected"
+ );
+
+ await promiseSanitized;
+
+ await promiseHistoryClearedState(uris, true);
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * The next three tests checks that when a certain history item cannot be
+ * cleared then the checkbox should be both disabled and unchecked.
+ * In addition, we ensure that this behavior does not modify the preferences.
+ */
+add_task(async function test_cannot_clear_history() {
+ // Add form entries
+ let formEntries = [await promiseAddFormEntryWithMinutesAgo(10)];
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ // Add history.
+ let pURI = makeURI("https://" + 10 + "-minutes-ago.com/");
+ await PlacesTestUtils.addVisits({
+ uri: pURI,
+ visitDate: visitTimeForMinutesAgo(10),
+ });
+ let uris = [pURI];
+
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ // Check that the relevant checkboxes are enabled
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[preference='privacy.cpd.formdata']"
+ );
+ ok(
+ cb.length == 1 && !cb[0].disabled,
+ "There is formdata, checkbox to clear formdata should be enabled."
+ );
+
+ cb = this.win.document.querySelectorAll(
+ "checkbox[preference='privacy.cpd.history']"
+ );
+ ok(
+ cb.length == 1 && !cb[0].disabled,
+ "There is history, checkbox to clear history should be enabled."
+ );
+
+ this.checkAllCheckboxes();
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ await promiseSanitized;
+
+ await promiseHistoryClearedState(uris, true);
+
+ let exists = await formNameExists(formEntries[0]);
+ ok(!exists, "form entry " + formEntries[0] + " should no longer exist");
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+add_task(async function test_no_formdata_history_to_clear() {
+ let promiseSanitized = promiseSanitizationComplete();
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ boolPrefIs(
+ "cpd.history",
+ true,
+ "history pref should be true after accepting dialog with " +
+ "history checkbox checked"
+ );
+ boolPrefIs(
+ "cpd.formdata",
+ true,
+ "formdata pref should be true after accepting dialog with " +
+ "formdata checkbox checked"
+ );
+
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[preference='privacy.cpd.history']"
+ );
+ ok(
+ cb.length == 1 && !cb[0].disabled && cb[0].checked,
+ "There is no history, but history checkbox should always be enabled " +
+ "and will be checked from previous preference."
+ );
+
+ this.acceptDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+ await promiseSanitized;
+});
+
+add_task(async function test_form_entries() {
+ let formEntry = await promiseAddFormEntryWithMinutesAgo(10);
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ boolPrefIs(
+ "cpd.formdata",
+ true,
+ "formdata pref should persist previous value after accepting " +
+ "dialog where you could not clear formdata."
+ );
+
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[preference='privacy.cpd.formdata']"
+ );
+
+ info(
+ "There exists formEntries so the checkbox should be in sync with the pref."
+ );
+ is(cb.length, 1, "There is only one checkbox for form data");
+ ok(!cb[0].disabled, "The checkbox is enabled");
+ ok(cb[0].checked, "The checkbox is checked");
+
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ await promiseSanitized;
+ let exists = await formNameExists(formEntry);
+ ok(!exists, "form entry " + formEntry + " should no longer exist");
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+// Test for offline apps permission deletion
+add_task(async function test_offline_apps_permissions() {
+ // Prepare stuff, we will work with www.example.com
+ var URL = "https://www.example.com";
+ var URI = makeURI(URL);
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ URI,
+ {}
+ );
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ // Open the dialog
+ let dh = new DialogHelper();
+ dh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ // Clear only offlineApps
+ this.uncheckAllCheckboxes();
+ this.checkPrefCheckbox("siteSettings", true);
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ await promiseSanitized;
+
+ // Check all has been deleted (privileges, data, cache)
+ is(
+ Services.perms.testPermissionFromPrincipal(principal, "offline-app"),
+ 0,
+ "offline-app permissions removed"
+ );
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+var now_mSec = Date.now();
+var now_uSec = now_mSec * 1000;
+
+/**
+ * This wraps the dialog and provides some convenience methods for interacting
+ * with it.
+ *
+ * @param browserWin (optional)
+ * The browser window that the dialog is expected to open in. If not
+ * supplied, the initial browser window of the test run is used.
+ */
+function DialogHelper(browserWin = window) {
+ this._browserWin = browserWin;
+ this.win = null;
+ this.promiseClosed = new Promise(resolve => {
+ this._resolveClosed = resolve;
+ });
+}
+
+DialogHelper.prototype = {
+ /**
+ * "Presses" the dialog's OK button.
+ */
+ acceptDialog() {
+ let dialogEl = this.win.document.querySelector("dialog");
+ is(
+ dialogEl.getButton("accept").disabled,
+ false,
+ "Dialog's OK button should not be disabled"
+ );
+ dialogEl.acceptDialog();
+ },
+
+ /**
+ * "Presses" the dialog's Cancel button.
+ */
+ cancelDialog() {
+ this.win.document.querySelector("dialog").cancelDialog();
+ },
+
+ /**
+ * (Un)checks a history scope checkbox (browser & download history,
+ * form history, etc.).
+ *
+ * @param aPrefName
+ * The final portion of the checkbox's privacy.cpd.* preference name
+ * @param aCheckState
+ * True if the checkbox should be checked, false otherwise
+ */
+ checkPrefCheckbox(aPrefName, aCheckState) {
+ var pref = "privacy.cpd." + aPrefName;
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[preference='" + pref + "']"
+ );
+ is(cb.length, 1, "found checkbox for " + pref + " preference");
+ if (cb[0].checked != aCheckState) {
+ cb[0].click();
+ }
+ },
+
+ /**
+ * Makes sure all the checkboxes are checked.
+ */
+ _checkAllCheckboxesCustom(check) {
+ var cb = this.win.document.querySelectorAll("checkbox[preference]");
+ ok(cb.length > 1, "found checkboxes for preferences");
+ for (var i = 0; i < cb.length; ++i) {
+ var pref = this.win.Preferences.get(cb[i].getAttribute("preference"));
+ if (!!pref.value ^ check) {
+ cb[i].click();
+ }
+ }
+ },
+
+ checkAllCheckboxes() {
+ this._checkAllCheckboxesCustom(true);
+ },
+
+ uncheckAllCheckboxes() {
+ this._checkAllCheckboxesCustom(false);
+ },
+
+ /**
+ * @return The dialog's duration dropdown
+ */
+ getDurationDropdown() {
+ return this.win.document.getElementById("sanitizeDurationChoice");
+ },
+
+ /**
+ * @return The clear-everything warning box
+ */
+ getWarningPanel() {
+ return this.win.document.getElementById("sanitizeEverythingWarningBox");
+ },
+
+ /**
+ * @return True if the "Everything" warning panel is visible (as opposed to
+ * the tree)
+ */
+ isWarningPanelVisible() {
+ return !this.getWarningPanel().hidden;
+ },
+
+ /**
+ * Opens the clear recent history dialog. Before calling this, set
+ * this.onload to a function to execute onload. It should close the dialog
+ * when done so that the tests may continue. Set this.onunload to a function
+ * to execute onunload. this.onunload is optional. If it returns true, the
+ * caller is expected to call promiseAsyncUpdates at some point; if false is
+ * returned, promiseAsyncUpdates is called automatically.
+ */
+ async open() {
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ "chrome://browser/content/sanitize.xhtml",
+ {
+ isSubDialog: true,
+ }
+ );
+
+ executeSoon(() => {
+ Sanitizer.showUI(this._browserWin);
+ });
+
+ this.win = await dialogPromise;
+ this.win.addEventListener(
+ "load",
+ () => {
+ // Run onload on next tick so that gSanitizePromptDialog.init can run first.
+ executeSoon(() => this.onload());
+ },
+ { once: true }
+ );
+
+ this.win.addEventListener(
+ "unload",
+ () => {
+ // Some exceptions that reach here don't reach the test harness, but
+ // ok()/is() do...
+ (async () => {
+ if (this.onunload) {
+ await this.onunload();
+ }
+ await PlacesTestUtils.promiseAsyncUpdates();
+ this._resolveClosed();
+ this.win = null;
+ })();
+ },
+ { once: true }
+ );
+ },
+
+ /**
+ * Selects a duration in the duration dropdown.
+ *
+ * @param aDurVal
+ * One of the Sanitizer.TIMESPAN_* values
+ */
+ selectDuration(aDurVal) {
+ this.getDurationDropdown().value = aDurVal;
+ if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) {
+ is(
+ this.isWarningPanelVisible(),
+ true,
+ "Warning panel should be visible for TIMESPAN_EVERYTHING"
+ );
+ } else {
+ is(
+ this.isWarningPanelVisible(),
+ false,
+ "Warning panel should not be visible for non-TIMESPAN_EVERYTHING"
+ );
+ }
+ },
+};
+
+function promiseSanitizationComplete() {
+ return TestUtils.topicObserved("sanitizer-sanitization-complete");
+}
+
+/**
+ * Adds a download to history.
+ *
+ * @param aMinutesAgo
+ * The download will be downloaded this many minutes ago
+ */
+async function addDownloadWithMinutesAgo(aExpectedPathList, aMinutesAgo) {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+
+ let name = "fakefile-" + aMinutesAgo + "-minutes-ago";
+ let download = await Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: name,
+ });
+ download.startTime = new Date(now_mSec - aMinutesAgo * kMsecPerMin);
+ download.canceled = true;
+ publicList.add(download);
+
+ ok(
+ await downloadExists(name),
+ "Sanity check: download " + name + " should exist after creating it"
+ );
+
+ aExpectedPathList.push(name);
+}
+
+/**
+ * Adds a form entry to history.
+ *
+ * @param aMinutesAgo
+ * The entry will be added this many minutes ago
+ */
+function promiseAddFormEntryWithMinutesAgo(aMinutesAgo) {
+ let name = aMinutesAgo + "-minutes-ago";
+
+ // Artifically age the entry to the proper vintage.
+ let timestamp = now_uSec - aMinutesAgo * kUsecPerMin;
+
+ return FormHistory.update({
+ op: "add",
+ fieldname: name,
+ value: "dummy",
+ firstUsed: timestamp,
+ });
+}
+
+/**
+ * Checks if a form entry exists.
+ */
+async function formNameExists(name) {
+ return !!(await FormHistory.count({ fieldname: name }));
+}
+
+/**
+ * Removes all history visits, downloads, and form entries.
+ */
+async function blankSlate() {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ await publicList.remove(download);
+ await download.finalize(true);
+ }
+
+ await FormHistory.update({ op: "remove" });
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Ensures that the given pref is the expected value.
+ *
+ * @param aPrefName
+ * The pref's sub-branch under the privacy branch
+ * @param aExpectedVal
+ * The pref's expected value
+ * @param aMsg
+ * Passed to is()
+ */
+function boolPrefIs(aPrefName, aExpectedVal, aMsg) {
+ is(Services.prefs.getBoolPref("privacy." + aPrefName), aExpectedVal, aMsg);
+}
+
+/**
+ * Checks to see if the download with the specified path exists.
+ *
+ * @param aPath
+ * The path of the download to check
+ * @return True if the download exists, false otherwise
+ */
+async function downloadExists(aPath) {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let listArray = await publicList.getAll();
+ return listArray.some(i => i.target.path == aPath);
+}
+
+/**
+ * Ensures that the specified downloads are either cleared or not.
+ *
+ * @param aDownloadIDs
+ * Array of download database IDs
+ * @param aShouldBeCleared
+ * True if each download should be cleared, false otherwise
+ */
+async function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) {
+ let niceStr = aShouldBeCleared ? "no longer" : "still";
+ for (let id of aDownloadIDs) {
+ is(
+ await downloadExists(id),
+ !aShouldBeCleared,
+ "download " + id + " should " + niceStr + " exist"
+ );
+ }
+}
+
+/**
+ * Ensures that the given pref is the expected value.
+ *
+ * @param aPrefName
+ * The pref's sub-branch under the privacy branch
+ * @param aExpectedVal
+ * The pref's expected value
+ * @param aMsg
+ * Passed to is()
+ */
+function intPrefIs(aPrefName, aExpectedVal, aMsg) {
+ is(Services.prefs.getIntPref("privacy." + aPrefName), aExpectedVal, aMsg);
+}
+
+/**
+ * Creates a visit time.
+ *
+ * @param aMinutesAgo
+ * The visit will be visited this many minutes ago
+ */
+function visitTimeForMinutesAgo(aMinutesAgo) {
+ return now_uSec - aMinutesAgo * kUsecPerMin;
+}
diff --git a/browser/base/content/test/sanitize/dummy.js b/browser/base/content/test/sanitize/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/base/content/test/sanitize/dummy.js
diff --git a/browser/base/content/test/sanitize/dummy_page.html b/browser/base/content/test/sanitize/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/base/content/test/sanitize/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/base/content/test/sanitize/head.js b/browser/base/content/test/sanitize/head.js
new file mode 100644
index 0000000000..161ccdc9fc
--- /dev/null
+++ b/browser/base/content/test/sanitize/head.js
@@ -0,0 +1,329 @@
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
+ PermissionTestUtils: "resource://testing-common/PermissionTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
+ SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.sys.mjs",
+});
+
+function createIndexedDB(host, originAttributes) {
+ let uri = Services.io.newURI("https://" + host);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ originAttributes
+ );
+ return SiteDataTestUtils.addToIndexedDB(principal.origin);
+}
+
+function checkIndexedDB(host, originAttributes) {
+ return new Promise(resolve => {
+ let data = true;
+ let uri = Services.io.newURI("https://" + host);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ originAttributes
+ );
+ let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
+ request.onupgradeneeded = function (e) {
+ data = false;
+ };
+ request.onsuccess = function (e) {
+ resolve(data);
+ };
+ });
+}
+
+function createHostCookie(host, originAttributes) {
+ Services.cookies.add(
+ host,
+ "/test",
+ "foo",
+ "bar",
+ false,
+ false,
+ false,
+ Date.now() + 24000 * 60 * 60,
+ originAttributes,
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+}
+
+function createDomainCookie(host, originAttributes) {
+ Services.cookies.add(
+ "." + host,
+ "/test",
+ "foo",
+ "bar",
+ false,
+ false,
+ false,
+ Date.now() + 24000 * 60 * 60,
+ originAttributes,
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+}
+
+function checkCookie(host, originAttributes) {
+ for (let cookie of Services.cookies.cookies) {
+ if (
+ ChromeUtils.isOriginAttributesEqual(
+ originAttributes,
+ cookie.originAttributes
+ ) &&
+ cookie.host.includes(host)
+ ) {
+ return true;
+ }
+ }
+ return false;
+}
+
+async function deleteOnShutdown(opt) {
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.sanitize.sanitizeOnShutdown", opt.sanitize],
+ ["privacy.clearOnShutdown.cookies", opt.sanitize],
+ ["privacy.clearOnShutdown.offlineApps", opt.sanitize],
+ ["browser.sanitizer.loglevel", "All"],
+ ],
+ });
+
+ // Custom permission without considering OriginAttributes
+ if (opt.cookiePermission !== undefined) {
+ let uri = Services.io.newURI("https://www.example.com");
+ PermissionTestUtils.add(uri, "cookie", opt.cookiePermission);
+ }
+
+ // Let's create a tab with some data.
+ await opt.createData(
+ (opt.fullHost ? "www." : "") + "example.org",
+ opt.originAttributes
+ );
+ ok(
+ await opt.checkData(
+ (opt.fullHost ? "www." : "") + "example.org",
+ opt.originAttributes
+ ),
+ "We have data for www.example.org"
+ );
+ await opt.createData(
+ (opt.fullHost ? "www." : "") + "example.com",
+ opt.originAttributes
+ );
+ ok(
+ await opt.checkData(
+ (opt.fullHost ? "www." : "") + "example.com",
+ opt.originAttributes
+ ),
+ "We have data for www.example.com"
+ );
+
+ // Cleaning up.
+ await Sanitizer.runSanitizeOnShutdown();
+
+ // All gone!
+ is(
+ !!(await opt.checkData(
+ (opt.fullHost ? "www." : "") + "example.org",
+ opt.originAttributes
+ )),
+ opt.expectedForOrg,
+ "Do we have data for www.example.org?"
+ );
+ is(
+ !!(await opt.checkData(
+ (opt.fullHost ? "www." : "") + "example.com",
+ opt.originAttributes
+ )),
+ opt.expectedForCom,
+ "Do we have data for www.example.com?"
+ );
+
+ // Clean up.
+ await Sanitizer.sanitize(["cookies", "offlineApps"]);
+
+ if (opt.cookiePermission !== undefined) {
+ let uri = Services.io.newURI("https://www.example.com");
+ PermissionTestUtils.remove(uri, "cookie");
+ }
+}
+
+function runAllCookiePermissionTests(originAttributes) {
+ let tests = [
+ { name: "IDB", createData: createIndexedDB, checkData: checkIndexedDB },
+ {
+ name: "Host Cookie",
+ createData: createHostCookie,
+ checkData: checkCookie,
+ },
+ {
+ name: "Domain Cookie",
+ createData: createDomainCookie,
+ checkData: checkCookie,
+ },
+ ];
+
+ // Delete all, no custom permission, data in example.com, cookie permission set
+ // for www.example.com
+ tests.forEach(methods => {
+ add_task(async function deleteStorageOnShutdown() {
+ info(
+ methods.name +
+ ": Delete all, no custom permission, data in example.com, cookie permission set for www.example.com - OA: " +
+ originAttributes.name
+ );
+ await deleteOnShutdown({
+ sanitize: true,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: undefined,
+ expectedForOrg: false,
+ expectedForCom: false,
+ fullHost: false,
+ });
+ });
+ });
+
+ // Delete all, no custom permission, data in www.example.com, cookie permission
+ // set for www.example.com
+ tests.forEach(methods => {
+ add_task(async function deleteStorageOnShutdown() {
+ info(
+ methods.name +
+ ": Delete all, no custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
+ originAttributes.name
+ );
+ await deleteOnShutdown({
+ sanitize: true,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: undefined,
+ expectedForOrg: false,
+ expectedForCom: false,
+ fullHost: true,
+ });
+ });
+ });
+
+ // All is session, but with ALLOW custom permission, data in example.com,
+ // cookie permission set for www.example.com
+ tests.forEach(methods => {
+ add_task(async function deleteStorageWithCustomPermission() {
+ info(
+ methods.name +
+ ": All is session, but with ALLOW custom permission, data in example.com, cookie permission set for www.example.com - OA: " +
+ originAttributes.name
+ );
+ await deleteOnShutdown({
+ sanitize: true,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: Ci.nsICookiePermission.ACCESS_ALLOW,
+ expectedForOrg: false,
+ expectedForCom: true,
+ fullHost: false,
+ });
+ });
+ });
+
+ // All is session, but with ALLOW custom permission, data in www.example.com,
+ // cookie permission set for www.example.com
+ tests.forEach(methods => {
+ add_task(async function deleteStorageWithCustomPermission() {
+ info(
+ methods.name +
+ ": All is session, but with ALLOW custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
+ originAttributes.name
+ );
+ await deleteOnShutdown({
+ sanitize: true,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: Ci.nsICookiePermission.ACCESS_ALLOW,
+ expectedForOrg: false,
+ expectedForCom: true,
+ fullHost: true,
+ });
+ });
+ });
+
+ // All is default, but with SESSION custom permission, data in example.com,
+ // cookie permission set for www.example.com
+ tests.forEach(methods => {
+ add_task(async function deleteStorageOnlyCustomPermission() {
+ info(
+ methods.name +
+ ": All is default, but with SESSION custom permission, data in example.com, cookie permission set for www.example.com - OA: " +
+ originAttributes.name
+ );
+ await deleteOnShutdown({
+ sanitize: false,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: Ci.nsICookiePermission.ACCESS_SESSION,
+ expectedForOrg: true,
+ // expected data just for example.com when using indexedDB because
+ // QuotaManager deletes for principal.
+ expectedForCom: false,
+ fullHost: false,
+ });
+ });
+ });
+
+ // All is default, but with SESSION custom permission, data in www.example.com,
+ // cookie permission set for www.example.com
+ tests.forEach(methods => {
+ add_task(async function deleteStorageOnlyCustomPermission() {
+ info(
+ methods.name +
+ ": All is default, but with SESSION custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
+ originAttributes.name
+ );
+ await deleteOnShutdown({
+ sanitize: false,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: Ci.nsICookiePermission.ACCESS_SESSION,
+ expectedForOrg: true,
+ expectedForCom: false,
+ fullHost: true,
+ });
+ });
+ });
+
+ // Session mode, but with unsupported custom permission, data in
+ // www.example.com, cookie permission set for www.example.com
+ tests.forEach(methods => {
+ add_task(async function deleteStorageOnlyCustomPermission() {
+ info(
+ methods.name +
+ ": All is session only, but with unsupported custom custom permission, data in www.example.com, cookie permission set for www.example.com - OA: " +
+ originAttributes.name
+ );
+ await deleteOnShutdown({
+ sanitize: true,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: 123, // invalid cookie permission
+ expectedForOrg: false,
+ expectedForCom: false,
+ fullHost: true,
+ });
+ });
+ });
+}
diff --git a/browser/base/content/test/sidebar/browser.ini b/browser/base/content/test/sidebar/browser.ini
new file mode 100644
index 0000000000..5be49123b5
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+
+[browser_sidebar_adopt.js]
+[browser_sidebar_app_locale_changed.js]
+[browser_sidebar_keys.js]
+[browser_sidebar_move.js]
+[browser_sidebar_persist.js]
+[browser_sidebar_switcher.js]
diff --git a/browser/base/content/test/sidebar/browser_sidebar_adopt.js b/browser/base/content/test/sidebar/browser_sidebar_adopt.js
new file mode 100644
index 0000000000..344a71cb9b
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_adopt.js
@@ -0,0 +1,74 @@
+/* This test checks that the SidebarFocused event doesn't fire in adopted
+ * windows when the sidebar gets opened during window opening, to make sure
+ * that sidebars don't steal focus from the page in this case (Bug 1394207).
+ * There's another case not covered here that has the same expected behavior -
+ * during the initial browser startup - but it would be hard to do with a mochitest. */
+
+registerCleanupFunction(() => {
+ SidebarUI.hide();
+});
+
+function failIfSidebarFocusedFires() {
+ ok(false, "This event shouldn't have fired");
+}
+
+add_setup(function () {
+ CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("sidebar-button")
+ );
+});
+
+add_task(async function testAdoptedTwoWindows() {
+ // First open a new window, show the sidebar in that window, and close it.
+ // Then, open another new window and confirm that the sidebar is closed since it is
+ // being adopted from the main window which doesn't have a shown sidebar. See Bug 1407737.
+ info("Ensure that sidebar state is adopted only from the opener");
+
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ await win1.SidebarUI.show("viewBookmarksSidebar");
+ await BrowserTestUtils.closeWindow(win1);
+
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ ok(
+ !win2.document.getElementById("sidebar-button").hasAttribute("checked"),
+ "Sidebar button isn't checked"
+ );
+ ok(!win2.SidebarUI.isOpen, "Sidebar is closed");
+ await BrowserTestUtils.closeWindow(win2);
+});
+
+add_task(async function testEventsReceivedInMainWindow() {
+ info(
+ "Opening the sidebar and expecting both SidebarShown and SidebarFocused events"
+ );
+
+ let initialShown = BrowserTestUtils.waitForEvent(window, "SidebarShown");
+ let initialFocus = BrowserTestUtils.waitForEvent(window, "SidebarFocused");
+
+ await SidebarUI.show("viewBookmarksSidebar");
+ await initialShown;
+ await initialFocus;
+
+ ok(true, "SidebarShown and SidebarFocused events fired on a new window");
+});
+
+add_task(async function testEventReceivedInNewWindow() {
+ info(
+ "Opening a new window and expecting the SidebarFocused event to not fire"
+ );
+
+ let promiseNewWindow = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow();
+
+ let adoptedShown = BrowserTestUtils.waitForEvent(win, "SidebarShown");
+ win.addEventListener("SidebarFocused", failIfSidebarFocusedFires);
+ registerCleanupFunction(async function () {
+ win.removeEventListener("SidebarFocused", failIfSidebarFocusedFires);
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ await promiseNewWindow;
+ await adoptedShown;
+ ok(true, "SidebarShown event fired on an adopted window");
+});
diff --git a/browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js b/browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js
new file mode 100644
index 0000000000..5b07da9839
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests that the sidebar recreates the contents of the <tree> element
+ * for live app locale switching.
+ */
+
+add_task(function cleanup() {
+ registerCleanupFunction(() => {
+ SidebarUI.hide();
+ });
+});
+
+/**
+ * @param {string} sidebarName
+ */
+async function testLiveReloading(sidebarName) {
+ info("Showing the sidebar " + sidebarName);
+ await SidebarUI.show(sidebarName);
+
+ function getTreeChildren() {
+ const sidebarDoc =
+ document.querySelector("#sidebar").contentWindow.document;
+ return sidebarDoc.querySelector(".sidebar-placesTreechildren");
+ }
+
+ const childrenBefore = getTreeChildren();
+ ok(childrenBefore, "Found the sidebar children");
+ is(childrenBefore, getTreeChildren(), "The children start out as equal");
+
+ info("Simulating an app locale change.");
+ Services.obs.notifyObservers(null, "intl:app-locales-changed");
+
+ await TestUtils.waitForCondition(
+ getTreeChildren,
+ "Waiting for a new child tree element."
+ );
+
+ isnot(
+ childrenBefore,
+ getTreeChildren(),
+ "The tree's contents are re-computed."
+ );
+
+ info("Hiding the sidebar");
+ SidebarUI.hide();
+}
+
+add_task(async function test_bookmarks_sidebar() {
+ await testLiveReloading("viewBookmarksSidebar");
+});
+
+add_task(async function test_history_sidebar() {
+ await testLiveReloading("viewHistorySidebar");
+});
+
+add_task(async function test_ext_sidebar_panel_reloaded_on_locale_changes() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `<html>
+ <head>
+ <meta charset="utf-8"/>
+ <script src="sidebar.js"></script>
+ </head>
+ <body>
+ A Test Sidebar
+ </body>
+ </html>`,
+ "sidebar.js": function () {
+ const { browser } = this;
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+ });
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+
+ // Test sidebar is opened on simulated locale changes.
+ info("Switch browser to bidi and expect the sidebar panel to be reloaded");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["intl.l10n.pseudo", "bidi"]],
+ });
+ await extension.awaitMessage("sidebar");
+ is(
+ window.document.documentElement.getAttribute("dir"),
+ "rtl",
+ "browser window changed direction to rtl as expected"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await extension.awaitMessage("sidebar");
+ is(
+ window.document.documentElement.getAttribute("dir"),
+ "ltr",
+ "browser window changed direction to ltr as expected"
+ );
+
+ await extension.unload();
+});
diff --git a/browser/base/content/test/sidebar/browser_sidebar_keys.js b/browser/base/content/test/sidebar/browser_sidebar_keys.js
new file mode 100644
index 0000000000..f12d1cf5f7
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_keys.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testSidebarKeyToggle(key, options, expectedSidebarId) {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {});
+ let promiseShown = BrowserTestUtils.waitForEvent(window, "SidebarShown");
+ EventUtils.synthesizeKey(key, options);
+ await promiseShown;
+ Assert.equal(
+ document.getElementById("sidebar-box").getAttribute("sidebarcommand"),
+ expectedSidebarId
+ );
+ EventUtils.synthesizeKey(key, options);
+ Assert.ok(!SidebarUI.isOpen);
+}
+
+add_task(async function test_sidebar_keys() {
+ registerCleanupFunction(() => SidebarUI.hide());
+
+ await testSidebarKeyToggle("b", { accelKey: true }, "viewBookmarksSidebar");
+
+ let options = { accelKey: true, shiftKey: AppConstants.platform == "macosx" };
+ await testSidebarKeyToggle("h", options, "viewHistorySidebar");
+});
+
+add_task(async function test_sidebar_in_customize_mode() {
+ // Test bug 1756385 - widgets to appear unchecked in customize mode. Test that
+ // the sidebar button widget doesn't appear checked, and that the sidebar
+ // button toggle is inert while in customize mode.
+ let { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+ );
+ registerCleanupFunction(() => SidebarUI.hide());
+
+ let placement = CustomizableUI.getPlacementOfWidget("sidebar-button");
+ if (!(placement?.area == CustomizableUI.AREA_NAVBAR)) {
+ CustomizableUI.addWidgetToArea(
+ "sidebar-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ CustomizableUI.ensureWidgetPlacedInWindow("sidebar-button", window);
+ registerCleanupFunction(function () {
+ CustomizableUI.removeWidgetFromArea("sidebar-button");
+ });
+ }
+
+ let widgetIcon = CustomizableUI.getWidget("sidebar-button")
+ .forWindow(window)
+ .node?.querySelector(".toolbarbutton-icon");
+ // Get the alpha value of the sidebar toggle widget's background
+ let getBGAlpha = () =>
+ InspectorUtils.colorToRGBA(
+ getComputedStyle(widgetIcon).getPropertyValue("background-color")
+ ).a;
+
+ let promiseShown = BrowserTestUtils.waitForEvent(window, "SidebarShown");
+ SidebarUI.show("viewBookmarksSidebar");
+ await promiseShown;
+
+ Assert.greater(
+ getBGAlpha(),
+ 0,
+ "Sidebar widget background should appear checked"
+ );
+
+ // Enter customize mode. This should disable the toggle and make the sidebar
+ // toggle widget appear unchecked.
+ let customizationReadyPromise = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await customizationReadyPromise;
+
+ Assert.equal(
+ getBGAlpha(),
+ 0,
+ "Sidebar widget background should appear unchecked"
+ );
+
+ // Attempt toggle - should fail in customize mode.
+ await SidebarUI.toggle();
+ ok(SidebarUI.isOpen, "Sidebar is still open");
+
+ // Exit customize mode. This should re-enable the toggle and make the sidebar
+ // toggle widget appear checked again, since toggle() didn't hide the sidebar.
+ let afterCustomizationPromise = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "aftercustomization"
+ );
+ gCustomizeMode.exit();
+ await afterCustomizationPromise;
+
+ Assert.greater(
+ getBGAlpha(),
+ 0,
+ "Sidebar widget background should appear checked again"
+ );
+
+ await SidebarUI.toggle();
+ ok(!SidebarUI.isOpen, "Sidebar is closed");
+ Assert.equal(
+ getBGAlpha(),
+ 0,
+ "Sidebar widget background should appear unchecked"
+ );
+});
diff --git a/browser/base/content/test/sidebar/browser_sidebar_move.js b/browser/base/content/test/sidebar/browser_sidebar_move.js
new file mode 100644
index 0000000000..3de26b7966
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_move.js
@@ -0,0 +1,72 @@
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("sidebar.position_start");
+ SidebarUI.hide();
+});
+
+const EXPECTED_START_ORDINALS = [
+ ["sidebar-box", 1],
+ ["sidebar-splitter", 2],
+ ["appcontent", 3],
+];
+
+const EXPECTED_END_ORDINALS = [
+ ["sidebar-box", 3],
+ ["sidebar-splitter", 2],
+ ["appcontent", 1],
+];
+
+function getBrowserChildrenWithOrdinals() {
+ let browser = document.getElementById("browser");
+ return [...browser.children].map(node => {
+ return [node.id, node.style.order];
+ });
+}
+
+add_task(async function () {
+ await SidebarUI.show("viewBookmarksSidebar");
+ SidebarUI.showSwitcherPanel();
+
+ let reversePositionButton = document.getElementById(
+ "sidebar-reverse-position"
+ );
+ let originalLabel = reversePositionButton.getAttribute("label");
+ let box = document.getElementById("sidebar-box");
+
+ // Default (position: left)
+ Assert.deepEqual(
+ getBrowserChildrenWithOrdinals(),
+ EXPECTED_START_ORDINALS,
+ "Correct ordinal (start)"
+ );
+ ok(!box.hasAttribute("positionend"), "Positioned start");
+
+ // Moved to right
+ SidebarUI.reversePosition();
+ SidebarUI.showSwitcherPanel();
+ Assert.deepEqual(
+ getBrowserChildrenWithOrdinals(),
+ EXPECTED_END_ORDINALS,
+ "Correct ordinal (end)"
+ );
+ isnot(
+ reversePositionButton.getAttribute("label"),
+ originalLabel,
+ "Label changed"
+ );
+ ok(box.hasAttribute("positionend"), "Positioned end");
+
+ // Moved to back to left
+ SidebarUI.reversePosition();
+ SidebarUI.showSwitcherPanel();
+ Assert.deepEqual(
+ getBrowserChildrenWithOrdinals(),
+ EXPECTED_START_ORDINALS,
+ "Correct ordinal (start)"
+ );
+ ok(!box.hasAttribute("positionend"), "Positioned start");
+ is(
+ reversePositionButton.getAttribute("label"),
+ originalLabel,
+ "Label is back to normal"
+ );
+});
diff --git a/browser/base/content/test/sidebar/browser_sidebar_persist.js b/browser/base/content/test/sidebar/browser_sidebar_persist.js
new file mode 100644
index 0000000000..fe67bed9e0
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_persist.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 persist_sidebar_width() {
+ {
+ // Make the main test window not count as a browser window any longer,
+ // which allows the persitence code to kick in.
+ const docEl = document.documentElement;
+ const oldWinType = docEl.getAttribute("windowtype");
+ docEl.setAttribute("windowtype", "navigator:testrunner");
+ registerCleanupFunction(() => {
+ docEl.setAttribute("windowtype", oldWinType);
+ });
+ }
+
+ {
+ info("Showing new window and setting sidebar box");
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ await win.SidebarUI.show("viewBookmarksSidebar");
+ win.document.getElementById("sidebar-box").style.width = "100px";
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ {
+ info("Showing new window and seeing persisted width");
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ await win.SidebarUI.show("viewBookmarksSidebar");
+ is(
+ win.document.getElementById("sidebar-box").style.width,
+ "100px",
+ "Width style should be persisted"
+ );
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/browser/base/content/test/sidebar/browser_sidebar_switcher.js b/browser/base/content/test/sidebar/browser_sidebar_switcher.js
new file mode 100644
index 0000000000..81d7c29776
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_switcher.js
@@ -0,0 +1,64 @@
+registerCleanupFunction(() => {
+ SidebarUI.hide();
+});
+
+function showSwitcherPanelPromise() {
+ return new Promise(resolve => {
+ SidebarUI._switcherPanel.addEventListener(
+ "popupshown",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ SidebarUI.showSwitcherPanel();
+ });
+}
+
+function clickSwitcherButton(querySelector) {
+ let sidebarPopup = document.querySelector("#sidebarMenu-popup");
+ let switcherPromise = Promise.all([
+ BrowserTestUtils.waitForEvent(window, "SidebarFocused"),
+ BrowserTestUtils.waitForEvent(sidebarPopup, "popuphidden"),
+ ]);
+ document.querySelector(querySelector).click();
+ return switcherPromise;
+}
+
+add_task(async function () {
+ // If a sidebar is already open, close it.
+ if (!document.getElementById("sidebar-box").hidden) {
+ ok(
+ false,
+ "Unexpected sidebar found - a previous test failed to cleanup correctly"
+ );
+ SidebarUI.hide();
+ }
+
+ let sidebar = document.querySelector("#sidebar-box");
+ await SidebarUI.show("viewBookmarksSidebar");
+
+ await showSwitcherPanelPromise();
+ await clickSwitcherButton("#sidebar-switcher-history");
+ is(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewHistorySidebar",
+ "History sidebar loaded"
+ );
+
+ await showSwitcherPanelPromise();
+ await clickSwitcherButton("#sidebar-switcher-tabs");
+ is(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewTabsSidebar",
+ "Tabs sidebar loaded"
+ );
+
+ await showSwitcherPanelPromise();
+ await clickSwitcherButton("#sidebar-switcher-bookmarks");
+ is(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewBookmarksSidebar",
+ "Bookmarks sidebar loaded"
+ );
+});
diff --git a/browser/base/content/test/siteIdentity/browser.ini b/browser/base/content/test/siteIdentity/browser.ini
new file mode 100644
index 0000000000..724669f18a
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser.ini
@@ -0,0 +1,152 @@
+[DEFAULT]
+support-files =
+ head.js
+ dummy_page.html
+ !/image/test/mochitest/blue.png
+
+[browser_about_blank_same_document_tabswitch.js]
+https_first_disabled = true
+support-files =
+ open-self-from-frame.html
+[browser_bug1045809.js]
+tags = mcb
+support-files =
+ file_bug1045809_1.html
+ file_bug1045809_2.html
+[browser_bug822367.js]
+tags = mcb
+support-files =
+ file_bug822367_1.html
+ file_bug822367_1.js
+ file_bug822367_2.html
+ file_bug822367_3.html
+ file_bug822367_4.html
+ file_bug822367_4.js
+ file_bug822367_4B.html
+ file_bug822367_5.html
+ file_bug822367_6.html
+[browser_bug902156.js]
+tags = mcb
+support-files =
+ file_bug902156.js
+ file_bug902156_1.html
+ file_bug902156_2.html
+ file_bug902156_3.html
+[browser_bug906190.js]
+tags = mcb
+support-files =
+ file_bug906190_1.html
+ file_bug906190_2.html
+ file_bug906190_3_4.html
+ file_bug906190_redirected.html
+ file_bug906190.js
+ file_bug906190.sjs
+[browser_check_identity_state.js]
+https_first_disabled = true
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_check_identity_state_pdf.js]
+https_first_disabled = true
+support-files =
+ file_pdf.pdf
+ file_pdf_blob.html
+[browser_csp_block_all_mixedcontent.js]
+tags = mcb
+support-files =
+ file_csp_block_all_mixedcontent.html
+ file_csp_block_all_mixedcontent.js
+[browser_deprecatedTLSVersions.js]
+[browser_geolocation_indicator.js]
+[browser_getSecurityInfo.js]
+https_first_disabled = true
+support-files =
+ dummy_iframe_page.html
+[browser_identityBlock_flicker.js]
+[browser_identityBlock_focus.js]
+support-files = ../permissions/permissions.html
+[browser_identityIcon_img_url.js]
+https_first_disabled = true
+support-files =
+ file_mixedPassiveContent.html
+ file_csp_block_all_mixedcontent.html
+[browser_identityPopup_HttpsOnlyMode.js]
+[browser_identityPopup_clearSiteData.js]
+skip-if = (os == "linux" && bits == 64) # Bug 1577395
+[browser_identityPopup_clearSiteData_extensions.js]
+[browser_identityPopup_custom_roots.js]
+https_first_disabled = true
+[browser_identityPopup_focus.js]
+skip-if =
+ verify
+ os == "linux" && (asan || tsan) # Bug 1723899
+[browser_identity_UI.js]
+https_first_disabled = true
+[browser_iframe_navigation.js]
+https_first_disabled = true
+support-files =
+ iframe_navigation.html
+[browser_ignore_same_page_navigation.js]
+[browser_mcb_redirect.js]
+https_first_disabled = true
+tags = mcb
+support-files =
+ test_mcb_redirect.html
+ test_mcb_redirect_image.html
+ test_mcb_double_redirect_image.html
+ test_mcb_redirect.js
+ test_mcb_redirect.sjs
+[browser_mixedContentFramesOnHttp.js]
+https_first_disabled = true
+tags = mcb
+support-files =
+ file_mixedContentFramesOnHttp.html
+ file_mixedPassiveContent.html
+[browser_mixedContentFromOnunload.js]
+https_first_disabled = true
+tags = mcb
+support-files =
+ file_mixedContentFromOnunload.html
+ file_mixedContentFromOnunload_test1.html
+ file_mixedContentFromOnunload_test2.html
+[browser_mixed_content_cert_override.js]
+skip-if = verify
+tags = mcb
+support-files =
+ test-mixedcontent-securityerrors.html
+[browser_mixed_content_with_navigation.js]
+tags = mcb
+support-files =
+ file_mixedPassiveContent.html
+ file_bug1045809_1.html
+[browser_mixed_passive_content_indicator.js]
+tags = mcb
+support-files =
+ simple_mixed_passive.html
+[browser_mixedcontent_securityflags.js]
+tags = mcb
+support-files =
+ test-mixedcontent-securityerrors.html
+[browser_navigation_failures.js]
+[browser_no_mcb_for_loopback.js]
+tags = mcb
+support-files =
+ ../general/moz.png
+ test_no_mcb_for_loopback.html
+[browser_no_mcb_for_onions.js]
+tags = mcb
+support-files =
+ test_no_mcb_for_onions.html
+[browser_no_mcb_on_http_site.js]
+https_first_disabled = true
+tags = mcb
+support-files =
+ test_no_mcb_on_http_site_img.html
+ test_no_mcb_on_http_site_img.css
+ test_no_mcb_on_http_site_font.html
+ test_no_mcb_on_http_site_font.css
+ test_no_mcb_on_http_site_font2.html
+ test_no_mcb_on_http_site_font2.css
+[browser_secure_transport_insecure_scheme.js]
+https_first_disabled = true
+[browser_session_store_pageproxystate.js]
+[browser_tab_sharing_state.js]
diff --git a/browser/base/content/test/siteIdentity/browser_about_blank_same_document_tabswitch.js b/browser/base/content/test/siteIdentity/browser_about_blank_same_document_tabswitch.js
new file mode 100644
index 0000000000..5e58b4bedb
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_about_blank_same_document_tabswitch.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org"
+);
+
+const TEST_PAGE = TEST_PATH + "open-self-from-frame.html";
+
+add_task(async function test_identityBlock_inherited_blank() {
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ let identityBox = document.getElementById("identity-box");
+ // Ensure we remove the 3rd party storage permission for example.org, or
+ // it'll mess up other tests:
+ let principal = browser.contentPrincipal;
+ registerCleanupFunction(() => {
+ Services.perms.removeFromPrincipal(
+ principal,
+ "3rdPartyStorage^http://example.org"
+ );
+ });
+ is(
+ identityBox.className,
+ "verifiedDomain",
+ "Should indicate a secure site."
+ );
+ // Open a popup from the web content.
+ let popupPromise = BrowserTestUtils.waitForNewWindow();
+ await SpecialPowers.spawn(browser, [TEST_PAGE], testPage => {
+ content.open(testPage, "_blank", "height=300,width=300");
+ });
+ // Open a tab back in the main window:
+ let popup = await popupPromise;
+ info("Opened popup");
+ let popupBC = popup.gBrowser.selectedBrowser.browsingContext;
+ await TestUtils.waitForCondition(
+ () => popupBC.children[0]?.currentWindowGlobal
+ );
+
+ info("Waiting for button to appear");
+ await SpecialPowers.spawn(popupBC.children[0], [], async () => {
+ await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector("button")
+ );
+ });
+
+ info("Got frame contents.");
+
+ let otherTabPromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_PAGE
+ );
+ info("Clicking button");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "button",
+ {},
+ popupBC.children[0]
+ );
+ info("Waiting for tab");
+ await otherTabPromise;
+
+ ok(
+ gURLBar.value.startsWith("example.org/"),
+ "URL bar value should be correct, was " + gURLBar.value
+ );
+ is(
+ identityBox.className,
+ "notSecure",
+ "Identity box should have been updated."
+ );
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await BrowserTestUtils.closeWindow(popup);
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_bug1045809.js b/browser/base/content/test/siteIdentity/browser_bug1045809.js
new file mode 100644
index 0000000000..b39d669d0b
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug1045809.js
@@ -0,0 +1,105 @@
+// Test that the Mixed Content Doorhanger Action to re-enable protection works
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_INSECURE = "security.insecure_connection_icon.enabled";
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_bug1045809_1.html";
+
+var origBlockActive;
+
+add_task(async function () {
+ registerCleanupFunction(function () {
+ Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
+ gBrowser.removeCurrentTab();
+ });
+
+ // Store original preferences so we can restore settings after testing
+ origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE);
+
+ // Make sure mixed content blocking is on
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ // Check with insecure lock disabled
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_INSECURE, false]] });
+ await runTests(tab);
+
+ // Check with insecure lock disabled
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_INSECURE, true]] });
+ await runTests(tab);
+});
+
+async function runTests(tab) {
+ // Test 1: mixed content must be blocked
+ await promiseTabLoadEvent(tab, TEST_URL);
+ await test1(gBrowser.getBrowserForTab(tab));
+
+ await promiseTabLoadEvent(tab);
+ // Test 2: mixed content must NOT be blocked
+ await test2(gBrowser.getBrowserForTab(tab));
+
+ // Test 3: mixed content must be blocked again
+ await promiseTabLoadEvent(tab);
+ await test3(gBrowser.getBrowserForTab(tab));
+}
+
+async function test1(gTestBrowser) {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gTestBrowser, [], function () {
+ let iframe = content.document.getElementsByTagName("iframe")[0];
+
+ SpecialPowers.spawn(iframe, [], () => {
+ let container = content.document.getElementById("mixedContentContainer");
+ is(container, null, "Mixed Content is NOT to be found in Test1");
+ });
+ });
+
+ // Disable Mixed Content Protection for the page (and reload)
+ gIdentityHandler.disableMixedContentProtection();
+}
+
+async function test2(gTestBrowser) {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gTestBrowser, [], function () {
+ let iframe = content.document.getElementsByTagName("iframe")[0];
+
+ SpecialPowers.spawn(iframe, [], () => {
+ let container = content.document.getElementById("mixedContentContainer");
+ isnot(container, null, "Mixed Content is to be found in Test2");
+ });
+ });
+
+ // Re-enable Mixed Content Protection for the page (and reload)
+ gIdentityHandler.enableMixedContentProtection();
+}
+
+async function test3(gTestBrowser) {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gTestBrowser, [], function () {
+ let iframe = content.document.getElementsByTagName("iframe")[0];
+
+ SpecialPowers.spawn(iframe, [], () => {
+ let container = content.document.getElementById("mixedContentContainer");
+ is(container, null, "Mixed Content is NOT to be found in Test3");
+ });
+ });
+}
diff --git a/browser/base/content/test/siteIdentity/browser_bug822367.js b/browser/base/content/test/siteIdentity/browser_bug822367.js
new file mode 100644
index 0000000000..881c920899
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug822367.js
@@ -0,0 +1,254 @@
+/*
+ * User Override Mixed Content Block - Tests for Bug 822367
+ */
+
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_DISPLAY_UPGRADE = "security.mixed_content.upgrade_display_content";
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+
+// We alternate for even and odd test cases to simulate different hosts
+const HTTPS_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const HTTPS_TEST_ROOT_2 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://test1.example.com"
+);
+
+var gTestBrowser = null;
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_DISPLAY, true],
+ [PREF_DISPLAY_UPGRADE, false],
+ [PREF_ACTIVE, true],
+ ],
+ });
+
+ var newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop();
+
+ // Mixed Script Test
+ var url = HTTPS_TEST_ROOT + "file_bug822367_1.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+// Mixed Script Test
+add_task(async function MixedTest1A() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ gTestBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(async function MixedTest1B() {
+ await SpecialPowers.spawn(gTestBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "hello",
+ "Waited too long for mixed script to run in Test 1"
+ );
+ });
+ gTestBrowser.ownerGlobal.gIdentityHandler.enableMixedContentProtectionNoReload();
+});
+
+// Mixed Display Test - Doorhanger should not appear
+add_task(async function MixedTest2() {
+ var url = HTTPS_TEST_ROOT_2 + "file_bug822367_2.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+});
+
+// Mixed Script and Display Test - User Override should cause both the script and the image to load.
+add_task(async function MixedTest3() {
+ var url = HTTPS_TEST_ROOT + "file_bug822367_3.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+add_task(async function MixedTest3A() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ gTestBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(async function MixedTest3B() {
+ await SpecialPowers.spawn(gTestBrowser, [], async function () {
+ let p1 = ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "hello",
+ "Waited too long for mixed script to run in Test 3"
+ );
+ let p2 = ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p2").innerHTML == "bye",
+ "Waited too long for mixed image to load in Test 3"
+ );
+ await Promise.all([p1, p2]);
+ });
+
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: true,
+ });
+ gTestBrowser.ownerGlobal.gIdentityHandler.enableMixedContentProtectionNoReload();
+});
+
+// Location change - User override on one page doesn't propagate to another page after location change.
+add_task(async function MixedTest4() {
+ var url = HTTPS_TEST_ROOT_2 + "file_bug822367_4.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+let preLocationChangePrincipal = null;
+add_task(async function MixedTest4A() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ preLocationChangePrincipal = gTestBrowser.contentPrincipal;
+ gTestBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(async function MixedTest4B() {
+ let url = HTTPS_TEST_ROOT + "file_bug822367_4B.html";
+ await SpecialPowers.spawn(gTestBrowser, [url], async function (wantedUrl) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.location == wantedUrl,
+ "Waited too long for mixed script to run in Test 4"
+ );
+ });
+});
+
+add_task(async function MixedTest4C() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gTestBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "",
+ "Mixed script loaded in test 4 after location change!"
+ );
+ });
+ SitePermissions.removeFromPrincipal(
+ preLocationChangePrincipal,
+ "mixed-content"
+ );
+});
+
+// Mixed script attempts to load in a document.open()
+add_task(async function MixedTest5() {
+ var url = HTTPS_TEST_ROOT + "file_bug822367_5.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+add_task(async function MixedTest5A() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ gTestBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(async function MixedTest5B() {
+ await SpecialPowers.spawn(gTestBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "hello",
+ "Waited too long for mixed script to run in Test 5"
+ );
+ });
+ gTestBrowser.ownerGlobal.gIdentityHandler.enableMixedContentProtectionNoReload();
+});
+
+// Mixed script attempts to load in a document.open() that is within an iframe.
+add_task(async function MixedTest6() {
+ var url = HTTPS_TEST_ROOT_2 + "file_bug822367_6.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+add_task(async function MixedTest6A() {
+ gTestBrowser.removeEventListener("load", MixedTest6A, true);
+ let { gIdentityHandler } = gTestBrowser.ownerGlobal;
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"),
+ "Waited too long for control center to get mixed active blocked state"
+ );
+});
+
+add_task(async function MixedTest6B() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ gTestBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(async function MixedTest6C() {
+ await SpecialPowers.spawn(gTestBrowser, [], async function () {
+ function test() {
+ try {
+ return (
+ content.document
+ .getElementById("f1")
+ .contentDocument.getElementById("p1").innerHTML == "hello"
+ );
+ } catch (e) {
+ return false;
+ }
+ }
+
+ await ContentTaskUtils.waitForCondition(
+ test,
+ "Waited too long for mixed script to run in Test 6"
+ );
+ });
+});
+
+add_task(async function MixedTest6D() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+ gTestBrowser.ownerGlobal.gIdentityHandler.enableMixedContentProtectionNoReload();
+});
+
+add_task(async function cleanup() {
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/siteIdentity/browser_bug902156.js b/browser/base/content/test/siteIdentity/browser_bug902156.js
new file mode 100644
index 0000000000..3485771427
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug902156.js
@@ -0,0 +1,171 @@
+/*
+ * Description of the Tests for
+ * - Bug 902156: Persist "disable protection" option for Mixed Content Blocker
+ *
+ * 1. Navigate to the same domain via document.location
+ * - Load a html page which has mixed content
+ * - Control Center button to disable protection appears - we disable it
+ * - Load a new page from the same origin using document.location
+ * - Control Center button should not appear anymore!
+ *
+ * 2. Navigate to the same domain via simulateclick for a link on the page
+ * - Load a html page which has mixed content
+ * - Control Center button to disable protection appears - we disable it
+ * - Load a new page from the same origin simulating a click
+ * - Control Center button should not appear anymore!
+ *
+ * 3. Navigate to a differnet domain and show the content is still blocked
+ * - Load a different html page which has mixed content
+ * - Control Center button to disable protection should appear again because
+ * we navigated away from html page where we disabled the protection.
+ *
+ * Note, for all tests we set gHttpTestRoot to use 'https'.
+ */
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+
+// We alternate for even and odd test cases to simulate different hosts.
+const HTTPS_TEST_ROOT_1 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://test1.example.com"
+);
+const HTTPS_TEST_ROOT_2 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://test2.example.com"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_ACTIVE, true]] });
+});
+
+add_task(async function test1() {
+ let url = HTTPS_TEST_ROOT_1 + "file_bug902156_1.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ // Disable Mixed Content Protection for the page (and reload)
+ let browserLoaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ let { gIdentityHandler } = browser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ await browserLoaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let expected = "Mixed Content Blocker disabled";
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.getElementById("mctestdiv").innerHTML == expected,
+ "Error: Waited too long for mixed script to run in Test 1"
+ );
+
+ let actual = content.document.getElementById("mctestdiv").innerHTML;
+ is(
+ actual,
+ "Mixed Content Blocker disabled",
+ "OK: Executed mixed script in Test 1"
+ );
+ });
+
+ // The Script loaded after we disabled the page, now we are going to reload the
+ // page and see if our decision is persistent
+ url = HTTPS_TEST_ROOT_1 + "file_bug902156_2.html";
+ browserLoaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.loadURIString(browser, url);
+ await browserLoaded;
+
+ // The Control Center button should appear but isMixedContentBlocked should be NOT true,
+ // because our decision of disabling the mixed content blocker is persistent.
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+ await SpecialPowers.spawn(browser, [], function () {
+ let actual = content.document.getElementById("mctestdiv").innerHTML;
+ is(
+ actual,
+ "Mixed Content Blocker disabled",
+ "OK: Executed mixed script in Test 1"
+ );
+ });
+ gIdentityHandler.enableMixedContentProtection();
+ });
+});
+
+// ------------------------ Test 2 ------------------------------
+
+add_task(async function test2() {
+ let url = HTTPS_TEST_ROOT_2 + "file_bug902156_2.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ // Disable Mixed Content Protection for the page (and reload)
+ let browserLoaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ let { gIdentityHandler } = browser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ await browserLoaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let expected = "Mixed Content Blocker disabled";
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.getElementById("mctestdiv").innerHTML == expected,
+ "Error: Waited too long for mixed script to run in Test 2"
+ );
+
+ let actual = content.document.getElementById("mctestdiv").innerHTML;
+ is(
+ actual,
+ "Mixed Content Blocker disabled",
+ "OK: Executed mixed script in Test 2"
+ );
+ });
+
+ // The Script loaded after we disabled the page, now we are going to reload the
+ // page and see if our decision is persistent
+ url = HTTPS_TEST_ROOT_2 + "file_bug902156_1.html";
+ browserLoaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ // reload the page using the provided link in the html file
+ await SpecialPowers.spawn(browser, [], function () {
+ let mctestlink = content.document.getElementById("mctestlink");
+ mctestlink.click();
+ });
+ await browserLoaded;
+
+ // The Control Center button should appear but isMixedContentBlocked should be NOT true,
+ // because our decision of disabling the mixed content blocker is persistent.
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(browser, [], function () {
+ let actual = content.document.getElementById("mctestdiv").innerHTML;
+ is(
+ actual,
+ "Mixed Content Blocker disabled",
+ "OK: Executed mixed script in Test 2"
+ );
+ });
+ gIdentityHandler.enableMixedContentProtection();
+ });
+});
+
+add_task(async function test3() {
+ let url = HTTPS_TEST_ROOT_1 + "file_bug902156_3.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_bug906190.js b/browser/base/content/test/siteIdentity/browser_bug906190.js
new file mode 100644
index 0000000000..a0410e76cb
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug906190.js
@@ -0,0 +1,340 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the persistence of the "disable protection" option for Mixed Content
+ * Blocker in child tabs (bug 906190).
+ */
+
+requestLongerTimeout(2);
+
+// We use the different urls for testing same origin checks before allowing
+// mixed content on child tabs.
+const HTTPS_TEST_ROOT_1 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://test1.example.com"
+);
+const HTTPS_TEST_ROOT_2 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://test2.example.com"
+);
+
+/**
+ * For all tests, we load the pages over HTTPS and test both:
+ * - |CTRL+CLICK|
+ * - |RIGHT CLICK -> OPEN LINK IN TAB|
+ */
+async function doTest(
+ parentTabSpec,
+ childTabSpec,
+ testTaskFn,
+ waitForMetaRefresh
+) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: parentTabSpec,
+ },
+ async function (browser) {
+ // As a sanity check, test that active content has been blocked as expected.
+ await assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ // Disable the Mixed Content Blocker for the page, which reloads it.
+ let promiseReloaded = BrowserTestUtils.browserLoaded(browser);
+ let principal = gBrowser.contentPrincipal;
+ gIdentityHandler.disableMixedContentProtection();
+ await promiseReloaded;
+
+ // Wait for the script in the page to update the contents of the test div.
+ await SpecialPowers.spawn(
+ browser,
+ [childTabSpec],
+ async childTabSpecContent => {
+ let testDiv = content.document.getElementById("mctestdiv");
+ await ContentTaskUtils.waitForCondition(
+ () => testDiv.innerHTML == "Mixed Content Blocker disabled"
+ );
+
+ // Add the link for the child tab to the page.
+ let mainDiv = content.document.createElement("div");
+
+ // eslint-disable-next-line no-unsanitized/property
+ mainDiv.innerHTML =
+ '<p><a id="linkToOpenInNewTab" href="' +
+ childTabSpecContent +
+ '">Link</a></p>';
+ content.document.body.appendChild(mainDiv);
+ }
+ );
+
+ // Execute the test in the child tabs with the two methods to open it.
+ for (let openFn of [simulateCtrlClick, simulateContextMenuOpenInTab]) {
+ let promiseTabLoaded = waitForSomeTabToLoad();
+ openFn(browser);
+ await promiseTabLoaded;
+ gBrowser.selectTabAtIndex(2);
+
+ if (waitForMetaRefresh) {
+ await waitForSomeTabToLoad();
+ }
+
+ await testTaskFn();
+
+ gBrowser.removeCurrentTab();
+ }
+
+ SitePermissions.removeFromPrincipal(principal, "mixed-content");
+ }
+ );
+}
+
+function simulateCtrlClick(browser) {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#linkToOpenInNewTab",
+ { ctrlKey: true, metaKey: true },
+ browser
+ );
+}
+
+function simulateContextMenuOpenInTab(browser) {
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-openlinkintab").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#linkToOpenInNewTab",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+}
+
+// Waits for a load event somewhere in the browser but ignore events coming
+// from <xul:browser>s without a tab assigned. That are most likely browsers
+// that preload the new tab page.
+function waitForSomeTabToLoad() {
+ return BrowserTestUtils.firstBrowserLoaded(window, true, browser => {
+ let tab = gBrowser.getTabForBrowser(browser);
+ return !!tab;
+ });
+}
+
+/**
+ * Ensure the Mixed Content Blocker is enabled.
+ */
+add_task(async function test_initialize() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.block_active_content", true],
+ // We need to disable the dFPI heuristic. So, we won't have unnecessary
+ // 3rd party cookie permission that could affect following tests because
+ // it will create a permission icon on the URL bar.
+ ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false],
+ ],
+ });
+});
+
+/**
+ * 1. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a subpage from the same origin in a new tab simulating a click
+ * - Doorhanger should >> NOT << appear anymore!
+ */
+add_task(async function test_same_origin() {
+ await doTest(
+ HTTPS_TEST_ROOT_1 + "file_bug906190_1.html",
+ HTTPS_TEST_ROOT_1 + "file_bug906190_2.html",
+ async function () {
+ // The doorhanger should appear but activeBlocked should be >> NOT << true,
+ // because our decision of disabling the mixed content blocker is persistent
+ // across tabs.
+ await assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ Assert.equal(
+ content.document.getElementById("mctestdiv").innerHTML,
+ "Mixed Content Blocker disabled",
+ "OK: Executed mixed script"
+ );
+ });
+ }
+ );
+});
+
+/**
+ * 2. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from a different origin in a new tab simulating a click
+ * - Doorhanger >> SHOULD << appear again!
+ */
+add_task(async function test_different_origin() {
+ await doTest(
+ HTTPS_TEST_ROOT_1 + "file_bug906190_2.html",
+ HTTPS_TEST_ROOT_2 + "file_bug906190_2.html",
+ async function () {
+ // The doorhanger should appear and activeBlocked should be >> TRUE <<,
+ // because our decision of disabling the mixed content blocker should only
+ // persist if pages are from the same domain.
+ await assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ Assert.equal(
+ content.document.getElementById("mctestdiv").innerHTML,
+ "Mixed Content Blocker enabled",
+ "OK: Blocked mixed script"
+ );
+ });
+ }
+ );
+});
+
+/**
+ * 3. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from the same origin using meta-refresh
+ * - Doorhanger should >> NOT << appear again!
+ */
+add_task(async function test_same_origin_metarefresh_same_origin() {
+ // file_bug906190_3_4.html redirects to page test1.example.com/* using meta-refresh
+ await doTest(
+ HTTPS_TEST_ROOT_1 + "file_bug906190_1.html",
+ HTTPS_TEST_ROOT_1 + "file_bug906190_3_4.html",
+ async function () {
+ // The doorhanger should appear but activeBlocked should be >> NOT << true!
+ await assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ Assert.equal(
+ content.document.getElementById("mctestdiv").innerHTML,
+ "Mixed Content Blocker disabled",
+ "OK: Executed mixed script"
+ );
+ });
+ },
+ true
+ );
+});
+
+/**
+ * 4. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from a different origin using meta-refresh
+ * - Doorhanger >> SHOULD << appear again!
+ */
+add_task(async function test_same_origin_metarefresh_different_origin() {
+ await doTest(
+ HTTPS_TEST_ROOT_2 + "file_bug906190_1.html",
+ HTTPS_TEST_ROOT_2 + "file_bug906190_3_4.html",
+ async function () {
+ // The doorhanger should appear and activeBlocked should be >> TRUE <<.
+ await assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ Assert.equal(
+ content.document.getElementById("mctestdiv").innerHTML,
+ "Mixed Content Blocker enabled",
+ "OK: Blocked mixed script"
+ );
+ });
+ },
+ true
+ );
+});
+
+/**
+ * 5. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from the same origin using 302 redirect
+ */
+add_task(async function test_same_origin_302redirect_same_origin() {
+ // the sjs files returns a 302 redirect- note, same origins
+ await doTest(
+ HTTPS_TEST_ROOT_1 + "file_bug906190_1.html",
+ HTTPS_TEST_ROOT_1 + "file_bug906190.sjs",
+ async function () {
+ // The doorhanger should appear but activeBlocked should be >> NOT << true.
+ // Currently it is >> TRUE << - see follow up bug 914860
+ ok(
+ !gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"),
+ "OK: Mixed Content is NOT being blocked"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ Assert.equal(
+ content.document.getElementById("mctestdiv").innerHTML,
+ "Mixed Content Blocker disabled",
+ "OK: Executed mixed script"
+ );
+ });
+ }
+ );
+});
+
+/**
+ * 6. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from a different origin using 302 redirect
+ */
+add_task(async function test_same_origin_302redirect_different_origin() {
+ // the sjs files returns a 302 redirect - note, different origins
+ await doTest(
+ HTTPS_TEST_ROOT_2 + "file_bug906190_1.html",
+ HTTPS_TEST_ROOT_2 + "file_bug906190.sjs",
+ async function () {
+ // The doorhanger should appear and activeBlocked should be >> TRUE <<.
+ await assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ Assert.equal(
+ content.document.getElementById("mctestdiv").innerHTML,
+ "Mixed Content Blocker enabled",
+ "OK: Blocked mixed script"
+ );
+ });
+ }
+ );
+});
+
+/**
+ * 7. - Test memory leak issue on redirection error. See Bug 1269426.
+ */
+add_task(async function test_bad_redirection() {
+ // the sjs files returns a 302 redirect - note, different origins
+ await doTest(
+ HTTPS_TEST_ROOT_2 + "file_bug906190_1.html",
+ HTTPS_TEST_ROOT_2 + "file_bug906190.sjs?bad-redirection=1",
+ function () {
+ // Nothing to do. Just see if memory leak is reported in the end.
+ ok(true, "Nothing to do");
+ }
+ );
+});
diff --git a/browser/base/content/test/siteIdentity/browser_check_identity_state.js b/browser/base/content/test/siteIdentity/browser_check_identity_state.js
new file mode 100644
index 0000000000..e5ecbc66f2
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_check_identity_state.js
@@ -0,0 +1,882 @@
+/*
+ * Test the identity mode UI for a variety of page types
+ */
+
+"use strict";
+
+const DUMMY = "browser/browser/base/content/test/siteIdentity/dummy_page.html";
+const INSECURE_ICON_PREF = "security.insecure_connection_icon.enabled";
+const INSECURE_TEXT_PREF = "security.insecure_connection_text.enabled";
+const INSECURE_PBMODE_ICON_PREF =
+ "security.insecure_connection_icon.pbmode.enabled";
+const HTTPS_FIRST_PBM_PREF = "dom.security.https_first_pbm";
+
+function loadNewTab(url) {
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
+}
+
+function getConnectionState() {
+ // Prevents items that are being lazy loaded causing issues
+ document.getElementById("identity-icon-box").click();
+ gIdentityHandler.refreshIdentityPopup();
+ return document.getElementById("identity-popup").getAttribute("connection");
+}
+
+function getSecurityConnectionBG() {
+ // Get the background image of the security connection.
+ document.getElementById("identity-icon-box").click();
+ gIdentityHandler.refreshIdentityPopup();
+ return gBrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-mainView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("list-style-image");
+}
+
+async function getReaderModeURL() {
+ // Gets the reader mode URL from "identity-popup mainView panel header span"
+ document.getElementById("identity-icon-box").click();
+ gIdentityHandler.refreshIdentityPopup();
+
+ let headerSpan = document.getElementById(
+ "identity-popup-mainView-panel-header-span"
+ );
+ await BrowserTestUtils.waitForCondition(() =>
+ headerSpan.innerHTML.includes("example.com")
+ );
+ return headerSpan.innerHTML;
+}
+
+// This test is slow on Linux debug e10s
+requestLongerTimeout(2);
+
+add_task(async function chromeUITest() {
+ // needs to be set due to bug in ion.js that occurs when testing
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["toolkit.pioneer.testCachedContent", "[]"],
+ ["toolkit.pioneer.testCachedAddons", "[]"],
+ ],
+ });
+ // Might needs to be extended with new secure chrome pages
+ // about:debugging is a secure chrome UI but is not tested for causing problems.
+ let secureChromePages = [
+ "addons",
+ "cache",
+ "certificate",
+ "compat",
+ "config",
+ "downloads",
+ "ion",
+ "license",
+ "logins",
+ "loginsimportreport",
+ "performance",
+ "plugins",
+ "policies",
+ "preferences",
+ "processes",
+ "profiles",
+ "profiling",
+ "protections",
+ "rights",
+ "sessionrestore",
+ "studies",
+ "support",
+ "telemetry",
+ "welcomeback",
+ ];
+
+ // else skip about:crashes, it is only available with plugin
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ secureChromePages.push("crashes");
+ }
+
+ let nonSecureExamplePages = [
+ "about:about",
+ "about:credits",
+ "about:home",
+ "about:logo",
+ "about:memory",
+ "about:mozilla",
+ "about:networking",
+ "about:privatebrowsing",
+ "about:robots",
+ "about:serviceWorkers",
+ "about:sync-log",
+ "about:unloads",
+ "about:url-classifier",
+ "about:webrtc",
+ "about:welcome",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/" + DUMMY,
+ ];
+
+ for (let i = 0; i < secureChromePages.length; i++) {
+ await BrowserTestUtils.withNewTab("about:" + secureChromePages[i], () => {
+ is(getIdentityMode(), "chromeUI", "Identity should be chromeUI");
+ });
+ }
+
+ for (let i = 0; i < nonSecureExamplePages.length; i++) {
+ console.log(nonSecureExamplePages[i]);
+ await BrowserTestUtils.withNewTab(nonSecureExamplePages[i], () => {
+ ok(getIdentityMode() != "chromeUI", "Identity should not be chromeUI");
+ });
+ }
+});
+
+async function webpageTest(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let oldTab = await loadNewTab("about:robots");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage() {
+ await webpageTest(false);
+ await webpageTest(true);
+});
+
+async function webpageTestTextWarning(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_TEXT_PREF, secureCheck]] });
+ let oldTab = await loadNewTab("about:robots");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should have not secure text"
+ );
+ } else {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should have not secure text"
+ );
+ } else {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage_text_warning() {
+ await webpageTestTextWarning(false);
+ await webpageTestTextWarning(true);
+});
+
+async function webpageTestTextWarningCombined(secureCheck) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [INSECURE_TEXT_PREF, secureCheck],
+ [INSECURE_ICON_PREF, secureCheck],
+ ],
+ });
+ let oldTab = await loadNewTab("about:robots");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should be not secure"
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should be not secure"
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage_text_warning_combined() {
+ await webpageTestTextWarning(false);
+ await webpageTestTextWarning(true);
+});
+
+async function blankPageTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("about:blank");
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "pageproxystate should be invalid"
+ );
+
+ gBrowser.selectedTab = oldTab;
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "pageproxystate should be valid"
+ );
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "pageproxystate should be invalid"
+ );
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_blank() {
+ await blankPageTest(true);
+ await blankPageTest(false);
+});
+
+async function secureTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("https://example.com/" + DUMMY);
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_secure_enabled() {
+ await secureTest(true);
+ await secureTest(false);
+});
+
+async function viewSourceTest() {
+ let sourceTab = await loadNewTab("view-source:https://example.com/" + DUMMY);
+
+ gBrowser.selectedTab = sourceTab;
+ is(
+ getIdentityMode(),
+ "verifiedDomain",
+ "Identity should be verified while viewing source"
+ );
+
+ gBrowser.removeTab(sourceTab);
+}
+
+add_task(async function test_viewSource() {
+ await viewSourceTest();
+});
+
+async function insecureTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ is(
+ document.getElementById("identity-icon").getAttribute("tooltiptext"),
+ gNavigatorBundle.getString("identity.notSecure.tooltip"),
+ "The insecure lock icon has a correct tooltip text."
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ is(
+ document.getElementById("identity-icon").getAttribute("tooltiptext"),
+ gNavigatorBundle.getString("identity.notSecure.tooltip"),
+ "The insecure lock icon has a correct tooltip text."
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_insecure() {
+ await insecureTest(true);
+ await insecureTest(false);
+});
+
+async function addonsTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("about:addons");
+ is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_addons() {
+ await addonsTest(true);
+ await addonsTest(false);
+});
+
+async function fileTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let fileURI = getTestFilePath("");
+
+ let newTab = await loadNewTab(fileURI);
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_file() {
+ await fileTest(true);
+ await fileTest(false);
+});
+
+async function resourceUriTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let dataURI = "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+ let newTab = await loadNewTab(dataURI);
+
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(
+ getIdentityMode(),
+ "localResource",
+ "Identity should be a local a resource"
+ );
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_resource_uri() {
+ await resourceUriTest(true);
+ await resourceUriTest(false);
+});
+
+async function noCertErrorTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser, "https://nocert.example.com/");
+ await promise;
+ is(
+ getIdentityMode(),
+ "certErrorPage notSecureText",
+ "Identity should be the cert error page."
+ );
+ is(
+ getConnectionState(),
+ "cert-error-page",
+ "Connection should be the cert error page."
+ );
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(
+ getIdentityMode(),
+ "certErrorPage notSecureText",
+ "Identity should be the cert error page."
+ );
+ is(
+ getConnectionState(),
+ "cert-error-page",
+ "Connection should be the cert error page."
+ );
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_net_error_uri() {
+ await noCertErrorTest(true);
+ await noCertErrorTest(false);
+});
+
+add_task(async function httpsOnlyErrorTest() {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(gBrowser, "http://nocert.example.com/");
+ await promise;
+ is(
+ getIdentityMode(),
+ "httpsOnlyErrorPage",
+ "Identity should be the https-only mode error page."
+ );
+ is(
+ getConnectionState(),
+ "https-only-error-page",
+ "Connection should be the https-only mode error page."
+ );
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(
+ getIdentityMode(),
+ "httpsOnlyErrorPage",
+ "Identity should be the https-only mode error page."
+ );
+ is(
+ getConnectionState(),
+ "https-only-error-page",
+ "Connection should be the https-only mode page."
+ );
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+async function noCertErrorFromNavigationTest(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.document.getElementById("no-cert").click();
+ });
+ await promise;
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ is(
+ content.window.location.href,
+ "https://nocert.example.com/",
+ "Should be the cert error URL"
+ );
+ });
+
+ is(
+ newTab.linkedBrowser.documentURI.spec.startsWith("about:certerror?"),
+ true,
+ "Should be an about:certerror"
+ );
+ is(
+ getIdentityMode(),
+ "certErrorPage notSecureText",
+ "Identity should be the cert error page."
+ );
+ is(
+ getConnectionState(),
+ "cert-error-page",
+ "Connection should be the cert error page."
+ );
+
+ gBrowser.removeTab(newTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_net_error_uri_from_navigation_tab() {
+ await noCertErrorFromNavigationTest(true);
+ await noCertErrorFromNavigationTest(false);
+});
+
+add_task(async function tlsErrorPageTest() {
+ const TLS10_PAGE = "https://tls1.example.com/";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.tls.version.min", 3],
+ ["security.tls.version.max", 4],
+ ],
+ });
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TLS10_PAGE);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ await pageLoaded;
+
+ await SpecialPowers.spawn(browser, [], function () {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+ });
+
+ is(
+ getConnectionState(),
+ "cert-error-page",
+ "Connection state should be the cert error page."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function netErrorPageTest() {
+ // Connect to a server that rejects all requests, to test network error pages:
+ let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+ let server = new HttpServer();
+ server.registerPrefixHandler("/", (req, res) =>
+ res.abort(new Error("Noooope."))
+ );
+ server.start(-1);
+ let port = server.identity.primaryPort;
+ const ERROR_PAGE = `http://localhost:${port}/`;
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, ERROR_PAGE);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ await pageLoaded;
+
+ await SpecialPowers.spawn(browser, [], function () {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+ });
+
+ is(
+ getConnectionState(),
+ "net-error-page",
+ "Connection should be the net error page."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+async function aboutBlockedTest(secureCheck) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let url = "http://www.itisatrap.org/firefox/its-an-attack.html";
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [INSECURE_ICON_PREF, secureCheck],
+ ["urlclassifier.blockedTable", "moztest-block-simple"],
+ ],
+ });
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url,
+ true
+ );
+
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown.");
+ is(getConnectionState(), "not-secure", "Connection should be not secure.");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown.");
+ is(getConnectionState(), "not-secure", "Connection should be not secure.");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_blocked() {
+ await aboutBlockedTest(true);
+ await aboutBlockedTest(false);
+});
+
+add_task(async function noCertErrorSecurityConnectionBGTest() {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = tab;
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser, "https://nocert.example.com/");
+ await promise;
+
+ is(
+ getSecurityConnectionBG(),
+ `url("chrome://global/skin/icons/security-warning.svg")`,
+ "Security connection should show a warning lock icon."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+async function aboutUriTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let aboutURI = "about:robots";
+
+ let newTab = await loadNewTab(aboutURI);
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_uri() {
+ await aboutUriTest(true);
+ await aboutUriTest(false);
+});
+
+async function readerUriTest(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("about:reader?url=http://example.com");
+ gBrowser.selectedTab = newTab;
+ let readerURL = await getReaderModeURL();
+ is(
+ readerURL,
+ "Site information for example.com",
+ "should be the correct URI in reader mode"
+ );
+
+ gBrowser.removeTab(newTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_reader_uri() {
+ await readerUriTest(true);
+ await readerUriTest(false);
+});
+
+async function dataUriTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let dataURI = "data:text/html,hi";
+
+ let newTab = await loadNewTab(dataURI);
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_data_uri() {
+ await dataUriTest(true);
+ await dataUriTest(false);
+});
+
+async function pbModeTest(prefs, secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let oldTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "about:robots"
+ );
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/" + DUMMY
+ );
+
+ if (secureCheck) {
+ is(
+ getIdentityMode(privateWin),
+ "notSecure",
+ "Identity should be not secure"
+ );
+ } else {
+ is(
+ getIdentityMode(privateWin),
+ "unknownIdentity",
+ "Identity should be unknown"
+ );
+ }
+
+ privateWin.gBrowser.selectedTab = oldTab;
+ is(
+ getIdentityMode(privateWin),
+ "localResource",
+ "Identity should be localResource"
+ );
+
+ privateWin.gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(
+ getIdentityMode(privateWin),
+ "notSecure",
+ "Identity should be not secure"
+ );
+ } else {
+ is(
+ getIdentityMode(privateWin),
+ "unknownIdentity",
+ "Identity should be unknown"
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_pb_mode() {
+ let prefs = [
+ [INSECURE_ICON_PREF, true],
+ [INSECURE_PBMODE_ICON_PREF, true],
+ [HTTPS_FIRST_PBM_PREF, false],
+ ];
+ await pbModeTest(prefs, true);
+ prefs = [
+ [INSECURE_ICON_PREF, false],
+ [INSECURE_PBMODE_ICON_PREF, true],
+ [HTTPS_FIRST_PBM_PREF, false],
+ ];
+ await pbModeTest(prefs, true);
+ prefs = [
+ [INSECURE_ICON_PREF, false],
+ [INSECURE_PBMODE_ICON_PREF, false],
+ [HTTPS_FIRST_PBM_PREF, false],
+ ];
+ await pbModeTest(prefs, false);
+});
diff --git a/browser/base/content/test/siteIdentity/browser_check_identity_state_pdf.js b/browser/base/content/test/siteIdentity/browser_check_identity_state_pdf.js
new file mode 100644
index 0000000000..8180238e84
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_check_identity_state_pdf.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that sites opened via the PDF viewer have the correct identity state.
+ */
+
+"use strict";
+
+function testIdentityMode(uri, expectedState, message) {
+ return BrowserTestUtils.withNewTab(uri, () => {
+ is(getIdentityMode(), expectedState, message);
+ });
+}
+
+/**
+ * Test site identity state for PDFs served via file URI.
+ */
+add_task(async function test_pdf_fileURI() {
+ let path = getTestFilePath("./file_pdf.pdf");
+ info("path:" + path);
+
+ await testIdentityMode(
+ path,
+ "localResource",
+ "Identity should be localResource for a PDF served via file URI"
+ );
+});
+
+/**
+ * Test site identity state for PDFs served via blob URI.
+ */
+add_task(async function test_pdf_blobURI() {
+ let uri =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_pdf_blob.html";
+
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("a", {}, browser);
+ await newTabOpened;
+
+ is(
+ getIdentityMode(),
+ "localResource",
+ "Identity should be localResource for a PDF served via blob URI"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+/**
+ * Test site identity state for PDFs served via HTTP.
+ */
+add_task(async function test_pdf_http() {
+ const PDF_URI_NOSCHEME =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "example.com"
+ ) + "file_pdf.pdf";
+
+ await testIdentityMode(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://" + PDF_URI_NOSCHEME,
+ "notSecure",
+ "Identity should be notSecure for a PDF served via HTTP."
+ );
+ await testIdentityMode(
+ "https://" + PDF_URI_NOSCHEME,
+ "verifiedDomain",
+ "Identity should be verifiedDomain for a PDF served via HTTPS."
+ );
+});
diff --git a/browser/base/content/test/siteIdentity/browser_csp_block_all_mixedcontent.js b/browser/base/content/test/siteIdentity/browser_csp_block_all_mixedcontent.js
new file mode 100644
index 0000000000..693c9418de
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_csp_block_all_mixedcontent.js
@@ -0,0 +1,60 @@
+/*
+ * Description of the Test:
+ * We load an https page which uses a CSP including block-all-mixed-content.
+ * The page tries to load a script over http. We make sure the UI is not
+ * influenced when blocking the mixed content. In particular the page
+ * should still appear fully encrypted with a green lock.
+ */
+
+const PRE_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+var gTestBrowser = null;
+
+// ------------------------------------------------------
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+ finish();
+}
+
+// ------------------------------------------------------
+async function verifyUInotDegraded() {
+ // make sure that not mixed content is loaded and also not blocked
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+ // clean up and finish test
+ cleanUpAfterTests();
+}
+
+// ------------------------------------------------------
+function runTests() {
+ var newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop();
+
+ // Starting the test
+ var url = PRE_PATH + "file_csp_block_all_mixedcontent.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ verifyUInotDegraded
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+// ------------------------------------------------------
+function test() {
+ // Performing async calls, e.g. 'onload', we have to wait till all of them finished
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ { set: [["security.mixed_content.block_active_content", true]] },
+ function () {
+ runTests();
+ }
+ );
+}
diff --git a/browser/base/content/test/siteIdentity/browser_deprecatedTLSVersions.js b/browser/base/content/test/siteIdentity/browser_deprecatedTLSVersions.js
new file mode 100644
index 0000000000..22fa33f3c2
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_deprecatedTLSVersions.js
@@ -0,0 +1,94 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Tests for Bug 1535210 - Set SSL STATE_IS_BROKEN flag for TLS1.0 and TLS 1.1 connections
+ */
+
+const HTTPS_TLS1_0 = "https://tls1.example.com";
+const HTTPS_TLS1_1 = "https://tls11.example.com";
+const HTTPS_TLS1_2 = "https://tls12.example.com";
+const HTTPS_TLS1_3 = "https://tls13.example.com";
+
+function getIdentityMode(aWindow = window) {
+ return aWindow.document.getElementById("identity-box").className;
+}
+
+function closeIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ gIdentityHandler._identityPopup.hidePopup();
+ return promise;
+}
+
+async function checkConnectionState(state) {
+ await openIdentityPopup();
+ is(getConnectionState(), state, "connectionState should be " + state);
+ await closeIdentityPopup();
+}
+
+function getConnectionState() {
+ return document.getElementById("identity-popup").getAttribute("connection");
+}
+
+registerCleanupFunction(function () {
+ // Set preferences back to their original values
+ Services.prefs.clearUserPref("security.tls.version.min");
+ Services.prefs.clearUserPref("security.tls.version.max");
+});
+
+add_task(async function () {
+ // Run with all versions enabled for this test.
+ Services.prefs.setIntPref("security.tls.version.min", 1);
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ // Try deprecated versions
+ BrowserTestUtils.loadURIString(browser, HTTPS_TLS1_0);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "broken");
+ is(
+ getIdentityMode(),
+ "unknownIdentity weakCipher",
+ "Identity should be unknownIdentity"
+ );
+ await checkConnectionState("not-secure");
+
+ BrowserTestUtils.loadURIString(browser, HTTPS_TLS1_1);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "broken");
+ is(
+ getIdentityMode(),
+ "unknownIdentity weakCipher",
+ "Identity should be unknownIdentity"
+ );
+ await checkConnectionState("not-secure");
+
+ // Transition to secure
+ BrowserTestUtils.loadURIString(browser, HTTPS_TLS1_2);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "secure");
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+ await checkConnectionState("secure");
+
+ // Transition back to broken
+ BrowserTestUtils.loadURIString(browser, HTTPS_TLS1_1);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "broken");
+ is(
+ getIdentityMode(),
+ "unknownIdentity weakCipher",
+ "Identity should be unknownIdentity"
+ );
+ await checkConnectionState("not-secure");
+
+ // TLS1.3 for completeness
+ BrowserTestUtils.loadURIString(browser, HTTPS_TLS1_3);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "secure");
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+ await checkConnectionState("secure");
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_geolocation_indicator.js b/browser/base/content/test/siteIdentity/browser_geolocation_indicator.js
new file mode 100644
index 0000000000..078b7ab975
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_geolocation_indicator.js
@@ -0,0 +1,381 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const { PermissionUI } = ChromeUtils.importESModule(
+ "resource:///modules/PermissionUI.sys.mjs"
+);
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const CP = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+);
+
+const EXAMPLE_PAGE_URL = "https://example.com";
+const EXAMPLE_PAGE_URI = Services.io.newURI(EXAMPLE_PAGE_URL);
+const EXAMPLE_PAGE_PRINCIPAL =
+ Services.scriptSecurityManager.createContentPrincipal(EXAMPLE_PAGE_URI, {});
+const GEO_CONTENT_PREF_KEY = "permissions.geoLocation.lastAccess";
+const POLL_INTERVAL_FALSE_STATE = 50;
+
+async function testGeoSharingIconVisible(state = true) {
+ let sharingIcon = document.getElementById("geo-sharing-icon");
+ ok(sharingIcon, "Geo sharing icon exists");
+
+ try {
+ await TestUtils.waitForCondition(
+ () => sharingIcon.hasAttribute("sharing") === true,
+ "Waiting for geo sharing icon visibility state",
+ // If we wait for sharing icon to *not* show, waitForCondition will always timeout on correct state.
+ // In these cases we want to reduce the wait time from 5 seconds to 2.5 seconds to prevent test duration timeouts
+ !state ? POLL_INTERVAL_FALSE_STATE : undefined
+ );
+ } catch (e) {
+ ok(!state, "Geo sharing icon not showing");
+ return;
+ }
+ ok(state, "Geo sharing icon showing");
+}
+
+async function checkForDOMElement(state, id) {
+ info(`Testing state ${state} of element ${id}`);
+ let el;
+ try {
+ await TestUtils.waitForCondition(
+ () => {
+ el = document.getElementById(id);
+ return el != null;
+ },
+ `Waiting for ${id}`,
+ !state ? POLL_INTERVAL_FALSE_STATE : undefined
+ );
+ } catch (e) {
+ ok(!state, `${id} has correct state`);
+ return el;
+ }
+ ok(state, `${id} has correct state`);
+
+ return el;
+}
+
+async function testPermissionPopupGeoContainer(
+ containerVisible,
+ timestampVisible
+) {
+ // The container holds the timestamp element, therefore we can't have a
+ // visible timestamp without the container.
+ if (timestampVisible && !containerVisible) {
+ ok(false, "Can't have timestamp without container");
+ }
+
+ // Only call openPermissionPopup if popup is closed, otherwise it does not resolve
+ if (!gPermissionPanel._identityPermissionBox.hasAttribute("open")) {
+ await openPermissionPopup();
+ }
+
+ let checkContainer = checkForDOMElement(
+ containerVisible,
+ "permission-popup-geo-container"
+ );
+
+ if (containerVisible && timestampVisible) {
+ // Wait for the geo container to be fully populated.
+ // The time label is computed async.
+ let container = await checkContainer;
+ await TestUtils.waitForCondition(
+ () => container.childElementCount == 2,
+ "permission-popup-geo-container should have two elements."
+ );
+ is(
+ container.childNodes[0].classList[0],
+ "permission-popup-permission-item",
+ "Geo container should have permission item."
+ );
+ is(
+ container.childNodes[1].id,
+ "geo-access-indicator-item",
+ "Geo container should have indicator item."
+ );
+ }
+ let checkAccessIndicator = checkForDOMElement(
+ timestampVisible,
+ "geo-access-indicator-item"
+ );
+
+ return Promise.all([checkContainer, checkAccessIndicator]);
+}
+
+function openExamplePage(tabbrowser = gBrowser) {
+ return BrowserTestUtils.openNewForegroundTab(tabbrowser, EXAMPLE_PAGE_URL);
+}
+
+function requestGeoLocation(browser) {
+ return SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve => {
+ content.navigator.geolocation.getCurrentPosition(
+ () => resolve(true),
+ error => resolve(error.code !== 1) // PERMISSION_DENIED = 1
+ );
+ });
+ });
+}
+
+function answerGeoLocationPopup(allow, remember = false) {
+ let notification = PopupNotifications.getNotification("geolocation");
+ ok(
+ PopupNotifications.isPanelOpen && notification,
+ "Geolocation notification is open"
+ );
+
+ let rememberCheck = PopupNotifications.panel.querySelector(
+ ".popup-notification-checkbox"
+ );
+ rememberCheck.checked = remember;
+
+ let popupHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ if (allow) {
+ let allowBtn = PopupNotifications.panel.querySelector(
+ ".popup-notification-primary-button"
+ );
+ allowBtn.click();
+ } else {
+ let denyBtn = PopupNotifications.panel.querySelector(
+ ".popup-notification-secondary-button"
+ );
+ denyBtn.click();
+ }
+ return popupHidden;
+}
+
+function setGeoLastAccess(browser, state) {
+ return new Promise(resolve => {
+ let host = browser.currentURI.host;
+ let handler = {
+ handleCompletion: () => resolve(),
+ };
+
+ if (!state) {
+ CP.removeByDomainAndName(
+ host,
+ GEO_CONTENT_PREF_KEY,
+ browser.loadContext,
+ handler
+ );
+ return;
+ }
+ CP.set(
+ host,
+ GEO_CONTENT_PREF_KEY,
+ new Date().toString(),
+ browser.loadContext,
+ handler
+ );
+ });
+}
+
+async function testGeoLocationLastAccessSet(browser) {
+ let timestamp = await new Promise(resolve => {
+ let lastAccess = null;
+ CP.getByDomainAndName(
+ gBrowser.currentURI.spec,
+ GEO_CONTENT_PREF_KEY,
+ browser.loadContext,
+ {
+ handleResult(pref) {
+ lastAccess = pref.value;
+ },
+ handleCompletion() {
+ resolve(lastAccess);
+ },
+ }
+ );
+ });
+
+ ok(timestamp != null, "Geo last access timestamp set");
+
+ let parseSuccess = true;
+ try {
+ timestamp = new Date(timestamp);
+ } catch (e) {
+ parseSuccess = false;
+ }
+ ok(
+ parseSuccess && !isNaN(timestamp),
+ "Geo last access timestamp is valid Date"
+ );
+}
+
+async function cleanup(tab) {
+ await setGeoLastAccess(tab.linkedBrowser, false);
+ SitePermissions.removeFromPrincipal(
+ tab.linkedBrowser.contentPrincipal,
+ "geo",
+ tab.linkedBrowser
+ );
+ gBrowser.resetBrowserSharing(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testIndicatorGeoSharingState(active) {
+ let tab = await openExamplePage();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: active });
+ await testGeoSharingIconVisible(active);
+
+ await cleanup(tab);
+}
+
+async function testIndicatorExplicitAllow(persistent) {
+ let tab = await openExamplePage();
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ info("Requesting geolocation");
+ let request = requestGeoLocation(tab.linkedBrowser);
+ await popupShown;
+ info("Allowing geolocation via popup");
+ answerGeoLocationPopup(true, persistent);
+ await request;
+
+ await Promise.all([
+ testGeoSharingIconVisible(true),
+ testPermissionPopupGeoContainer(true, true),
+ testGeoLocationLastAccessSet(tab.linkedBrowser),
+ ]);
+
+ await cleanup(tab);
+}
+
+// Indicator and permission popup entry shown after explicit PermissionUI geolocation allow
+add_task(function test_indicator_and_timestamp_after_explicit_allow() {
+ return testIndicatorExplicitAllow(false);
+});
+add_task(function test_indicator_and_timestamp_after_explicit_allow_remember() {
+ return testIndicatorExplicitAllow(true);
+});
+
+// Indicator and permission popup entry shown after auto PermissionUI geolocation allow
+add_task(async function test_indicator_and_timestamp_after_implicit_allow() {
+ PermissionTestUtils.add(
+ EXAMPLE_PAGE_URI,
+ "geo",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+ let tab = await openExamplePage();
+ let result = await requestGeoLocation(tab.linkedBrowser);
+ ok(result, "Request should be allowed");
+
+ await Promise.all([
+ testGeoSharingIconVisible(true),
+ testPermissionPopupGeoContainer(true, true),
+ testGeoLocationLastAccessSet(tab.linkedBrowser),
+ ]);
+
+ await cleanup(tab);
+});
+
+// Indicator shown when manually setting sharing state to true
+add_task(function test_indicator_sharing_state_active() {
+ return testIndicatorGeoSharingState(true);
+});
+
+// Indicator not shown when manually setting sharing state to false
+add_task(function test_indicator_sharing_state_inactive() {
+ return testIndicatorGeoSharingState(false);
+});
+
+// Permission popup shows permission if geo permission is set to persistent allow
+add_task(async function test_permission_popup_permission_scope_permanent() {
+ PermissionTestUtils.add(
+ EXAMPLE_PAGE_URI,
+ "geo",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+ let tab = await openExamplePage();
+
+ await testPermissionPopupGeoContainer(true, false); // Expect permission to be visible, but not lastAccess indicator
+
+ await cleanup(tab);
+});
+
+// Sharing state set, but no permission
+add_task(async function test_permission_popup_permission_sharing_state() {
+ let tab = await openExamplePage();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ await testPermissionPopupGeoContainer(true, false);
+
+ await cleanup(tab);
+});
+
+// Permission popup has correct state if sharing state and last geo access timestamp are set
+add_task(
+ async function test_permission_popup_permission_sharing_state_timestamp() {
+ let tab = await openExamplePage();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ await setGeoLastAccess(tab.linkedBrowser, true);
+
+ await testPermissionPopupGeoContainer(true, true);
+
+ await cleanup(tab);
+ }
+);
+
+// Clicking permission clear button clears permission and resets geo sharing state
+add_task(async function test_permission_popup_permission_clear() {
+ PermissionTestUtils.add(
+ EXAMPLE_PAGE_URI,
+ "geo",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+ let tab = await openExamplePage();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+
+ await openPermissionPopup();
+
+ let clearButton = document.querySelector(
+ "#permission-popup-geo-container button"
+ );
+ ok(clearButton, "Clear button is visible");
+ clearButton.click();
+
+ await Promise.all([
+ testGeoSharingIconVisible(false),
+ testPermissionPopupGeoContainer(false, false),
+ TestUtils.waitForCondition(() => {
+ let sharingState = tab._sharingState;
+ return (
+ sharingState == null ||
+ sharingState.geo == null ||
+ sharingState.geo === false
+ );
+ }, "Waiting for geo sharing state to reset"),
+ ]);
+ await cleanup(tab);
+});
+
+/**
+ * Tests that we only show the last access label once when the sharing
+ * state is updated multiple times while the popup is open.
+ */
+add_task(async function test_permission_no_duplicate_last_access_label() {
+ let tab = await openExamplePage();
+ await setGeoLastAccess(tab.linkedBrowser, true);
+ await openPermissionPopup();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ await testPermissionPopupGeoContainer(true, true);
+ await cleanup(tab);
+});
diff --git a/browser/base/content/test/siteIdentity/browser_getSecurityInfo.js b/browser/base/content/test/siteIdentity/browser_getSecurityInfo.js
new file mode 100644
index 0000000000..b3b086f39d
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_getSecurityInfo.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
+const MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT = MOZILLA_PKIX_ERROR_BASE + 14;
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.waitForErrorPage(browser);
+ BrowserTestUtils.loadURIString(browser, "https://self-signed.example.com");
+ await loaded;
+ let securityInfo = gBrowser.securityUI.secInfo;
+ ok(!securityInfo, "Found no security info");
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(browser, "http://example.com");
+ await loaded;
+ securityInfo = gBrowser.securityUI.secInfo;
+ ok(!securityInfo, "Found no security info");
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURIString(browser, "https://example.com");
+ await loaded;
+ securityInfo = gBrowser.securityUI.secInfo;
+ ok(securityInfo, "Found some security info");
+ ok(securityInfo.succeededCertChain, "Has a succeeded cert chain");
+ is(securityInfo.errorCode, 0, "Has no error code");
+ is(
+ securityInfo.serverCert.commonName,
+ "example.com",
+ "Has the correct certificate"
+ );
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js b/browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js
new file mode 100644
index 0000000000..de2a137100
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests that the identity icons don't flicker when navigating,
+ * i.e. the box should show no intermediate identity state. */
+
+add_task(async function test() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots",
+ true
+ );
+ let identityBox = document.getElementById("identity-box");
+
+ is(
+ identityBox.className,
+ "localResource",
+ "identity box has the correct class"
+ );
+
+ let observerOptions = {
+ attributes: true,
+ attributeFilter: ["class"],
+ };
+ let classChanges = 0;
+
+ let observer = new MutationObserver(function (mutations) {
+ for (let mutation of mutations) {
+ is(mutation.type, "attributes");
+ is(mutation.attributeName, "class");
+ classChanges++;
+ is(
+ identityBox.className,
+ "verifiedDomain",
+ "identity box class changed correctly"
+ );
+ }
+ });
+ observer.observe(identityBox, observerOptions);
+
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "https://example.com");
+ await loaded;
+
+ is(classChanges, 1, "Changed the className once");
+ observer.disconnect();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js b/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js
new file mode 100644
index 0000000000..858cd3d632
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js
@@ -0,0 +1,126 @@
+/* Tests that the identity block can be reached via keyboard
+ * shortcuts and that it has the correct tab order.
+ */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const PERMISSIONS_PAGE = TEST_PATH + "permissions.html";
+
+// The DevEdition has the DevTools button in the toolbar by default. Remove it
+// to prevent branch-specific rules what button should be focused.
+CustomizableUI.removeWidgetFromArea("developer-button");
+
+registerCleanupFunction(async function resetToolbar() {
+ await CustomizableUI.reset();
+});
+
+add_task(async function setupHomeButton() {
+ // Put the home button in the pre-proton placement to test focus states.
+ CustomizableUI.addWidgetToArea(
+ "home-button",
+ "nav-bar",
+ CustomizableUI.getPlacementOfWidget("stop-reload-button").position + 1
+ );
+});
+
+function synthesizeKeyAndWaitForFocus(element, keyCode, options) {
+ let focused = BrowserTestUtils.waitForEvent(element, "focus");
+ EventUtils.synthesizeKey(keyCode, options);
+ return focused;
+}
+
+// Checks that the tracking protection icon container is the next element after
+// the urlbar to be focused if there are no active notification anchors.
+add_task(async function testWithoutNotifications() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ await synthesizeKeyAndWaitForFocus(gURLBar, "l", { accelKey: true });
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+ await synthesizeKeyAndWaitForFocus(
+ gProtectionsHandler._trackingProtectionIconContainer,
+ "VK_TAB",
+ { shiftKey: true }
+ );
+ is(
+ document.activeElement,
+ gProtectionsHandler._trackingProtectionIconContainer,
+ "tracking protection icon container should be focused"
+ );
+ });
+});
+
+// Checks that when there is a notification anchor, it will receive
+// focus before the identity block.
+add_task(async function testWithNotifications() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) {
+ let popupshown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ // Request a permission;
+ BrowserTestUtils.synthesizeMouseAtCenter("#geo", {}, browser);
+ await popupshown;
+
+ await synthesizeKeyAndWaitForFocus(gURLBar, "l", { accelKey: true });
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+ await synthesizeKeyAndWaitForFocus(
+ gProtectionsHandler._trackingProtectionIconContainer,
+ "VK_TAB",
+ { shiftKey: true }
+ );
+ is(
+ document.activeElement,
+ gProtectionsHandler._trackingProtectionIconContainer,
+ "tracking protection icon container should be focused"
+ );
+ await synthesizeKeyAndWaitForFocus(
+ gIdentityHandler._identityIconBox,
+ "ArrowRight"
+ );
+ is(
+ document.activeElement,
+ gIdentityHandler._identityIconBox,
+ "identity block should be focused"
+ );
+ let geoIcon = document.getElementById("geo-notification-icon");
+ await synthesizeKeyAndWaitForFocus(geoIcon, "ArrowRight");
+ is(
+ document.activeElement,
+ geoIcon,
+ "notification anchor should be focused"
+ );
+ });
+});
+
+// Checks that with invalid pageproxystate the identity block is ignored.
+add_task(async function testInvalidPageProxyState() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ // Loading about:blank will automatically focus the urlbar, which, however, can
+ // race with the test code. So we only send the shortcut if the urlbar isn't focused yet.
+ if (document.activeElement != gURLBar.inputField) {
+ await synthesizeKeyAndWaitForFocus(gURLBar, "l", { accelKey: true });
+ }
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+ await synthesizeKeyAndWaitForFocus(
+ document.getElementById("home-button"),
+ "VK_TAB",
+ { shiftKey: true }
+ );
+ await synthesizeKeyAndWaitForFocus(
+ document.getElementById("tabs-newtab-button"),
+ "VK_TAB",
+ { shiftKey: true }
+ );
+ isnot(
+ document.activeElement,
+ gProtectionsHandler._trackingProtectionIconContainer,
+ "tracking protection icon container should not be focused"
+ );
+ // Restore focus to the url bar.
+ gURLBar.focus();
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityIcon_img_url.js b/browser/base/content/test/siteIdentity/browser_identityIcon_img_url.js
new file mode 100644
index 0000000000..9ef8b6dfed
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityIcon_img_url.js
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/**
+ * Test Bug 1562881 - Ensuring the identity icon loads correct img in different
+ * circumstances.
+ */
+
+const kBaseURI = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const kBaseURILocalhost = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://127.0.0.1"
+);
+
+const TEST_CASES = [
+ {
+ type: "http",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ testURL: "http://example.com",
+ img_url: `url("chrome://global/skin/icons/security-broken.svg")`,
+ },
+ {
+ type: "https",
+ testURL: "https://example.com",
+ img_url: `url("chrome://global/skin/icons/security.svg")`,
+ },
+ {
+ type: "non-chrome about page",
+ testURL: "about:about",
+ img_url: `url("chrome://global/skin/icons/page-portrait.svg")`,
+ },
+ {
+ type: "chrome about page",
+ testURL: "about:preferences",
+ img_url: `url("chrome://branding/content/icon${
+ window.devicePixelRatio > 1 ? 32 : 16
+ }.png")`,
+ },
+ {
+ type: "file",
+ testURL: "dummy_page.html",
+ img_url: `url("chrome://global/skin/icons/page-portrait.svg")`,
+ },
+ {
+ type: "resource",
+ testURL: "resource://gre/modules/Log.sys.mjs",
+ img_url: `url("chrome://global/skin/icons/page-portrait.svg")`,
+ },
+ {
+ type: "mixedPassiveContent",
+ testURL: kBaseURI + "file_mixedPassiveContent.html",
+ img_url: `url("chrome://global/skin/icons/security-warning.svg")`,
+ },
+ {
+ type: "mixedActiveContent",
+ testURL: kBaseURI + "file_csp_block_all_mixedcontent.html",
+ img_url: `url("chrome://global/skin/icons/security.svg")`,
+ },
+ {
+ type: "certificateError",
+ testURL: "https://self-signed.example.com",
+ img_url: `url("chrome://global/skin/icons/security-warning.svg")`,
+ },
+ {
+ type: "localhost",
+ testURL: "http://127.0.0.1",
+ img_url: `url("chrome://global/skin/icons/page-portrait.svg")`,
+ },
+ {
+ type: "localhost + http frame",
+ testURL: kBaseURILocalhost + "file_csp_block_all_mixedcontent.html",
+ img_url: `url("chrome://global/skin/icons/page-portrait.svg")`,
+ },
+ {
+ type: "data URI",
+ testURL: "data:text/html,<div>",
+ img_url: `url("chrome://global/skin/icons/security-broken.svg")`,
+ },
+ {
+ type: "view-source HTTP",
+ testURL: "view-source:http://example.com/",
+ img_url: `url("chrome://global/skin/icons/security-broken.svg")`,
+ },
+ {
+ type: "view-source HTTPS",
+ testURL: "view-source:https://example.com/",
+ img_url: `url("chrome://global/skin/icons/security.svg")`,
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // By default, proxies don't apply to 127.0.0.1. We need them to for this test, though:
+ ["network.proxy.allow_hijacking_localhost", true],
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+
+ for (let testData of TEST_CASES) {
+ info(`Testing for ${testData.type}`);
+ // Open the page for testing.
+ let testURL = testData.testURL;
+
+ // Overwrite the url if it is testing the file url.
+ if (testData.type === "file") {
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append(testURL);
+ dir.normalize();
+ testURL = Services.io.newFileURI(dir).spec;
+ }
+
+ let pageLoaded;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, testURL);
+ let browser = gBrowser.selectedBrowser;
+ if (testData.type === "certificateError") {
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ } else {
+ pageLoaded = BrowserTestUtils.browserLoaded(browser);
+ }
+ },
+ false
+ );
+ await pageLoaded;
+
+ let identityIcon = document.getElementById("identity-icon");
+
+ // Get the image url from the identity icon.
+ let identityIconImageURL = gBrowser.ownerGlobal
+ .getComputedStyle(identityIcon)
+ .getPropertyValue("list-style-image");
+
+ is(
+ identityIconImageURL,
+ testData.img_url,
+ "The identity icon has a correct image url."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_HttpsOnlyMode.js b/browser/base/content/test/siteIdentity/browser_identityPopup_HttpsOnlyMode.js
new file mode 100644
index 0000000000..a83f38e1f6
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_HttpsOnlyMode.js
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 HTTPS_ONLY_PERMISSION = "https-only-load-insecure";
+const WEBSITE = scheme => `${scheme}://example.com`;
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ // Site is already HTTPS, so the UI should not be visible.
+ await runTest({
+ name: "No HTTPS-Only UI",
+ initialScheme: "https",
+ initialPermission: 0,
+ permissionScheme: "https",
+ isUiVisible: false,
+ });
+
+ // Site gets upgraded to HTTPS, so the UI should be visible.
+ // Disabling HTTPS-Only Mode through the menulist should reload the page and
+ // set the permission accordingly.
+ await runTest({
+ name: "Disable HTTPS-Only",
+ initialScheme: "http",
+ initialPermission: 0,
+ permissionScheme: "https",
+ isUiVisible: true,
+ selectPermission: 1,
+ expectReload: true,
+ finalScheme: "https",
+ });
+
+ // HTTPS-Only Mode is disabled for this site, so the UI should be visible.
+ // Disabling HTTPS-Only Mode through the menulist should not reload the page
+ // but set the permission accordingly.
+ await runTest({
+ name: "Switch between off states",
+ initialScheme: "http",
+ initialPermission: 1,
+ permissionScheme: "http",
+ isUiVisible: true,
+ selectPermission: 2,
+ expectReload: false,
+ finalScheme: "http",
+ });
+
+ // HTTPS-Only Mode is disabled for this site, so the UI should be visible.
+ // Enabling HTTPS-Only Mode through the menulist should reload and upgrade the
+ // page and set the permission accordingly.
+ await runTest({
+ name: "Enable HTTPS-Only again",
+ initialScheme: "http",
+ initialPermission: 2,
+ permissionScheme: "http",
+ isUiVisible: true,
+ selectPermission: 0,
+ expectReload: true,
+ finalScheme: "https",
+ });
+});
+
+async function runTest(options) {
+ // Set the initial permission
+ setPermission(WEBSITE(options.permissionScheme), options.initialPermission);
+
+ await BrowserTestUtils.withNewTab(
+ WEBSITE(options.initialScheme),
+ async function (browser) {
+ const name = options.name + " | ";
+
+ // Check if the site has the expected scheme
+ is(
+ browser.currentURI.scheme,
+ options.permissionScheme,
+ name + "Expected scheme should match actual scheme"
+ );
+
+ // Open the identity popup.
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+
+ // Check if the HTTPS-Only UI is visible
+ const httpsOnlyUI = document.getElementById(
+ "identity-popup-security-httpsonlymode"
+ );
+ is(
+ gBrowser.ownerGlobal.getComputedStyle(httpsOnlyUI).display != "none",
+ options.isUiVisible,
+ options.isUiVisible
+ ? name + "HTTPS-Only UI should be visible."
+ : name + "HTTPS-Only UI shouldn't be visible."
+ );
+
+ // If it's not visible we can't do much else :)
+ if (!options.isUiVisible) {
+ return;
+ }
+
+ // Check if the value of the menulist matches the initial permission
+ const httpsOnlyMenulist = document.getElementById(
+ "identity-popup-security-httpsonlymode-menulist"
+ );
+ is(
+ parseInt(httpsOnlyMenulist.value, 10),
+ options.initialPermission,
+ name + "Menulist value should match expected permission value."
+ );
+
+ // Select another HTTPS-Only state and potentially wait for the page to reload
+ if (options.expectReload) {
+ const loaded = BrowserTestUtils.browserLoaded(browser);
+ httpsOnlyMenulist.getItemAtIndex(options.selectPermission).doCommand();
+ await loaded;
+ } else {
+ httpsOnlyMenulist.getItemAtIndex(options.selectPermission).doCommand();
+ }
+
+ // Check if the site has the expected scheme
+ is(
+ browser.currentURI.scheme,
+ options.finalScheme,
+ name + "Unexpected scheme after page reloaded."
+ );
+
+ // Check if the permission was sucessfully changed
+ is(
+ getPermission(WEBSITE(options.permissionScheme)),
+ options.selectPermission,
+ name + "Set permission should match the one selected from the menulist."
+ );
+ }
+ );
+
+ // Reset permission
+ Services.perms.removeAll();
+}
+
+function setPermission(url, newValue) {
+ let uri = Services.io.newURI(url);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ if (newValue === 0) {
+ Services.perms.removeFromPrincipal(principal, HTTPS_ONLY_PERMISSION);
+ } else if (newValue === 1) {
+ Services.perms.addFromPrincipal(
+ principal,
+ HTTPS_ONLY_PERMISSION,
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW,
+ Ci.nsIPermissionManager.EXPIRE_NEVER
+ );
+ } else {
+ Services.perms.addFromPrincipal(
+ principal,
+ HTTPS_ONLY_PERMISSION,
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
+ Ci.nsIPermissionManager.EXPIRE_SESSION
+ );
+ }
+}
+
+function getPermission(url) {
+ let uri = Services.io.newURI(url);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ const state = Services.perms.testPermissionFromPrincipal(
+ principal,
+ HTTPS_ONLY_PERMISSION
+ );
+ switch (state) {
+ case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION:
+ return 2; // Off temporarily
+ case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW:
+ return 1; // Off
+ default:
+ return 0; // On
+ }
+}
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js
new file mode 100644
index 0000000000..efc4f34310
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_ORIGIN = "https://example.com";
+const TEST_SUB_ORIGIN = "https://test1.example.com";
+const TEST_ORIGIN_2 = "https://example.net";
+const REMOVE_DIALOG_URL =
+ "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml";
+
+// Greek IDN for 'example.test'.
+const TEST_IDN_ORIGIN =
+ "https://\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1.\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE";
+const TEST_PUNY_ORIGIN = "https://xn--hxajbheg2az3al.xn--jxalpdlp/";
+const TEST_PUNY_SUB_ORIGIN = "https://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.sys.mjs",
+});
+
+async function testClearing(
+ testQuota,
+ testCookies,
+ testURI,
+ originA,
+ subOriginA,
+ originB
+) {
+ // Create a variant of originB which is partitioned under top level originA.
+ let { scheme, host } = Services.io.newURI(originA);
+ let partitionKey = `(${scheme},${host})`;
+
+ let { origin: originBPartitioned } =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(originB),
+ { partitionKey }
+ );
+
+ // Add some test quota storage.
+ if (testQuota) {
+ await SiteDataTestUtils.addToIndexedDB(originA);
+ await SiteDataTestUtils.addToIndexedDB(subOriginA);
+ await SiteDataTestUtils.addToIndexedDB(originBPartitioned);
+ }
+
+ // Add some test cookies.
+ if (testCookies) {
+ SiteDataTestUtils.addToCookies({
+ origin: originA,
+ name: "test1",
+ value: "1",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: originA,
+ name: "test2",
+ value: "2",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: subOriginA,
+ name: "test3",
+ value: "1",
+ });
+
+ SiteDataTestUtils.addToCookies({
+ origin: originBPartitioned,
+ name: "test4",
+ value: "1",
+ });
+ }
+
+ await BrowserTestUtils.withNewTab(testURI, async function (browser) {
+ // Verify we have added quota storage.
+ if (testQuota) {
+ let usage = await SiteDataTestUtils.getQuotaUsage(originA);
+ Assert.greater(usage, 0, "Should have data for the base origin.");
+
+ usage = await SiteDataTestUtils.getQuotaUsage(subOriginA);
+ Assert.greater(usage, 0, "Should have data for the sub origin.");
+
+ usage = await SiteDataTestUtils.getQuotaUsage(originBPartitioned);
+ Assert.greater(usage, 0, "Should have data for the partitioned origin.");
+ }
+
+ // Open the identity popup.
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+
+ let clearFooter = document.getElementById(
+ "identity-popup-clear-sitedata-footer"
+ );
+ let clearButton = document.getElementById(
+ "identity-popup-clear-sitedata-button"
+ );
+ TestUtils.waitForCondition(
+ () => !clearFooter.hidden,
+ "The clear data footer is not hidden."
+ );
+
+ let cookiesCleared;
+ if (testCookies) {
+ cookiesCleared = Promise.all([
+ TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted" && subj.name == "test1"
+ ),
+ TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted" && subj.name == "test2"
+ ),
+ TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted" && subj.name == "test3"
+ ),
+ TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted" && subj.name == "test4"
+ ),
+ ]);
+ }
+
+ // Click the "Clear data" button.
+ let siteDataUpdated = TestUtils.topicObserved(
+ "sitedatamanager:sites-updated"
+ );
+ let hideEvent = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ clearButton.click();
+ await hideEvent;
+ await removeDialogPromise;
+
+ await siteDataUpdated;
+
+ // Check that cookies were deleted.
+ if (testCookies) {
+ await cookiesCleared;
+ let uri = Services.io.newURI(originA);
+ is(
+ Services.cookies.countCookiesFromHost(uri.host),
+ 0,
+ "Cookies from the base domain should be cleared"
+ );
+ uri = Services.io.newURI(subOriginA);
+ is(
+ Services.cookies.countCookiesFromHost(uri.host),
+ 0,
+ "Cookies from the sub domain should be cleared"
+ );
+ ok(
+ !SiteDataTestUtils.hasCookies(originBPartitioned),
+ "Partitioned cookies should be cleared"
+ );
+ }
+
+ // Check that quota storage was deleted.
+ if (testQuota) {
+ await TestUtils.waitForCondition(async () => {
+ let usage = await SiteDataTestUtils.getQuotaUsage(originA);
+ return usage == 0;
+ }, "Should have no data for the base origin.");
+
+ let usage = await SiteDataTestUtils.getQuotaUsage(subOriginA);
+ is(usage, 0, "Should have no data for the sub origin.");
+
+ usage = await SiteDataTestUtils.getQuotaUsage(originBPartitioned);
+ is(usage, 0, "Should have no data for the partitioned origin.");
+ }
+
+ // Open the site identity panel again to check that the button isn't shown anymore.
+ promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popupshown"
+ );
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+
+ // Wait for a second to see if the button is shown.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 1000));
+
+ ok(
+ clearFooter.hidden,
+ "The clear data footer is hidden after clearing data."
+ );
+ });
+}
+
+// Test removing quota managed storage.
+add_task(async function test_ClearSiteData() {
+ await testClearing(
+ true,
+ false,
+ TEST_ORIGIN,
+ TEST_ORIGIN,
+ TEST_SUB_ORIGIN,
+ TEST_ORIGIN_2
+ );
+});
+
+// Test removing cookies.
+add_task(async function test_ClearCookies() {
+ await testClearing(
+ false,
+ true,
+ TEST_ORIGIN,
+ TEST_ORIGIN,
+ TEST_SUB_ORIGIN,
+ TEST_ORIGIN_2
+ );
+});
+
+// Test removing both.
+add_task(async function test_ClearCookiesAndSiteData() {
+ await testClearing(
+ true,
+ true,
+ TEST_ORIGIN,
+ TEST_ORIGIN,
+ TEST_SUB_ORIGIN,
+ TEST_ORIGIN_2
+ );
+});
+
+// Test IDN Domains
+add_task(async function test_IDN_ClearCookiesAndSiteData() {
+ await testClearing(
+ true,
+ true,
+ TEST_IDN_ORIGIN,
+ TEST_PUNY_ORIGIN,
+ TEST_PUNY_SUB_ORIGIN,
+ TEST_ORIGIN_2
+ );
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData_extensions.js b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData_extensions.js
new file mode 100644
index 0000000000..2d0d9f7068
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData_extensions.js
@@ -0,0 +1,80 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/*
+ * Test for Bug 1661534 - Extension page: "Clear Cookies and Site Data"
+ * does nothing.
+ *
+ * Expected behavior: when viewing a page controlled by a WebExtension,
+ * the "Clear Cookies and Site Data..." button should not be visible.
+ */
+
+add_task(async function testClearSiteDataFooterHiddenForExtensions() {
+ // Create an extension that opens an options page
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ permissions: ["tabs"],
+ options_ui: {
+ page: "options.html",
+ open_in_tab: true,
+ },
+ },
+ files: {
+ "options.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <h1>This is a test options page for a WebExtension</h1>
+ </body>
+ </html>`,
+ },
+ async background() {
+ await browser.runtime.openOptionsPage();
+ browser.test.sendMessage("optionsopened");
+ },
+ });
+
+ // Run the extension and wait until its options page has finished loading
+ let browser = gBrowser.selectedBrowser;
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(browser);
+ await extension.startup();
+ await extension.awaitMessage("optionsopened");
+ await browserLoadedPromise;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ ok(
+ content.document.documentURI.startsWith("moz-extension://"),
+ "Extension page has now finished loading in the browser window"
+ );
+ });
+
+ // Open the site identity popup
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+
+ let clearSiteDataFooter = document.getElementById(
+ "identity-popup-clear-sitedata-footer"
+ );
+
+ ok(
+ clearSiteDataFooter.hidden,
+ "The clear site data footer is hidden on a WebExtension page."
+ );
+
+ // Unload the extension
+ await extension.unload();
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_custom_roots.js b/browser/base/content/test/siteIdentity/browser_identityPopup_custom_roots.js
new file mode 100644
index 0000000000..2b9ef53bb0
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_custom_roots.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that the UI for imported root certificates shows up correctly in the identity popup.
+ */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+// This test is incredibly simple, because our test framework already
+// imports root certificates by default, so we just visit example.com
+// and verify that the custom root certificates UI is visible.
+add_task(async function test_https() {
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+ let customRootWarning = document.getElementById(
+ "identity-popup-security-decription-custom-root"
+ );
+ ok(
+ BrowserTestUtils.is_visible(customRootWarning),
+ "custom root warning is visible"
+ );
+
+ let securityView = document.getElementById("identity-popup-securityView");
+ let shown = BrowserTestUtils.waitForEvent(securityView, "ViewShown");
+ document.getElementById("identity-popup-security-button").click();
+ await shown;
+
+ let subPanelInfo = document.getElementById(
+ "identity-popup-content-verifier-unknown"
+ );
+ ok(
+ BrowserTestUtils.is_visible(subPanelInfo),
+ "custom root warning in sub panel is visible"
+ );
+ });
+});
+
+// Also check that there are conditions where this isn't shown.
+add_task(async function test_http() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com", async function () {
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+ let customRootWarning = document.getElementById(
+ "identity-popup-security-decription-custom-root"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(customRootWarning),
+ "custom root warning is hidden"
+ );
+
+ let securityView = document.getElementById("identity-popup-securityView");
+ let shown = BrowserTestUtils.waitForEvent(securityView, "ViewShown");
+ document.getElementById("identity-popup-security-button").click();
+ await shown;
+
+ let subPanelInfo = document.getElementById(
+ "identity-popup-content-verifier-unknown"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(subPanelInfo),
+ "custom root warning in sub panel is hidden"
+ );
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js b/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js
new file mode 100644
index 0000000000..80e70619ff
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js
@@ -0,0 +1,120 @@
+/* Tests the focus behavior of the identity popup. */
+
+// Focusing on the identity box is handled by the ToolbarKeyboardNavigator
+// component (see browser/base/content/browser-toolbarKeyNav.js).
+async function focusIdentityBox() {
+ gURLBar.inputField.focus();
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+ const focused = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityIconBox,
+ "focus"
+ );
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ EventUtils.synthesizeKey("ArrowRight");
+ await focused;
+}
+
+// Access the identity popup via mouseclick. Focus should not be moved inside.
+add_task(async function testIdentityPopupFocusClick() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ let shown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ EventUtils.synthesizeMouseAtCenter(gIdentityHandler._identityIconBox, {});
+ await shown;
+ isnot(
+ Services.focus.focusedElement,
+ document.getElementById("identity-popup-security-button")
+ );
+ });
+});
+
+// Access the identity popup via keyboard. Focus should be moved inside.
+add_task(async function testIdentityPopupFocusKeyboard() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ await focusIdentityBox();
+ let shown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ EventUtils.sendString(" ");
+ await shown;
+ is(
+ Services.focus.focusedElement,
+ document.getElementById("identity-popup-security-button")
+ );
+ });
+});
+
+// Access the Site Security panel, then move focus with the tab key.
+// Tabbing should be able to reach the More Information button.
+add_task(async function testSiteSecurityTabOrder() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ // 1. Access the identity popup.
+ await focusIdentityBox();
+ let shown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ EventUtils.sendString(" ");
+ await shown;
+ is(
+ Services.focus.focusedElement,
+ document.getElementById("identity-popup-security-button")
+ );
+
+ // 2. Access the Site Security section.
+ let securityView = document.getElementById("identity-popup-securityView");
+ shown = BrowserTestUtils.waitForEvent(securityView, "ViewShown");
+ EventUtils.sendString(" ");
+ await shown;
+
+ // 3. Custom root learn more info should be focused by default
+ // This is probably not present in real-world scenarios, but needs to be present in our test infrastructure.
+ let customRootLearnMore = document.getElementById(
+ "identity-popup-custom-root-learn-more"
+ );
+ is(
+ Services.focus.focusedElement,
+ customRootLearnMore,
+ "learn more option for custom roots is focused"
+ );
+
+ // 4. First press of tab should move to the More Information button.
+ let moreInfoButton = document.getElementById("identity-popup-more-info");
+ let focused = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "focusin"
+ );
+ EventUtils.sendKey("tab");
+ await focused;
+ is(
+ Services.focus.focusedElement,
+ moreInfoButton,
+ "more info button is focused"
+ );
+
+ // 5. Second press of tab should focus the Back button.
+ let backButton = gIdentityHandler._identityPopup.querySelector(
+ ".subviewbutton-back"
+ );
+ // Wait for focus to move somewhere. We use focusin because focus doesn't bubble.
+ focused = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "focusin"
+ );
+ EventUtils.sendKey("tab");
+ await focused;
+ is(Services.focus.focusedElement, backButton, "back button is focused");
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identity_UI.js b/browser/base/content/test/siteIdentity/browser_identity_UI.js
new file mode 100644
index 0000000000..bda5e225b0
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identity_UI.js
@@ -0,0 +1,192 @@
+/* Tests for correct behaviour of getHostForDisplay on identity handler */
+
+requestLongerTimeout(2);
+
+// Greek IDN for 'example.test'.
+var idnDomain =
+ "\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1.\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE";
+var tests = [
+ {
+ name: "normal domain",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ location: "http://test1.example.org/",
+ hostForDisplay: "test1.example.org",
+ hasSubview: true,
+ },
+ {
+ name: "view-source",
+ location: "view-source:http://example.com/",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ newURI: "http://example.com/",
+ hostForDisplay: "example.com",
+ hasSubview: true,
+ },
+ {
+ name: "normal HTTPS",
+ location: "https://example.com/",
+ hostForDisplay: "example.com",
+ hasSubview: true,
+ },
+ {
+ name: "IDN subdomain",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ location: "http://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/",
+ hostForDisplay: "sub1." + idnDomain,
+ hasSubview: true,
+ },
+ {
+ name: "subdomain with port",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ location: "http://sub1.test1.example.org:8000/",
+ hostForDisplay: "sub1.test1.example.org",
+ hasSubview: true,
+ },
+ {
+ name: "subdomain HTTPS",
+ location: "https://test1.example.com/",
+ hostForDisplay: "test1.example.com",
+ hasSubview: true,
+ },
+ {
+ name: "view-source HTTPS",
+ location: "view-source:https://example.com/",
+ newURI: "https://example.com/",
+ hostForDisplay: "example.com",
+ hasSubview: true,
+ },
+ {
+ name: "IP address",
+ location: "http://127.0.0.1:8888/",
+ hostForDisplay: "127.0.0.1",
+ hasSubview: false,
+ },
+ {
+ name: "about:certificate",
+ location:
+ "about:certificate?cert=MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO&cert=1pVzllk7ZFHzANBgkqhkiG9w0BAQ",
+ hostForDisplay: "about:certificate",
+ hasSubview: false,
+ },
+ {
+ name: "about:reader",
+ location: "about:reader?url=http://example.com",
+ hostForDisplay: "example.com",
+ hasSubview: false,
+ },
+ {
+ name: "chrome:",
+ location: "chrome://global/skin/in-content/info-pages.css",
+ hostForDisplay: "chrome://global/skin/in-content/info-pages.css",
+ hasSubview: false,
+ },
+];
+
+add_task(async function test() {
+ ok(gIdentityHandler, "gIdentityHandler should exist");
+
+ await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ for (let i = 0; i < tests.length; i++) {
+ await runTest(i, true);
+ }
+
+ gBrowser.removeCurrentTab();
+ await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ for (let i = tests.length - 1; i >= 0; i--) {
+ await runTest(i, false);
+ }
+
+ gBrowser.removeCurrentTab();
+});
+
+async function runTest(i, forward) {
+ let currentTest = tests[i];
+ let testDesc = "#" + i + " (" + currentTest.name + ")";
+ if (!forward) {
+ testDesc += " (second time)";
+ }
+
+ info("Running test " + testDesc);
+
+ let popupHidden = null;
+ if ((forward && i > 0) || (!forward && i < tests.length - 1)) {
+ popupHidden = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ currentTest.location
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ currentTest.location
+ );
+ await loaded;
+ await popupHidden;
+ ok(
+ !gIdentityHandler._identityPopup ||
+ BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup),
+ "Control Center is hidden"
+ );
+
+ // Sanity check other values, and the value of gIdentityHandler.getHostForDisplay()
+ is(
+ gIdentityHandler._uri.spec,
+ currentTest.newURI || currentTest.location,
+ "location matches for test " + testDesc
+ );
+ // getHostForDisplay can't be called for all modes
+ if (currentTest.hostForDisplay !== null) {
+ is(
+ gIdentityHandler.getHostForDisplay(),
+ currentTest.hostForDisplay,
+ "hostForDisplay matches for test " + testDesc
+ );
+ }
+
+ // Open the Control Center and make sure it closes after nav (Bug 1207542).
+ let popupShown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ info("Waiting for the Control Center to be shown");
+ await popupShown;
+ ok(
+ !BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup),
+ "Control Center is visible"
+ );
+ let displayedHost = currentTest.hostForDisplay || currentTest.location;
+ ok(
+ gIdentityHandler._identityPopupMainViewHeaderLabel.textContent.includes(
+ displayedHost
+ ),
+ "identity UI header shows the host for test " + testDesc
+ );
+
+ let securityButton = gBrowser.ownerDocument.querySelector(
+ "#identity-popup-security-button"
+ );
+ is(
+ securityButton.disabled,
+ !currentTest.hasSubview,
+ "Security button has correct disabled state"
+ );
+ if (currentTest.hasSubview) {
+ // Show the subview, which is an easy way in automation to reproduce
+ // Bug 1207542, where the CC wouldn't close on navigation.
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "ViewShown"
+ );
+ securityButton.click();
+ await promiseViewShown;
+ }
+}
diff --git a/browser/base/content/test/siteIdentity/browser_iframe_navigation.js b/browser/base/content/test/siteIdentity/browser_iframe_navigation.js
new file mode 100644
index 0000000000..ac2884d31a
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_iframe_navigation.js
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the site identity icon and related machinery reflects the correct
+// security state after navigating an iframe in various contexts.
+// See bug 1490982.
+
+const ROOT_URI = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const SECURE_TEST_URI = ROOT_URI + "iframe_navigation.html";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const INSECURE_TEST_URI = SECURE_TEST_URI.replace("https://", "http://");
+
+// From a secure URI, navigate the iframe to about:blank (should still be
+// secure).
+add_task(async function () {
+ let uri = SECURE_TEST_URI + "#blank";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "verifiedDomain", "identity should be secure before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode =
+ window.document.getElementById("identity-box").className;
+ is(newIdentityMode, "verifiedDomain", "identity should be secure after");
+ });
+});
+
+// From a secure URI, navigate the iframe to an insecure URI (http://...)
+// (mixed active content should be blocked, should still be secure).
+add_task(async function () {
+ let uri = SECURE_TEST_URI + "#insecure";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "verifiedDomain", "identity should be secure before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode =
+ window.document.getElementById("identity-box").classList;
+ ok(
+ newIdentityMode.contains("mixedActiveBlocked"),
+ "identity should be blocked mixed active content after"
+ );
+ ok(
+ newIdentityMode.contains("verifiedDomain"),
+ "identity should still contain 'verifiedDomain'"
+ );
+ is(newIdentityMode.length, 2, "shouldn't have any other identity states");
+ });
+});
+
+// From an insecure URI (http://..), navigate the iframe to about:blank (should
+// still be insecure).
+add_task(async function () {
+ let uri = INSECURE_TEST_URI + "#blank";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "notSecure", "identity should be 'not secure' before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode =
+ window.document.getElementById("identity-box").className;
+ is(newIdentityMode, "notSecure", "identity should be 'not secure' after");
+ });
+});
+
+// From an insecure URI (http://..), navigate the iframe to a secure URI
+// (https://...) (should still be insecure).
+add_task(async function () {
+ let uri = INSECURE_TEST_URI + "#secure";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "notSecure", "identity should be 'not secure' before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode =
+ window.document.getElementById("identity-box").className;
+ is(newIdentityMode, "notSecure", "identity should be 'not secure' after");
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_ignore_same_page_navigation.js b/browser/base/content/test/siteIdentity/browser_ignore_same_page_navigation.js
new file mode 100644
index 0000000000..86ec70a0cf
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_ignore_same_page_navigation.js
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the nsISecureBrowserUI implementation doesn't send extraneous OnSecurityChange events
+// when it receives OnLocationChange events with the LOCATION_CHANGE_SAME_DOCUMENT flag set.
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let onLocationChangeCount = 0;
+ let onSecurityChangeCount = 0;
+ let progressListener = {
+ onStateChange() {},
+ onLocationChange() {
+ onLocationChangeCount++;
+ },
+ onSecurityChange() {
+ onSecurityChangeCount++;
+ },
+ onProgressChange() {},
+ onStatusChange() {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ browser.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ let uri =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+ BrowserTestUtils.loadURIString(browser, uri);
+ await BrowserTestUtils.browserLoaded(browser, false, uri);
+ is(onLocationChangeCount, 1, "should have 1 onLocationChange event");
+ is(onSecurityChangeCount, 1, "should have 1 onSecurityChange event");
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.history.pushState({}, "", "https://example.com");
+ });
+ is(onLocationChangeCount, 2, "should have 2 onLocationChange events");
+ is(
+ onSecurityChangeCount,
+ 1,
+ "should still have only 1 onSecurityChange event"
+ );
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_mcb_redirect.js b/browser/base/content/test/siteIdentity/browser_mcb_redirect.js
new file mode 100644
index 0000000000..df7f6be15c
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mcb_redirect.js
@@ -0,0 +1,360 @@
+/*
+ * Description of the Tests for
+ * - Bug 418354 - Call Mixed content blocking on redirects
+ *
+ * Single redirect script tests
+ * 1. Load a script over https inside an https page
+ * - the server responds with a 302 redirect to a >> HTTP << script
+ * - the doorhanger should appear!
+ *
+ * 2. Load a script over https inside an http page
+ * - the server responds with a 302 redirect to a >> HTTP << script
+ * - the doorhanger should not appear!
+ *
+ * Single redirect image tests
+ * 3. Load an image over https inside an https page
+ * - the server responds with a 302 redirect to a >> HTTP << image
+ * - the image should not load
+ *
+ * 4. Load an image over https inside an http page
+ * - the server responds with a 302 redirect to a >> HTTP << image
+ * - the image should load and get cached
+ *
+ * Single redirect cached image tests
+ * 5. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an http page
+ * - the server would have responded with a 302 redirect to a >> HTTP <<
+ * image, but instead we try to use the cached image.
+ * - the image should load
+ *
+ * 6. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an https page
+ * - the server would have responded with a 302 redirect to a >> HTTP <<
+ * image, but instead we try to use the cached image.
+ * - the image should not load
+ *
+ * Double redirect image test
+ * 7. Load an image over https inside an http page
+ * - the server responds with a 302 redirect to a >> HTTP << server
+ * - the HTTP server responds with a 302 redirect to a >> HTTPS << image
+ * - the image should load and get cached
+ *
+ * Double redirect cached image tests
+ * 8. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an http page
+ * - the image would have gone through two redirects: HTTPS->HTTP->HTTPS,
+ * but instead we try to use the cached image.
+ * - the image should load
+ *
+ * 9. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an https page
+ * - the image would have gone through two redirects: HTTPS->HTTP->HTTPS,
+ * but instead we try to use the cached image.
+ * - the image should not load
+ */
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_DISPLAY_UPGRADE = "security.mixed_content.upgrade_display_content";
+const HTTPS_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const HTTP_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const PREF_INSECURE_ICON = "security.insecure_connection_icon.enabled";
+
+var origBlockActive;
+var origBlockDisplay;
+var origUpgradeDisplay;
+var origInsecurePref;
+var gTestBrowser = null;
+
+// ------------------------ Helper Functions ---------------------
+
+registerCleanupFunction(function () {
+ // Set preferences back to their original values
+ Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
+ Services.prefs.setBoolPref(PREF_DISPLAY, origBlockDisplay);
+ Services.prefs.setBoolPref(PREF_DISPLAY_UPGRADE, origUpgradeDisplay);
+ Services.prefs.setBoolPref(PREF_INSECURE_ICON, origInsecurePref);
+
+ // Make sure we are online again
+ Services.io.offline = false;
+});
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+ finish();
+}
+
+// ------------------------ Test 1 ------------------------------
+
+function test1() {
+ Services.prefs.setBoolPref(PREF_INSECURE_ICON, false);
+
+ var url = HTTPS_TEST_ROOT + "test_mcb_redirect.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkUIForTest1
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function testInsecure1() {
+ Services.prefs.setBoolPref(PREF_INSECURE_ICON, true);
+
+ var url = HTTPS_TEST_ROOT + "test_mcb_redirect.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkUIForTest1
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+async function checkUIForTest1() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "script blocked";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test1!"
+ );
+ }).then(test2);
+}
+
+// ------------------------ Test 2 ------------------------------
+
+function test2() {
+ var url = HTTP_TEST_ROOT + "test_mcb_redirect.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkUIForTest2
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+async function checkUIForTest2() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "script executed";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test2!"
+ );
+ }).then(test3);
+}
+
+// ------------------------ Test 3 ------------------------------
+// HTTPS page loading insecure image
+function test3() {
+ info("test3");
+ var url = HTTPS_TEST_ROOT + "test_mcb_redirect_image.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkLoadEventForTest3
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function checkLoadEventForTest3() {
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "image blocked";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test3!"
+ );
+ }).then(test4);
+}
+
+// ------------------------ Test 4 ------------------------------
+// HTTP page loading insecure image
+function test4() {
+ info("test4");
+ var url = HTTP_TEST_ROOT + "test_mcb_redirect_image.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkLoadEventForTest4
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function checkLoadEventForTest4() {
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "image loaded";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test4!"
+ );
+ }).then(test5);
+}
+
+// ------------------------ Test 5 ------------------------------
+// HTTP page laoding insecure cached image
+// Assuming test 4 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test5() {
+ // Go into offline mode
+ info("test5");
+ Services.io.offline = true;
+ var url = HTTP_TEST_ROOT + "test_mcb_redirect_image.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkLoadEventForTest5
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function checkLoadEventForTest5() {
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "image loaded";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test5!"
+ );
+ }).then(() => {
+ // Go back online
+ Services.io.offline = false;
+ test6();
+ });
+}
+
+// ------------------------ Test 6 ------------------------------
+// HTTPS page loading insecure cached image
+// Assuming test 4 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test6() {
+ // Go into offline mode
+ info("test6");
+ Services.io.offline = true;
+ var url = HTTPS_TEST_ROOT + "test_mcb_redirect_image.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkLoadEventForTest6
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function checkLoadEventForTest6() {
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "image blocked";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test6!"
+ );
+ }).then(() => {
+ // Go back online
+ Services.io.offline = false;
+ test7();
+ });
+}
+
+// ------------------------ Test 7 ------------------------------
+// HTTP page loading insecure image that went through a double redirect
+function test7() {
+ var url = HTTP_TEST_ROOT + "test_mcb_double_redirect_image.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkLoadEventForTest7
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function checkLoadEventForTest7() {
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "image loaded";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test7!"
+ );
+ }).then(test8);
+}
+
+// ------------------------ Test 8 ------------------------------
+// HTTP page loading insecure cached image that went through a double redirect
+// Assuming test 7 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test8() {
+ // Go into offline mode
+ Services.io.offline = true;
+ var url = HTTP_TEST_ROOT + "test_mcb_double_redirect_image.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkLoadEventForTest8
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function checkLoadEventForTest8() {
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "image loaded";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test8!"
+ );
+ }).then(() => {
+ // Go back online
+ Services.io.offline = false;
+ test9();
+ });
+}
+
+// ------------------------ Test 9 ------------------------------
+// HTTPS page loading insecure cached image that went through a double redirect
+// Assuming test 7 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test9() {
+ // Go into offline mode
+ Services.io.offline = true;
+ var url = HTTPS_TEST_ROOT + "test_mcb_double_redirect_image.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkLoadEventForTest9
+ );
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+}
+
+function checkLoadEventForTest9() {
+ SpecialPowers.spawn(gTestBrowser, [], async function () {
+ var expected = "image blocked";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test9!"
+ );
+ }).then(() => {
+ // Go back online
+ Services.io.offline = false;
+ cleanUpAfterTests();
+ });
+}
+
+// ------------------------ SETUP ------------------------------
+
+function test() {
+ // Performing async calls, e.g. 'onload', we have to wait till all of them finished
+ waitForExplicitFinish();
+
+ // Store original preferences so we can restore settings after testing
+ origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE);
+ origBlockDisplay = Services.prefs.getBoolPref(PREF_DISPLAY);
+ origUpgradeDisplay = Services.prefs.getBoolPref(PREF_DISPLAY_UPGRADE);
+ origInsecurePref = Services.prefs.getBoolPref(PREF_INSECURE_ICON);
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+ Services.prefs.setBoolPref(PREF_DISPLAY, true);
+ Services.prefs.setBoolPref(PREF_DISPLAY_UPGRADE, false);
+
+ var newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop();
+
+ executeSoon(testInsecure1);
+}
diff --git a/browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js b/browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js
new file mode 100644
index 0000000000..c6096342cc
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js
@@ -0,0 +1,37 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Test for Bug 1182551 -
+ *
+ * This test has a top level HTTP page with an HTTPS iframe. The HTTPS iframe
+ * includes an HTTP image. We check that the top level security state is
+ * STATE_IS_INSECURE. The mixed content from the iframe shouldn't "upgrade"
+ * the HTTP top level page to broken HTTPS.
+ */
+
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ ) + "file_mixedContentFramesOnHttp.html";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", false],
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
+ isSecurityState(browser, "insecure");
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: false,
+ activeBlocked: false,
+ passiveLoaded: true,
+ });
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_mixedContentFromOnunload.js b/browser/base/content/test/siteIdentity/browser_mixedContentFromOnunload.js
new file mode 100644
index 0000000000..c9e11e54a7
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixedContentFromOnunload.js
@@ -0,0 +1,68 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Tests for Bug 947079 - Fix bug in nsSecureBrowserUIImpl that sets the wrong
+ * security state on a page because of a subresource load that is not on the
+ * same page.
+ */
+
+// We use different domains for each test and for navigation within each test
+const HTTP_TEST_ROOT_1 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const HTTPS_TEST_ROOT_1 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://test1.example.com"
+);
+const HTTP_TEST_ROOT_2 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.net"
+);
+const HTTPS_TEST_ROOT_2 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://test2.example.com"
+);
+
+add_task(async function () {
+ let url = HTTP_TEST_ROOT_1 + "file_mixedContentFromOnunload.html";
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", false],
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+ // Navigation from an http page to a https page with no mixed content
+ // The http page loads an http image on unload
+ url = HTTPS_TEST_ROOT_1 + "file_mixedContentFromOnunload_test1.html";
+ BrowserTestUtils.loadURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser);
+ // check security state. Since current url is https and doesn't have any
+ // mixed content resources, we expect it to be secure.
+ isSecurityState(browser, "secure");
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: false,
+ activeBlocked: false,
+ passiveLoaded: false,
+ });
+ // Navigation from an http page to a https page that has mixed display content
+ // The https page loads an http image on unload
+ url = HTTP_TEST_ROOT_2 + "file_mixedContentFromOnunload.html";
+ BrowserTestUtils.loadURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser);
+ url = HTTPS_TEST_ROOT_2 + "file_mixedContentFromOnunload_test2.html";
+ BrowserTestUtils.loadURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "broken");
+ await assertMixedContentBlockingState(browser, {
+ activeLoaded: false,
+ activeBlocked: false,
+ passiveLoaded: true,
+ });
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_mixed_content_cert_override.js b/browser/base/content/test/siteIdentity/browser_mixed_content_cert_override.js
new file mode 100644
index 0000000000..6ca9655406
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixed_content_cert_override.js
@@ -0,0 +1,69 @@
+/*
+ * Bug 1253771 - check mixed content blocking in combination with overriden certificates
+ */
+
+"use strict";
+
+const MIXED_CONTENT_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://self-signed.example.com"
+ ) + "test-mixedcontent-securityerrors.html";
+
+function getConnectionState() {
+ return document.getElementById("identity-popup").getAttribute("connection");
+}
+
+function getPopupContentVerifier() {
+ return document.getElementById("identity-popup-content-verifier");
+}
+
+function getIdentityIcon() {
+ return window.getComputedStyle(document.getElementById("identity-icon"))
+ .listStyleImage;
+}
+
+function checkIdentityPopup(icon) {
+ gIdentityHandler.refreshIdentityPopup();
+ is(getIdentityIcon(), `url("chrome://global/skin/icons/${icon}")`);
+ is(getConnectionState(), "secure-cert-user-overridden");
+ isnot(
+ getPopupContentVerifier().style.display,
+ "none",
+ "Overridden certificate warning is shown"
+ );
+ ok(
+ getPopupContentVerifier().textContent.includes("security exception"),
+ "Text shows overridden certificate warning."
+ );
+}
+
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // check that a warning is shown when loading a page with mixed content and an overridden certificate
+ await loadBadCertPage(MIXED_CONTENT_URL);
+ checkIdentityPopup("security-warning.svg");
+
+ // check that the crossed out icon is shown when disabling mixed content protection
+ gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ checkIdentityPopup("security-broken.svg");
+
+ // check that a warning is shown even without mixed content
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "https://self-signed.example.com"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ checkIdentityPopup("security-warning.svg");
+
+ // remove cert exception
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("self-signed.example.com", -1, {});
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/siteIdentity/browser_mixed_content_with_navigation.js b/browser/base/content/test/siteIdentity/browser_mixed_content_with_navigation.js
new file mode 100644
index 0000000000..48171ee876
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixed_content_with_navigation.js
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the site identity indicator is properly updated when loading from
+// the BF cache. This is achieved by loading a page, navigating to another page,
+// and then going "back" to the first page, as well as the reverse (loading to
+// the other page, navigating to the page we're interested in, going back, and
+// then going forward again).
+
+const kBaseURI = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const kSecureURI = kBaseURI + "dummy_page.html";
+
+const kTestcases = [
+ {
+ uri: kBaseURI + "file_mixedPassiveContent.html",
+ expectErrorPage: false,
+ expectedIdentityMode: "mixedDisplayContent",
+ },
+ {
+ uri: kBaseURI + "file_bug1045809_1.html",
+ expectErrorPage: false,
+ expectedIdentityMode: "mixedActiveBlocked",
+ },
+ {
+ uri: "https://expired.example.com",
+ expectErrorPage: true,
+ expectedIdentityMode: "certErrorPage",
+ },
+];
+
+add_task(async function () {
+ for (let testcase of kTestcases) {
+ await run_testcase(testcase);
+ }
+});
+
+async function run_testcase(testcase) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.mixed_content.upgrade_display_content", false]],
+ });
+ // Test the forward and back case.
+ // Start by loading an unrelated URI so that this generalizes well when the
+ // testcase would otherwise first navigate to an error page, which doesn't
+ // seem to work with withNewTab.
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ // Navigate to the test URI.
+ BrowserTestUtils.loadURIString(browser, testcase.uri);
+ if (!testcase.expectErrorPage) {
+ await BrowserTestUtils.browserLoaded(browser, false, testcase.uri);
+ } else {
+ await BrowserTestUtils.waitForErrorPage(browser);
+ }
+ let identityMode = window.document.getElementById("identity-box").classList;
+ ok(
+ identityMode.contains(testcase.expectedIdentityMode),
+ `identity should be ${testcase.expectedIdentityMode}`
+ );
+
+ // Navigate to a URI that should be secure.
+ BrowserTestUtils.loadURIString(browser, kSecureURI);
+ await BrowserTestUtils.browserLoaded(browser, false, kSecureURI);
+ let secureIdentityMode =
+ window.document.getElementById("identity-box").className;
+ is(secureIdentityMode, "verifiedDomain", "identity should be secure now");
+
+ // Go back to the test page.
+ browser.webNavigation.goBack();
+ if (!testcase.expectErrorPage) {
+ await BrowserTestUtils.browserStopped(browser, testcase.uri);
+ } else {
+ await BrowserTestUtils.waitForErrorPage(browser);
+ }
+ let identityModeAgain =
+ window.document.getElementById("identity-box").classList;
+ ok(
+ identityModeAgain.contains(testcase.expectedIdentityMode),
+ `identity should again be ${testcase.expectedIdentityMode}`
+ );
+ });
+
+ // Test the back and forward case.
+ // Start on a secure page.
+ await BrowserTestUtils.withNewTab(kSecureURI, async browser => {
+ let secureIdentityMode =
+ window.document.getElementById("identity-box").className;
+ is(secureIdentityMode, "verifiedDomain", "identity should start as secure");
+
+ // Navigate to the test URI.
+ BrowserTestUtils.loadURIString(browser, testcase.uri);
+ if (!testcase.expectErrorPage) {
+ await BrowserTestUtils.browserLoaded(browser, false, testcase.uri);
+ } else {
+ await BrowserTestUtils.waitForErrorPage(browser);
+ }
+ let identityMode = window.document.getElementById("identity-box").classList;
+ ok(
+ identityMode.contains(testcase.expectedIdentityMode),
+ `identity should be ${testcase.expectedIdentityMode}`
+ );
+
+ // Go back to the secure page.
+ browser.webNavigation.goBack();
+ await BrowserTestUtils.browserStopped(browser, kSecureURI);
+ let secureIdentityModeAgain =
+ window.document.getElementById("identity-box").className;
+ is(
+ secureIdentityModeAgain,
+ "verifiedDomain",
+ "identity should be secure again"
+ );
+
+ // Go forward again to the test URI.
+ browser.webNavigation.goForward();
+ if (!testcase.expectErrorPage) {
+ await BrowserTestUtils.browserStopped(browser, testcase.uri);
+ } else {
+ await BrowserTestUtils.waitForErrorPage(browser);
+ }
+ let identityModeAgain =
+ window.document.getElementById("identity-box").classList;
+ ok(
+ identityModeAgain.contains(testcase.expectedIdentityMode),
+ `identity should again be ${testcase.expectedIdentityMode}`
+ );
+ });
+}
diff --git a/browser/base/content/test/siteIdentity/browser_mixed_passive_content_indicator.js b/browser/base/content/test/siteIdentity/browser_mixed_passive_content_indicator.js
new file mode 100644
index 0000000000..909764c597
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixed_passive_content_indicator.js
@@ -0,0 +1,18 @@
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "simple_mixed_passive.html";
+
+add_task(async function test_mixed_passive_content_indicator() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.mixed_content.upgrade_display_content", false]],
+ });
+ await BrowserTestUtils.withNewTab(TEST_URL, function () {
+ is(
+ document.getElementById("identity-box").className,
+ "unknownIdentity mixedDisplayContent",
+ "identity box has class name for mixed content"
+ );
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_mixedcontent_securityflags.js b/browser/base/content/test/siteIdentity/browser_mixedcontent_securityflags.js
new file mode 100644
index 0000000000..3e39426c51
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixedcontent_securityflags.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The test loads a web page with mixed active and mixed display content and
+// makes sure that the mixed content flags on the docshell are set correctly.
+// * Using default about:config prefs (mixed active blocked, mixed display
+// loaded) we load the page and check the flags.
+// * We change the about:config prefs (mixed active blocked, mixed display
+// blocked), reload the page, and check the flags again.
+// * We override protection so all mixed content can load and check the
+// flags again.
+
+const TEST_URI =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "test-mixedcontent-securityerrors.html";
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_DISPLAY_UPGRADE = "security.mixed_content.upgrade_display_content";
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+var gTestBrowser = null;
+
+registerCleanupFunction(function () {
+ // Set preferences back to their original values
+ Services.prefs.clearUserPref(PREF_DISPLAY);
+ Services.prefs.clearUserPref(PREF_DISPLAY_UPGRADE);
+ Services.prefs.clearUserPref(PREF_ACTIVE);
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function blockMixedActiveContentTest() {
+ // Turn on mixed active blocking and mixed display loading and load the page.
+ Services.prefs.setBoolPref(PREF_DISPLAY, false);
+ Services.prefs.setBoolPref(PREF_DISPLAY_UPGRADE, false);
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI);
+ gTestBrowser = gBrowser.getBrowserForTab(tab);
+
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: true,
+ });
+
+ // Turn on mixed active and mixed display blocking and reload the page.
+ Services.prefs.setBoolPref(PREF_DISPLAY, true);
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+
+ gBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+});
+
+add_task(async function overrideMCB() {
+ // Disable mixed content blocking (reloads page) and retest
+ let { gIdentityHandler } = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: true,
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_navigation_failures.js b/browser/base/content/test/siteIdentity/browser_navigation_failures.js
new file mode 100644
index 0000000000..ddb0d93fab
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_navigation_failures.js
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the site identity indicator is properly updated for navigations
+// that fail for various reasons. In particular, we currently test TLS handshake
+// failures, about: pages that don't actually exist, and situations where the
+// TLS handshake completes but the server then closes the connection.
+// See bug 1492424, bug 1493427, and bug 1391207.
+
+const kSecureURI =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(kSecureURI, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "verifiedDomain", "identity should be secure before");
+
+ const TLS_HANDSHAKE_FAILURE_URI = "https://ssl3.example.com/";
+ // Try to connect to a server where the TLS handshake will fail.
+ BrowserTestUtils.loadURIString(browser, TLS_HANDSHAKE_FAILURE_URI);
+ await BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ TLS_HANDSHAKE_FAILURE_URI,
+ true
+ );
+
+ let newIdentityMode =
+ window.document.getElementById("identity-box").className;
+ is(
+ newIdentityMode,
+ "certErrorPage notSecureText",
+ "identity should be unknown (not secure) after"
+ );
+ });
+});
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(kSecureURI, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "verifiedDomain", "identity should be secure before");
+
+ const BAD_ABOUT_PAGE_URI = "about:somethingthatdoesnotexist";
+ // Try to load an about: page that doesn't exist
+ BrowserTestUtils.loadURIString(browser, BAD_ABOUT_PAGE_URI);
+ await BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ BAD_ABOUT_PAGE_URI,
+ true
+ );
+
+ let newIdentityMode =
+ window.document.getElementById("identity-box").className;
+ is(
+ newIdentityMode,
+ "unknownIdentity",
+ "identity should be unknown (not secure) after"
+ );
+ });
+});
+
+// Helper function to start a TLS server that will accept a connection, complete
+// the TLS handshake, but then close the connection.
+function startServer(cert) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let input, output;
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ let connectionInfo = transport.securityCallbacks.getInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ connectionInfo.setSecurityObserver(listener);
+ input = transport.openInputStream(0, 0, 0);
+ output = transport.openOutputStream(0, 0, 0);
+ },
+
+ onHandshakeDone(socket, status) {
+ input.asyncWait(
+ {
+ onInputStreamReady(readyInput) {
+ try {
+ input.close();
+ output.close();
+ } catch (e) {
+ info(e);
+ }
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ },
+
+ onStopListening() {},
+ };
+
+ tlsServer.setSessionTickets(false);
+ tlsServer.asyncListen(listener);
+
+ return tlsServer;
+}
+
+// Test that if we complete a TLS handshake but the server closes the connection
+// just after doing so (resulting in a "connection reset" error page), the site
+// identity information gets updated appropriately (it should indicate "not
+// secure").
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // This test fails on some platforms if we leave IPv6 enabled.
+ set: [["network.dns.disableIPv6", true]],
+ });
+
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+
+ let cert = getTestServerCertificate();
+ // Start a server and trust its certificate.
+ let server = startServer(cert);
+ certOverrideService.rememberValidityOverride(
+ "localhost",
+ server.port,
+ {},
+ cert,
+ true
+ );
+
+ // Un-do configuration changes we've made when the test is done.
+ registerCleanupFunction(() => {
+ certOverrideService.clearValidityOverride("localhost", server.port, {});
+ server.close();
+ });
+
+ // Open up a new tab...
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ const TLS_HANDSHAKE_FAILURE_URI = `https://localhost:${server.port}/`;
+ // Try to connect to a server where the TLS handshake will succeed, but then
+ // the server closes the connection right after.
+ BrowserTestUtils.loadURIString(browser, TLS_HANDSHAKE_FAILURE_URI);
+ await BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ TLS_HANDSHAKE_FAILURE_URI,
+ true
+ );
+
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(
+ identityMode,
+ "certErrorPage notSecureText",
+ "identity should be 'unknown'"
+ );
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js b/browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js
new file mode 100644
index 0000000000..1c854e2849
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The test loads a HTTPS web page with active content from HTTP loopback URLs
+// and makes sure that the mixed content flags on the docshell are not set.
+//
+// Note that the URLs referenced within the test page intentionally use the
+// unassigned port 8 because we don't want to actually load anything, we just
+// want to check that the URLs are not blocked.
+
+// The following rejections should not be left uncaught. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+ const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+ );
+ PromiseTestUtils.expectUncaughtRejection(/NetworkError/);
+ PromiseTestUtils.expectUncaughtRejection(/NetworkError/);
+}
+
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "test_no_mcb_for_loopback.html";
+
+const LOOPBACK_PNG_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://127.0.0.1:8888"
+ ) + "moz.png";
+
+const PREF_BLOCK_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_UPGRADE_DISPLAY = "security.mixed_content.upgrade_display_content";
+const PREF_BLOCK_ACTIVE = "security.mixed_content.block_active_content";
+
+function clearAllImageCaches() {
+ let tools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
+ let imageCache = tools.getImgCacheForDocument(window.document);
+ imageCache.clearCache(true); // true=chrome
+ imageCache.clearCache(false); // false=content
+}
+
+registerCleanupFunction(function () {
+ clearAllImageCaches();
+ Services.prefs.clearUserPref(PREF_BLOCK_DISPLAY);
+ Services.prefs.clearUserPref(PREF_UPGRADE_DISPLAY);
+ Services.prefs.clearUserPref(PREF_BLOCK_ACTIVE);
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function allowLoopbackMixedContent() {
+ Services.prefs.setBoolPref(PREF_BLOCK_DISPLAY, true);
+ Services.prefs.setBoolPref(PREF_UPGRADE_DISPLAY, false);
+ Services.prefs.setBoolPref(PREF_BLOCK_ACTIVE, true);
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ const browser = gBrowser.getBrowserForTab(tab);
+
+ // Check that loopback content served from the cache is not blocked.
+ await SpecialPowers.spawn(
+ browser,
+ [LOOPBACK_PNG_URL],
+ async function (loopbackPNGUrl) {
+ const doc = content.document;
+ const img = doc.createElement("img");
+ const promiseImgLoaded = ContentTaskUtils.waitForEvent(
+ img,
+ "load",
+ false
+ );
+ img.src = loopbackPNGUrl;
+ Assert.ok(!img.complete, "loopback image not yet loaded");
+ doc.body.appendChild(img);
+ await promiseImgLoaded;
+
+ const cachedImg = doc.createElement("img");
+ cachedImg.src = img.src;
+ Assert.ok(cachedImg.complete, "loopback image loaded from cache");
+ }
+ );
+
+ await assertMixedContentBlockingState(browser, {
+ activeBlocked: false,
+ activeLoaded: false,
+ passiveLoaded: false,
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_no_mcb_for_onions.js b/browser/base/content/test/siteIdentity/browser_no_mcb_for_onions.js
new file mode 100644
index 0000000000..4a5c65b125
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_no_mcb_for_onions.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The test loads a HTTPS web page with active content from HTTP .onion URLs
+// and makes sure that the mixed content flags on the docshell are not set.
+//
+// Note that the URLs referenced within the test page intentionally use the
+// unassigned port 8 because we don't want to actually load anything, we just
+// want to check that the URLs are not blocked.
+
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "test_no_mcb_for_onions.html";
+
+const PREF_BLOCK_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_BLOCK_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_ONION_ALLOWLIST = "dom.securecontext.allowlist_onions";
+
+add_task(async function allowOnionMixedContent() {
+ registerCleanupFunction(function () {
+ gBrowser.removeCurrentTab();
+ });
+
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_BLOCK_DISPLAY, true]] });
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_BLOCK_ACTIVE, true]] });
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_ONION_ALLOWLIST, true]] });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_URL
+ ).catch(console.error);
+ const browser = gBrowser.getBrowserForTab(tab);
+
+ await assertMixedContentBlockingState(browser, {
+ activeBlocked: false,
+ activeLoaded: false,
+ passiveLoaded: false,
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_no_mcb_on_http_site.js b/browser/base/content/test/siteIdentity/browser_no_mcb_on_http_site.js
new file mode 100644
index 0000000000..30caae4ea5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_no_mcb_on_http_site.js
@@ -0,0 +1,133 @@
+/*
+ * Description of the Tests for
+ * - Bug 909920 - Mixed content warning should not show on a HTTP site
+ *
+ * Description of the tests:
+ * Test 1:
+ * 1) Load an http page
+ * 2) The page includes a css file using https
+ * 3) The css file loads an |IMAGE| << over http
+ *
+ * Test 2:
+ * 1) Load an http page
+ * 2) The page includes a css file using https
+ * 3) The css file loads a |FONT| over http
+ *
+ * Test 3:
+ * 1) Load an http page
+ * 2) The page includes a css file using https
+ * 3) The css file imports (@import) another css file using http
+ * 3) The imported css file loads a |FONT| over http
+ *
+ * Since the top-domain is >> NOT << served using https, the MCB
+ * should >> NOT << trigger a warning.
+ */
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+
+const HTTP_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+var gTestBrowser = null;
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_ACTIVE, true],
+ [PREF_DISPLAY, true],
+ ],
+ });
+ let url = HTTP_TEST_ROOT + "test_no_mcb_on_http_site_img.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ gTestBrowser = tab.linkedBrowser;
+});
+
+// ------------- TEST 1 -----------------------------------------
+
+add_task(async function test1() {
+ let expected =
+ "Verifying MCB does not trigger warning/error for an http page ";
+ expected += "with https css that includes http image";
+
+ await SpecialPowers.spawn(
+ gTestBrowser,
+ [expected],
+ async function (condition) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("testDiv").innerHTML == condition,
+ "Waited too long for status in Test 1!"
+ );
+ }
+ );
+
+ // Explicit OKs needed because the harness requires at least one call to ok.
+ ok(true, "test 1 passed");
+
+ // set up test 2
+ let url = HTTP_TEST_ROOT + "test_no_mcb_on_http_site_font.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+// ------------- TEST 2 -----------------------------------------
+
+add_task(async function test2() {
+ let expected =
+ "Verifying MCB does not trigger warning/error for an http page ";
+ expected += "with https css that includes http font";
+
+ await SpecialPowers.spawn(
+ gTestBrowser,
+ [expected],
+ async function (condition) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("testDiv").innerHTML == condition,
+ "Waited too long for status in Test 2!"
+ );
+ }
+ );
+
+ ok(true, "test 2 passed");
+
+ // set up test 3
+ let url = HTTP_TEST_ROOT + "test_no_mcb_on_http_site_font2.html";
+ BrowserTestUtils.loadURIString(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+// ------------- TEST 3 -----------------------------------------
+
+add_task(async function test3() {
+ let expected =
+ "Verifying MCB does not trigger warning/error for an http page ";
+ expected +=
+ "with https css that imports another http css which includes http font";
+
+ await SpecialPowers.spawn(
+ gTestBrowser,
+ [expected],
+ async function (condition) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("testDiv").innerHTML == condition,
+ "Waited too long for status in Test 3!"
+ );
+ }
+ );
+
+ ok(true, "test3 passed");
+});
+
+// ------------------------------------------------------
+
+add_task(async function cleanup() {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js b/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js
new file mode 100644
index 0000000000..1d282ef6de
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that an insecure resource routed over a secure transport is considered
+// insecure in terms of the site identity panel. We achieve this by running an
+// HTTP-over-TLS "proxy" and having Firefox request an http:// URI over it.
+
+/**
+ * Tests that the page info dialog "security" section labels a
+ * connection as unencrypted and does not show certificate.
+ * @param {string} uri - URI of the page to test with.
+ */
+async function testPageInfoNotEncrypted(uri) {
+ let pageInfo = BrowserPageInfo(uri, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let secLabel = pageInfoDoc.getElementById("security-technical-shortform");
+ await TestUtils.waitForCondition(
+ () => secLabel.value == "Connection Not Encrypted",
+ "pageInfo 'Security Details' should show not encrypted"
+ );
+
+ let viewCertBtn = pageInfoDoc.getElementById("security-view-cert");
+ ok(
+ viewCertBtn.collapsed,
+ "pageInfo 'View Cert' button should not be visible"
+ );
+ pageInfo.close();
+}
+
+// But first, a quick test that we don't incorrectly treat a
+// blob:https://example.com URI as secure.
+add_task(async function () {
+ let uri =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let debug = { hello: "world" };
+ let blob = new Blob([JSON.stringify(debug, null, 2)], {
+ type: "application/json",
+ });
+ let blobUri = URL.createObjectURL(blob);
+ content.document.location = blobUri;
+ });
+ await BrowserTestUtils.browserLoaded(browser);
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "localResource", "identity should be 'localResource'");
+ await testPageInfoNotEncrypted(uri);
+ });
+});
+
+// This server pretends to be a HTTP over TLS proxy. It isn't really, but this
+// is sufficient for the purposes of this test.
+function startServer(cert) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let input, output;
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ let connectionInfo = transport.securityCallbacks.getInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ connectionInfo.setSecurityObserver(listener);
+ input = transport.openInputStream(0, 0, 0);
+ output = transport.openOutputStream(0, 0, 0);
+ },
+
+ onHandshakeDone(socket, status) {
+ 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) {
+ info(e);
+ }
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ },
+
+ onStopListening() {
+ input.close();
+ output.close();
+ },
+ };
+
+ tlsServer.setSessionTickets(false);
+ tlsServer.asyncListen(listener);
+
+ return tlsServer;
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // This test fails on some platforms if we leave IPv6 enabled.
+ set: [["network.dns.disableIPv6", true]],
+ });
+
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+
+ let cert = getTestServerCertificate();
+ // Start the proxy and configure Firefox to trust its certificate.
+ let server = startServer(cert);
+ certOverrideService.rememberValidityOverride(
+ "localhost",
+ server.port,
+ {},
+ cert,
+ true
+ );
+ // Configure Firefox to use the proxy.
+ let systemProxySettings = {
+ QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]),
+ mainThreadOnly: true,
+ PACURI: null,
+ getProxyForURI: (aSpec, aScheme, aHost, aPort) => {
+ return `HTTPS localhost:${server.port}`;
+ },
+ };
+ let oldProxyType = Services.prefs.getIntPref("network.proxy.type");
+ Services.prefs.setIntPref(
+ "network.proxy.type",
+ Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM
+ );
+ let { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+ );
+ let mockProxy = MockRegistrar.register(
+ "@mozilla.org/system-proxy-settings;1",
+ systemProxySettings
+ );
+ // Register cleanup to undo the configuration changes we've made.
+ registerCleanupFunction(() => {
+ certOverrideService.clearValidityOverride("localhost", server.port, {});
+ Services.prefs.setIntPref("network.proxy.type", oldProxyType);
+ MockRegistrar.unregister(mockProxy);
+ server.close();
+ });
+
+ // Navigate to 'http://example.com'. Our proxy settings will route this via
+ // the "proxy" we just started. Even though our connection to the proxy is
+ // secure, in a real situation the connection from the proxy to
+ // http://example.com won't be secure, so we treat it as not secure.
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com/", async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "notSecure", "identity should be 'not secure'");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await testPageInfoNotEncrypted("http://example.com");
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_session_store_pageproxystate.js b/browser/base/content/test/siteIdentity/browser_session_store_pageproxystate.js
new file mode 100644
index 0000000000..5d8c011727
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_session_store_pageproxystate.js
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+let origBrowserState = SessionStore.getBrowserState();
+
+add_setup(async function () {
+ registerCleanupFunction(() => {
+ SessionStore.setBrowserState(origBrowserState);
+ });
+});
+
+// Test that when restoring tabs via SessionStore, we directly show the correct
+// security state.
+add_task(async function test_session_store_security_state() {
+ const state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "https://example.net", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "https://example.org", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ selected: 1,
+ },
+ ],
+ };
+
+ // Create a promise that resolves when the tabs have finished restoring.
+ let promiseTabsRestored = Promise.all([
+ TestUtils.topicObserved("sessionstore-browser-state-restored"),
+ BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"),
+ ]);
+
+ SessionStore.setBrowserState(JSON.stringify(state));
+
+ await promiseTabsRestored;
+
+ is(gBrowser.selectedTab, gBrowser.tabs[0], "First tab is selected initially");
+
+ info("Switch to second tab which has not been loaded yet.");
+ BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
+ is(
+ gURLBar.textbox.getAttribute("pageproxystate"),
+ "invalid",
+ "Page proxy state is invalid after tab switch"
+ );
+
+ // Wait for valid pageproxystate. As soon as we have a valid pageproxystate,
+ // showing the identity box, it should indicate a secure connection.
+ await BrowserTestUtils.waitForMutationCondition(
+ gURLBar.textbox,
+ {
+ attributeFilter: ["pageproxystate"],
+ },
+ () => gURLBar.textbox.getAttribute("pageproxystate") == "valid"
+ );
+
+ // Wait for a tick for security state to apply.
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ is(
+ gBrowser.currentURI.spec,
+ "https://example.org/",
+ "Should have loaded example.org"
+ );
+ is(
+ gIdentityHandler._identityBox.getAttribute("pageproxystate"),
+ "valid",
+ "identityBox pageproxystate is valid"
+ );
+
+ ok(
+ gIdentityHandler._isSecureConnection,
+ "gIdentityHandler._isSecureConnection is true"
+ );
+ is(
+ gIdentityHandler._identityBox.className,
+ "verifiedDomain",
+ "identityBox class signals secure connection."
+ );
+});
diff --git a/browser/base/content/test/siteIdentity/browser_tab_sharing_state.js b/browser/base/content/test/siteIdentity/browser_tab_sharing_state.js
new file mode 100644
index 0000000000..6c6ba57c55
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_tab_sharing_state.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests gBrowser#updateBrowserSharing
+ */
+add_task(async function testBrowserSharingStateSetter() {
+ const WEBRTC_TEST_STATE = {
+ camera: 0,
+ microphone: 1,
+ paused: false,
+ sharing: "microphone",
+ showMicrophoneIndicator: true,
+ showScreenSharingIndicator: "",
+ windowId: 0,
+ };
+
+ const WEBRTC_TEST_STATE2 = {
+ camera: 1,
+ microphone: 1,
+ paused: false,
+ sharing: "camera",
+ showCameraIndicator: true,
+ showMicrophoneIndicator: true,
+ showScreenSharingIndicator: "",
+ windowId: 1,
+ };
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ let tab = gBrowser.selectedTab;
+ is(tab._sharingState, undefined, "No sharing state initially.");
+ ok(!tab.hasAttribute("sharing"), "No tab sharing attribute initially.");
+
+ // Set an active sharing state for webrtc
+ gBrowser.updateBrowserSharing(browser, { webRTC: WEBRTC_TEST_STATE });
+ Assert.deepEqual(
+ tab._sharingState,
+ { webRTC: WEBRTC_TEST_STATE },
+ "Should have correct webRTC sharing state."
+ );
+ is(
+ tab.getAttribute("sharing"),
+ WEBRTC_TEST_STATE.sharing,
+ "Tab sharing attribute reflects webRTC sharing state."
+ );
+
+ // Set sharing state for geolocation
+ gBrowser.updateBrowserSharing(browser, { geo: true });
+ Assert.deepEqual(
+ tab._sharingState,
+ {
+ webRTC: WEBRTC_TEST_STATE,
+ geo: true,
+ },
+ "Should have sharing state for both webRTC and geolocation."
+ );
+ is(
+ tab.getAttribute("sharing"),
+ WEBRTC_TEST_STATE.sharing,
+ "Geolocation sharing doesn't update the tab sharing attribute."
+ );
+
+ // Update webRTC sharing state
+ gBrowser.updateBrowserSharing(browser, { webRTC: WEBRTC_TEST_STATE2 });
+ Assert.deepEqual(
+ tab._sharingState,
+ { geo: true, webRTC: WEBRTC_TEST_STATE2 },
+ "Should have updated webRTC sharing state while maintaining geolocation state."
+ );
+ is(
+ tab.getAttribute("sharing"),
+ WEBRTC_TEST_STATE2.sharing,
+ "Tab sharing attribute reflects webRTC sharing state."
+ );
+
+ // Clear webRTC sharing state
+ gBrowser.updateBrowserSharing(browser, { webRTC: null });
+ Assert.deepEqual(
+ tab._sharingState,
+ { geo: true, webRTC: null },
+ "Should only have sharing state for geolocation."
+ );
+ ok(
+ !tab.hasAttribute("sharing"),
+ "Ending webRTC sharing should remove tab sharing attribute."
+ );
+
+ // Clear geolocation sharing state
+ gBrowser.updateBrowserSharing(browser, { geo: null });
+ Assert.deepEqual(tab._sharingState, { geo: null, webRTC: null });
+ ok(
+ !tab.hasAttribute("sharing"),
+ "Tab sharing attribute should not be set."
+ );
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/dummy_iframe_page.html b/browser/base/content/test/siteIdentity/dummy_iframe_page.html
new file mode 100644
index 0000000000..ea80367aa5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/dummy_iframe_page.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+<title>Dummy iframe test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <iframe src="https://example.org"></iframe>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/dummy_page.html b/browser/base/content/test/siteIdentity/dummy_page.html
new file mode 100644
index 0000000000..a7747a0bca
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/dummy_page.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <a href="https://nocert.example.com" id="no-cert">No Cert page</a>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug1045809_1.html b/browser/base/content/test/siteIdentity/file_bug1045809_1.html
new file mode 100644
index 0000000000..c4f281d670
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug1045809_1.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ </head>
+ <body>
+ <iframe src="http://test1.example.com/browser/browser/base/content/test/siteIdentity/file_bug1045809_2.html"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug1045809_2.html b/browser/base/content/test/siteIdentity/file_bug1045809_2.html
new file mode 100644
index 0000000000..67a297dbc5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug1045809_2.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ </head>
+ <body>
+ <div id="mixedContentContainer">Mixed Content is here</div>
+ </body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_1.html b/browser/base/content/test/siteIdentity/file_bug822367_1.html
new file mode 100644
index 0000000000..a6e3fafc23
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 1 for Mixed Content Blocker User Override - Mixed Script
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/siteIdentity/file_bug822367_1.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_1.js b/browser/base/content/test/siteIdentity/file_bug822367_1.js
new file mode 100644
index 0000000000..e4b5fb86c6
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_1.js
@@ -0,0 +1 @@
+document.getElementById("p1").innerHTML = "hello";
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_2.html b/browser/base/content/test/siteIdentity/file_bug822367_2.html
new file mode 100644
index 0000000000..fe56ee2130
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_2.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 2 for Mixed Content Blocker User Override - Mixed Display
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 822367 - Mixed Display</title>
+</head>
+<body>
+ <div id="testContent">
+ <img src="http://example.com/tests/image/test/mochitest/blue.png">
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_3.html b/browser/base/content/test/siteIdentity/file_bug822367_3.html
new file mode 100644
index 0000000000..0cf5db7b20
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_3.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 3 for Mixed Content Blocker User Override - Mixed Script and Display
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 3 for Bug 822367</title>
+ <script>
+ function foo() {
+ var x = document.createElement("p");
+ x.setAttribute("id", "p2");
+ x.innerHTML = "bye";
+ document.getElementById("testContent").appendChild(x);
+ }
+ </script>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ <img src="http://example.com/tests/image/test/mochitest/blue.png" onload="foo()">
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/siteIdentity/file_bug822367_1.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_4.html b/browser/base/content/test/siteIdentity/file_bug822367_4.html
new file mode 100644
index 0000000000..8e5aeb67f2
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_4.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 4 for Mixed Content Blocker User Override - Mixed Script and Display
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 4 for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/siteIdentity/file_bug822367_4.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_4.js b/browser/base/content/test/siteIdentity/file_bug822367_4.js
new file mode 100644
index 0000000000..8bdc791180
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_4.js
@@ -0,0 +1,2 @@
+document.location =
+ "https://example.com/browser/browser/base/content/test/siteIdentity/file_bug822367_4B.html";
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_4B.html b/browser/base/content/test/siteIdentity/file_bug822367_4B.html
new file mode 100644
index 0000000000..9af942525f
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_4B.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 4B for Mixed Content Blocker User Override - Location Changed
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 4B Location Change for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/siteIdentity/file_bug822367_1.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_5.html b/browser/base/content/test/siteIdentity/file_bug822367_5.html
new file mode 100644
index 0000000000..6341539e83
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_5.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 5 for Mixed Content Blocker User Override - Mixed Script in document.open()
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 5 for Bug 822367</title>
+ <script>
+ function createDoc() {
+ var doc = document.open("text/html", "replace");
+ doc.write('<!DOCTYPE html><html><body><p id="p1">This is some content</p><script src="http://example.com/browser/browser/base/content/test/siteIdentity/file_bug822367_1.js">\<\/script\>\<\/body>\<\/html>');
+ doc.close();
+ }
+ </script>
+</head>
+<body>
+ <div id="testContent">
+ <img src="https://example.com/tests/image/test/mochitest/blue.png" onload="createDoc()">
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug822367_6.html b/browser/base/content/test/siteIdentity/file_bug822367_6.html
new file mode 100644
index 0000000000..2c071a785d
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug822367_6.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 6 for Mixed Content Blocker User Override - Mixed Script in document.open() within an iframe
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 6 for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <iframe name="f1" id="f1" src="https://example.com/browser/browser/base/content/test/siteIdentity/file_bug822367_5.html"></iframe>
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug902156.js b/browser/base/content/test/siteIdentity/file_bug902156.js
new file mode 100644
index 0000000000..01ef4073fb
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug902156.js
@@ -0,0 +1,6 @@
+/*
+ * Once the mixed content blocker is disabled for the page, this scripts loads
+ * and updates the text inside the div container.
+ */
+document.getElementById("mctestdiv").innerHTML =
+ "Mixed Content Blocker disabled";
diff --git a/browser/base/content/test/siteIdentity/file_bug902156_1.html b/browser/base/content/test/siteIdentity/file_bug902156_1.html
new file mode 100644
index 0000000000..4cac7cfb93
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug902156_1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 902156 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=902156
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 902156</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/siteIdentity/file_bug902156.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug902156_2.html b/browser/base/content/test/siteIdentity/file_bug902156_2.html
new file mode 100644
index 0000000000..c815a09a93
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug902156_2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 2 for Bug 902156 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=902156
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 902156</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <a href="https://test2.example.com/browser/browser/base/content/test/siteIdentity/file_bug902156_1.html"
+ id="mctestlink" target="_top">Go to http site</a>
+ <script src="http://test2.example.com/browser/browser/base/content/test/siteIdentity/file_bug902156.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug902156_3.html b/browser/base/content/test/siteIdentity/file_bug902156_3.html
new file mode 100644
index 0000000000..7a26f4b0f0
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug902156_3.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3 for Bug 902156 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=902156
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 3 for Bug 902156</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/siteIdentity/file_bug902156.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug906190.js b/browser/base/content/test/siteIdentity/file_bug906190.js
new file mode 100644
index 0000000000..01ef4073fb
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug906190.js
@@ -0,0 +1,6 @@
+/*
+ * Once the mixed content blocker is disabled for the page, this scripts loads
+ * and updates the text inside the div container.
+ */
+document.getElementById("mctestdiv").innerHTML =
+ "Mixed Content Blocker disabled";
diff --git a/browser/base/content/test/siteIdentity/file_bug906190.sjs b/browser/base/content/test/siteIdentity/file_bug906190.sjs
new file mode 100644
index 0000000000..088153d671
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug906190.sjs
@@ -0,0 +1,18 @@
+function handleRequest(request, response) {
+ var page = "<!DOCTYPE html><html><body>bug 906190</body></html>";
+ var path =
+ "https://test1.example.com/browser/browser/base/content/test/siteIdentity/";
+ var url;
+
+ if (request.queryString.includes("bad-redirection=1")) {
+ url = path + "this_page_does_not_exist.html";
+ } else {
+ url = path + "file_bug906190_redirected.html";
+ }
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "302", "Found");
+ response.setHeader("Location", url, false);
+ response.write(page);
+}
diff --git a/browser/base/content/test/siteIdentity/file_bug906190_1.html b/browser/base/content/test/siteIdentity/file_bug906190_1.html
new file mode 100644
index 0000000000..031c229f0d
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug906190_1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 906190</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/siteIdentity/file_bug906190.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug906190_2.html b/browser/base/content/test/siteIdentity/file_bug906190_2.html
new file mode 100644
index 0000000000..2a7546dca4
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug906190_2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 2 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 906190</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test2.example.com/browser/browser/base/content/test/siteIdentity/file_bug906190.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug906190_3_4.html b/browser/base/content/test/siteIdentity/file_bug906190_3_4.html
new file mode 100644
index 0000000000..e78e271f85
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug906190_3_4.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3 and 4 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="refresh" content="0; url=https://test1.example.com/browser/browser/base/content/test/siteIdentity/file_bug906190_redirected.html">
+ <title>Test 3 and 4 for Bug 906190</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_bug906190_redirected.html b/browser/base/content/test/siteIdentity/file_bug906190_redirected.html
new file mode 100644
index 0000000000..d0bc4a39f5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug906190_redirected.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Redirected Page of Test 3 to 6 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Redirected Page for Bug 906190</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/siteIdentity/file_bug906190.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.html b/browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.html
new file mode 100644
index 0000000000..b5463d8d5b
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>
+ <meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
+</head>
+<body>
+ <script src="http://example.com/browser/browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.js"></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.js b/browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.js
new file mode 100644
index 0000000000..dc6d6a64e4
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_csp_block_all_mixedcontent.js
@@ -0,0 +1,3 @@
+// empty script file just used for testing Bug 1122236.
+// Making sure the UI is not degraded when blocking
+// mixed content using the CSP directive: block-all-mixed-content.
diff --git a/browser/base/content/test/siteIdentity/file_mixedContentFramesOnHttp.html b/browser/base/content/test/siteIdentity/file_mixedContentFramesOnHttp.html
new file mode 100644
index 0000000000..3ed5b82641
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_mixedContentFramesOnHttp.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for https://bugzilla.mozilla.org/show_bug.cgi?id=1182551
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1182551</title>
+</head>
+<body>
+ <p>Test for Bug 1182551. This is an HTTP top level page. We include an HTTPS iframe that loads mixed passive content.</p>
+ <iframe src="https://example.org/browser/browser/base/content/test/siteIdentity/file_mixedPassiveContent.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload.html b/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload.html
new file mode 100644
index 0000000000..ae134f8cb0
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for https://bugzilla.mozilla.org/show_bug.cgi?id=947079
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 947079</title>
+</head>
+<body>
+ <p>Test for Bug 947079</p>
+ <script>
+ window.addEventListener("unload", function() {
+ new Image().src = "http://mochi.test:8888/tests/image/test/mochitest/blue.png";
+ });
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test1.html b/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test1.html
new file mode 100644
index 0000000000..1d027b0362
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 1 for https://bugzilla.mozilla.org/show_bug.cgi?id=947079
+Page with no insecure subresources
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 947079</title>
+</head>
+<body>
+ <p>There are no insecure resource loads on this page</p>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test2.html b/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test2.html
new file mode 100644
index 0000000000..4813337cc8
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_mixedContentFromOnunload_test2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 2 for https://bugzilla.mozilla.org/show_bug.cgi?id=947079
+Page with an insecure image load
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 947079</title>
+</head>
+<body>
+ <p>Page with http image load</p>
+ <img src="http://test2.example.com/tests/image/test/mochitest/blue.png">
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_mixedPassiveContent.html b/browser/base/content/test/siteIdentity/file_mixedPassiveContent.html
new file mode 100644
index 0000000000..a60ac94e8b
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_mixedPassiveContent.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for https://bugzilla.mozilla.org/show_bug.cgi?id=1182551
+-->
+<head>
+ <meta charset="utf-8">
+ <title>HTTPS page with HTTP image</title>
+</head>
+<body>
+ <img src="http://mochi.test:8888/tests/image/test/mochitest/blue.png">
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/file_pdf.pdf b/browser/base/content/test/siteIdentity/file_pdf.pdf
new file mode 100644
index 0000000000..593558f9a4
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_pdf.pdf
@@ -0,0 +1,12 @@
+%PDF-1.0
+1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
+xref
+0 4
+0000000000 65535 f
+0000000010 00000 n
+0000000053 00000 n
+0000000102 00000 n
+trailer<</Size 4/Root 1 0 R>>
+startxref
+149
+%EOF \ No newline at end of file
diff --git a/browser/base/content/test/siteIdentity/file_pdf_blob.html b/browser/base/content/test/siteIdentity/file_pdf_blob.html
new file mode 100644
index 0000000000..ff6ed659a2
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_pdf_blob.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset='utf-8'>
+</head>
+<body>
+ <script>
+ let blob = new Blob(["x"], { type: "application/pdf" });
+ let blobURL = URL.createObjectURL(blob);
+
+ let link = document.createElement("a");
+ link.innerText = "PDF blob";
+ link.target = "_blank";
+ link.href = blobURL;
+ document.body.appendChild(link);
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/head.js b/browser/base/content/test/siteIdentity/head.js
new file mode 100644
index 0000000000..d2a588a815
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/head.js
@@ -0,0 +1,435 @@
+function openIdentityPopup() {
+ gIdentityHandler._initializePopup();
+ let mainView = document.getElementById("identity-popup-mainView");
+ let viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ gIdentityHandler._identityIconBox.click();
+ return viewShown;
+}
+
+function openPermissionPopup() {
+ gPermissionPanel._initializePopup();
+ let mainView = document.getElementById("permission-popup-mainView");
+ let viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ gPermissionPanel.openPopup();
+ return viewShown;
+}
+
+function getIdentityMode(aWindow = window) {
+ return aWindow.document.getElementById("identity-box").className;
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url) {
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+// Compares the security state of the page with what is expected
+function isSecurityState(browser, expectedState) {
+ let ui = browser.securityUI;
+ if (!ui) {
+ ok(false, "No security UI to get the security state");
+ return;
+ }
+
+ const wpl = Ci.nsIWebProgressListener;
+
+ // determine the security state
+ let isSecure = ui.state & wpl.STATE_IS_SECURE;
+ let isBroken = ui.state & wpl.STATE_IS_BROKEN;
+ let isInsecure = ui.state & wpl.STATE_IS_INSECURE;
+
+ let actualState;
+ if (isSecure && !(isBroken || isInsecure)) {
+ actualState = "secure";
+ } else if (isBroken && !(isSecure || isInsecure)) {
+ actualState = "broken";
+ } else if (isInsecure && !(isSecure || isBroken)) {
+ actualState = "insecure";
+ } else {
+ actualState = "unknown";
+ }
+
+ is(
+ expectedState,
+ actualState,
+ "Expected state " +
+ expectedState +
+ " and the actual state is " +
+ actualState +
+ "."
+ );
+}
+
+/**
+ * Test the state of the identity box and control center to make
+ * sure they are correctly showing the expected mixed content states.
+ *
+ * @note The checks are done synchronously, but new code should wait on the
+ * returned Promise object to ensure the identity panel has closed.
+ * Bug 1221114 is filed to fix the existing code.
+ *
+ * @param tabbrowser
+ * @param Object states
+ * MUST include the following properties:
+ * {
+ * activeLoaded: true|false,
+ * activeBlocked: true|false,
+ * passiveLoaded: true|false,
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished and the identity panel has closed.
+ */
+async function assertMixedContentBlockingState(tabbrowser, states = {}) {
+ if (
+ !tabbrowser ||
+ !("activeLoaded" in states) ||
+ !("activeBlocked" in states) ||
+ !("passiveLoaded" in states)
+ ) {
+ throw new Error(
+ "assertMixedContentBlockingState requires a browser and a states object"
+ );
+ }
+
+ let { passiveLoaded, activeLoaded, activeBlocked } = states;
+ let { gIdentityHandler } = tabbrowser.ownerGlobal;
+ let doc = tabbrowser.ownerDocument;
+ let identityBox = gIdentityHandler._identityBox;
+ let classList = identityBox.classList;
+ let identityIcon = doc.getElementById("identity-icon");
+ let identityIconImage = tabbrowser.ownerGlobal
+ .getComputedStyle(identityIcon)
+ .getPropertyValue("list-style-image");
+
+ let stateSecure =
+ gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
+ let stateBroken =
+ gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
+ let stateInsecure =
+ gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_INSECURE;
+ let stateActiveBlocked =
+ gIdentityHandler._state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
+ let stateActiveLoaded =
+ gIdentityHandler._state &
+ Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
+ let statePassiveLoaded =
+ gIdentityHandler._state &
+ Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
+
+ is(
+ activeBlocked,
+ !!stateActiveBlocked,
+ "Expected state for activeBlocked matches UI state"
+ );
+ is(
+ activeLoaded,
+ !!stateActiveLoaded,
+ "Expected state for activeLoaded matches UI state"
+ );
+ is(
+ passiveLoaded,
+ !!statePassiveLoaded,
+ "Expected state for passiveLoaded matches UI state"
+ );
+
+ if (stateInsecure) {
+ const insecureConnectionIcon = Services.prefs.getBoolPref(
+ "security.insecure_connection_icon.enabled"
+ );
+ if (!insecureConnectionIcon) {
+ // HTTP request, there should be no MCB classes for the identity box and the non secure icon
+ // should always be visible regardless of MCB state.
+ ok(classList.contains("unknownIdentity"), "unknownIdentity on HTTP page");
+ ok(
+ BrowserTestUtils.is_visible(identityIcon),
+ "information icon should be still visible"
+ );
+ } else {
+ // HTTP request, there should be a broken padlock shown always.
+ ok(classList.contains("notSecure"), "notSecure on HTTP page");
+ ok(
+ !BrowserTestUtils.is_hidden(identityIcon),
+ "information icon should be visible"
+ );
+ }
+
+ ok(!classList.contains("mixedActiveContent"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedActiveBlocked"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedDisplayContent"), "No MCB icon on HTTP page");
+ ok(
+ !classList.contains("mixedDisplayContentLoadedActiveBlocked"),
+ "No MCB icon on HTTP page"
+ );
+ } else {
+ // Make sure the identity box UI has the correct mixedcontent states and icons
+ is(
+ classList.contains("mixedActiveContent"),
+ activeLoaded,
+ "identityBox has expected class for activeLoaded"
+ );
+ is(
+ classList.contains("mixedActiveBlocked"),
+ activeBlocked && !passiveLoaded,
+ "identityBox has expected class for activeBlocked && !passiveLoaded"
+ );
+ is(
+ classList.contains("mixedDisplayContent"),
+ passiveLoaded && !(activeLoaded || activeBlocked),
+ "identityBox has expected class for passiveLoaded && !(activeLoaded || activeBlocked)"
+ );
+ is(
+ classList.contains("mixedDisplayContentLoadedActiveBlocked"),
+ passiveLoaded && activeBlocked,
+ "identityBox has expected class for passiveLoaded && activeBlocked"
+ );
+
+ ok(
+ !BrowserTestUtils.is_hidden(identityIcon),
+ "information icon should be visible"
+ );
+ if (activeLoaded) {
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/security-broken.svg")',
+ "Using active loaded icon"
+ );
+ }
+ if (activeBlocked && !passiveLoaded) {
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/security.svg")',
+ "Using active blocked icon"
+ );
+ }
+ if (passiveLoaded && !(activeLoaded || activeBlocked)) {
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/security-warning.svg")',
+ "Using passive loaded icon"
+ );
+ }
+ if (passiveLoaded && activeBlocked) {
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/security-warning.svg")',
+ "Using active blocked and passive loaded icon"
+ );
+ }
+ }
+
+ // Make sure the identity popup has the correct mixedcontent states
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ tabbrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ await promisePanelOpen;
+ let popupAttr = doc
+ .getElementById("identity-popup")
+ .getAttribute("mixedcontent");
+ let bodyAttr = doc
+ .getElementById("identity-popup-securityView-extended-info")
+ .getAttribute("mixedcontent");
+
+ is(
+ popupAttr.includes("active-loaded"),
+ activeLoaded,
+ "identity-popup has expected attr for activeLoaded"
+ );
+ is(
+ bodyAttr.includes("active-loaded"),
+ activeLoaded,
+ "securityView-body has expected attr for activeLoaded"
+ );
+
+ is(
+ popupAttr.includes("active-blocked"),
+ activeBlocked,
+ "identity-popup has expected attr for activeBlocked"
+ );
+ is(
+ bodyAttr.includes("active-blocked"),
+ activeBlocked,
+ "securityView-body has expected attr for activeBlocked"
+ );
+
+ is(
+ popupAttr.includes("passive-loaded"),
+ passiveLoaded,
+ "identity-popup has expected attr for passiveLoaded"
+ );
+ is(
+ bodyAttr.includes("passive-loaded"),
+ passiveLoaded,
+ "securityView-body has expected attr for passiveLoaded"
+ );
+
+ // Make sure the correct icon is visible in the Control Center.
+ // This logic is controlled with CSS, so this helps prevent regressions there.
+ let securityViewBG = tabbrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-securityView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("list-style-image");
+ let securityContentBG = tabbrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-mainView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("list-style-image");
+
+ if (stateInsecure) {
+ is(
+ securityViewBG,
+ 'url("chrome://global/skin/icons/security-broken.svg")',
+ "CC using 'not secure' icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/security-broken.svg")',
+ "CC using 'not secure' icon"
+ );
+ }
+
+ if (stateSecure) {
+ is(
+ securityViewBG,
+ 'url("chrome://global/skin/icons/security.svg")',
+ "CC using secure icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/security.svg")',
+ "CC using secure icon"
+ );
+ }
+
+ if (stateBroken) {
+ if (activeLoaded) {
+ is(
+ securityViewBG,
+ 'url("chrome://browser/skin/controlcenter/mcb-disabled.svg")',
+ "CC using active loaded icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://browser/skin/controlcenter/mcb-disabled.svg")',
+ "CC using active loaded icon"
+ );
+ } else if (activeBlocked || passiveLoaded) {
+ is(
+ securityViewBG,
+ 'url("chrome://global/skin/icons/security-warning.svg")',
+ "CC using degraded icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/security-warning.svg")',
+ "CC using degraded icon"
+ );
+ } else {
+ // There is a case here with weak ciphers, but no bc tests are handling this yet.
+ is(
+ securityViewBG,
+ 'url("chrome://global/skin/icons/security.svg")',
+ "CC using degraded icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/security.svg")',
+ "CC using degraded icon"
+ );
+ }
+ }
+
+ if (activeLoaded || activeBlocked || passiveLoaded) {
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "ViewShown"
+ );
+ doc.getElementById("identity-popup-security-button").click();
+ await promiseViewShown;
+ is(
+ Array.prototype.filter.call(
+ doc
+ .getElementById("identity-popup-securityView")
+ .querySelectorAll(".identity-popup-mcb-learn-more"),
+ element => !BrowserTestUtils.is_hidden(element)
+ ).length,
+ 1,
+ "The 'Learn more' link should be visible once."
+ );
+ }
+
+ if (gIdentityHandler._identityPopup.state != "closed") {
+ let hideEvent = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ info("Hiding identity popup");
+ gIdentityHandler._identityPopup.hidePopup();
+ await hideEvent;
+ }
+}
+
+async function loadBadCertPage(url) {
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await loaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.getElementById("exceptionDialogButton").click();
+ });
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+}
+
+// nsITLSServerSocket needs a certificate with a corresponding private key
+// available. In mochitests, the certificate with the common name "Mochitest
+// client" has such a key.
+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;
+}
diff --git a/browser/base/content/test/siteIdentity/iframe_navigation.html b/browser/base/content/test/siteIdentity/iframe_navigation.html
new file mode 100644
index 0000000000..d4564569e7
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/iframe_navigation.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+<meta charset="UTF-8">
+</head>
+<body class="running">
+ <script>
+ window.addEventListener("message", doNavigation);
+
+ function doNavigation() {
+ let destination;
+ let destinationIdentifier = window.location.hash.substring(1);
+ switch (destinationIdentifier) {
+ case "blank":
+ destination = "about:blank";
+ break;
+ case "secure":
+ destination =
+ "https://example.com/browser/browser/base/content/test/siteIdentity/dummy_page.html";
+ break;
+ case "insecure":
+ destination =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/siteIdentity/dummy_page.html";
+ break;
+ }
+ setTimeout(() => {
+ let frame = document.getElementById("navigateMe");
+ frame.onload = done;
+ frame.onerror = done;
+ frame.src = destination;
+ }, 0);
+ }
+
+ function done() {
+ document.body.classList.toggle("running");
+ }
+ </script>
+ <iframe id="navigateMe" src="dummy_page.html">
+ </iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/insecure_opener.html b/browser/base/content/test/siteIdentity/insecure_opener.html
new file mode 100644
index 0000000000..26ed014f63
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/insecure_opener.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <a id="link" target="_blank" href="https://example.com/browser/toolkit/components/passwordmgr/test/browser/form_basic.html">Click me, I'm "secure".</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/open-self-from-frame.html b/browser/base/content/test/siteIdentity/open-self-from-frame.html
new file mode 100644
index 0000000000..17d0cf56ef
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/open-self-from-frame.html
@@ -0,0 +1,6 @@
+<iframe src="about:blank"></iframe>
+<script>
+ document.querySelector("iframe").contentDocument.write(
+ `<button onclick="window.open().document.write('Hi')">click me!</button>`
+ );
+</script>
diff --git a/browser/base/content/test/siteIdentity/simple_mixed_passive.html b/browser/base/content/test/siteIdentity/simple_mixed_passive.html
new file mode 100644
index 0000000000..2e4cda790a
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/simple_mixed_passive.html
@@ -0,0 +1 @@
+<img src="http://example.com/browser/browser/base/content/test/siteIdentity/moz.png">
diff --git a/browser/base/content/test/siteIdentity/test-mixedcontent-securityerrors.html b/browser/base/content/test/siteIdentity/test-mixedcontent-securityerrors.html
new file mode 100644
index 0000000000..cb8cfdaaf5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test-mixedcontent-securityerrors.html
@@ -0,0 +1,21 @@
+<!--
+ Bug 875456 - Log mixed content messages from the Mixed Content Blocker to the
+ Security Pane in the Web Console
+-->
+
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <title>Mixed Content test - http on https</title>
+ <script src="testscript.js"></script>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <iframe src="http://example.com"></iframe>
+ <img src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+ </body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_mcb_double_redirect_image.html b/browser/base/content/test/siteIdentity/test_mcb_double_redirect_image.html
new file mode 100644
index 0000000000..adadf01944
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_mcb_double_redirect_image.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 7-9 for Bug 1082837 - See file browser_mcb_redirect.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1082837
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1082837</title>
+ <script>
+ function image_loaded() {
+ document.getElementById("mctestdiv").innerHTML = "image loaded";
+ }
+ function image_blocked() {
+ document.getElementById("mctestdiv").innerHTML = "image blocked";
+ }
+ </script>
+</head>
+<body>
+ <div id="mctestdiv"></div>
+ <img src="https://example.com/browser/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs?image_redirect_http_sjs" onload="image_loaded()" onerror="image_blocked()" ></image>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_mcb_redirect.html b/browser/base/content/test/siteIdentity/test_mcb_redirect.html
new file mode 100644
index 0000000000..fc7ccc2764
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_mcb_redirect.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 418354 - See file browser_mcb_redirect.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=418354
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 418354</title>
+</head>
+<body>
+ <div id="mctestdiv">script blocked</div>
+ <script src="https://example.com/browser/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs?script" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_mcb_redirect.js b/browser/base/content/test/siteIdentity/test_mcb_redirect.js
new file mode 100644
index 0000000000..48538c9409
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_mcb_redirect.js
@@ -0,0 +1,5 @@
+/*
+ * Once the mixed content blocker is disabled for the page, this scripts loads
+ * and updates the text inside the div container.
+ */
+document.getElementById("mctestdiv").innerHTML = "script executed";
diff --git a/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs b/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs
new file mode 100644
index 0000000000..53b8cf2b08
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs
@@ -0,0 +1,29 @@
+function handleRequest(request, response) {
+ var page =
+ "<!DOCTYPE html><html><body>bug 418354 and bug 1082837</body></html>";
+
+ let redirect;
+ if (request.queryString === "script") {
+ redirect =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/siteIdentity/test_mcb_redirect.js";
+ response.setHeader("Cache-Control", "no-cache", false);
+ } else if (request.queryString === "image_http") {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ redirect = "http://example.com/tests/image/test/mochitest/blue.png";
+ response.setHeader("Cache-Control", "max-age=3600", false);
+ } else if (request.queryString === "image_redirect_http_sjs") {
+ redirect =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs?image_redirect_https";
+ response.setHeader("Cache-Control", "max-age=3600", false);
+ } else if (request.queryString === "image_redirect_https") {
+ redirect = "https://example.com/tests/image/test/mochitest/blue.png";
+ response.setHeader("Cache-Control", "max-age=3600", false);
+ }
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "302", "Found");
+ response.setHeader("Location", redirect, false);
+ response.write(page);
+}
diff --git a/browser/base/content/test/siteIdentity/test_mcb_redirect_image.html b/browser/base/content/test/siteIdentity/test_mcb_redirect_image.html
new file mode 100644
index 0000000000..42da0d7c13
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_mcb_redirect_image.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3-6 for Bug 1082837 - See file browser_mcb_redirect.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1082837
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1082837</title>
+ <script>
+ function image_loaded() {
+ document.getElementById("mctestdiv").innerHTML = "image loaded";
+ }
+ function image_blocked() {
+ document.getElementById("mctestdiv").innerHTML = "image blocked";
+ }
+ </script>
+</head>
+<body>
+ <div id="mctestdiv"></div>
+ <img src="https://example.com/browser/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs?image_http" onload="image_loaded()" onerror="image_blocked()" ></image>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_for_loopback.html b/browser/base/content/test/siteIdentity/test_no_mcb_for_loopback.html
new file mode 100644
index 0000000000..34193d370b
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_for_loopback.html
@@ -0,0 +1,56 @@
+<!-- See browser_no_mcb_for_localhost.js -->
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 903966, Bug 1402530</title>
+ </head>
+
+ <style>
+ @font-face {
+ font-family: "Font-IPv4";
+ src: url("http://127.0.0.1:8/test.ttf");
+ }
+
+ @font-face {
+ font-family: "Font-IPv6";
+ src: url("http://[::1]:8/test.ttf");
+ }
+
+ #ip-v4 {
+ font-family: "Font-IPv4"
+ }
+
+ #ip-v6 {
+ font-family: "Font-IPv6"
+ }
+ </style>
+
+ <body>
+ <div id="ip-v4">test</div>
+ <div id="ip-v6">test</div>
+
+ <img src="http://127.0.0.1:8/test.png">
+ <img src="http://[::1]:8/test.png">
+ <img src="http://localhost:8/test.png">
+
+ <iframe src="http://127.0.0.1:8/test.html"></iframe>
+ <iframe src="http://[::1]:8/test.html"></iframe>
+ <iframe src="http://localhost:8/test.html"></iframe>
+ </body>
+
+ <script src="http://127.0.0.1:8/test.js"></script>
+ <script src="http://[::1]:8/test.js"></script>
+ <script src="http://localhost:8/test.js"></script>
+
+ <link href="http://127.0.0.1:8/test.css" rel="stylesheet"></link>
+ <link href="http://[::1]:8/test.css" rel="stylesheet"></link>
+ <link href="http://localhost:8/test.css" rel="stylesheet"></link>
+
+ <script>
+ fetch("http://127.0.0.1:8");
+ fetch("http://localhost:8");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ fetch("http://[::1]:8");
+ </script>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_for_onions.html b/browser/base/content/test/siteIdentity/test_no_mcb_for_onions.html
new file mode 100644
index 0000000000..c73c3681a3
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_for_onions.html
@@ -0,0 +1,29 @@
+<!-- See browser_no_mcb_for_onions.js -->
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 1382359</title>
+ </head>
+
+ <style>
+ @font-face {
+ src: url("http://123456789abcdef.onion:8/test.ttf");
+ }
+ </style>
+
+ <body>
+ <img src="http://123456789abcdef.onion:8/test.png">
+
+ <iframe src="http://123456789abcdef.onion:8/test.html"></iframe>
+ </body>
+
+ <script src="http://123456789abcdef.onion:8/test.js"></script>
+
+ <link href="http://123456789abcdef.onion:8/test.css" rel="stylesheet"></link>
+
+ <script>
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ fetch("http://123456789abcdef.onion:8");
+ </script>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css
new file mode 100644
index 0000000000..0587ef7939
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css
@@ -0,0 +1,11 @@
+@font-face {
+ font-family: testFont;
+ src: url(http://example.com/browser/devtools/client/fontinspector/test/browser_font.woff);
+}
+/* stylelint-disable font-family-no-missing-generic-family-keyword */
+body {
+ font-family: Arial;
+}
+div {
+ font-family: testFont;
+}
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html
new file mode 100644
index 0000000000..7b39be064c
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 2 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ async function checkLoadStates() {
+ let state = await SpecialPowers.getSecurityState(window);
+
+ var loadedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http font";
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that includes http font
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css
new file mode 100644
index 0000000000..3ac6c87a6b
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css
@@ -0,0 +1 @@
+@import url(http://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css);
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html
new file mode 100644
index 0000000000..3da31592dd
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 3 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ async function checkLoadStates() {
+ let state = await SpecialPowers.getSecurityState(window);
+
+ var loadedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page ";
+ newValue += "with https css that imports another http css which includes http font";
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that imports another http css which includes http font
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css
new file mode 100644
index 0000000000..d045e21ba0
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css
@@ -0,0 +1,3 @@
+#testDiv {
+ background: url(http://example.com/tests/image/test/mochitest/blue.png)
+}
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html
new file mode 100644
index 0000000000..10aa281959
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ async function checkLoadStates() {
+ let state = await SpecialPowers.getSecurityState(window);
+
+ var loadedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http image";
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that includes http image
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/startup/browser.ini b/browser/base/content/test/startup/browser.ini
new file mode 100644
index 0000000000..00ee1d9ed2
--- /dev/null
+++ b/browser/base/content/test/startup/browser.ini
@@ -0,0 +1,2 @@
+[browser_preXULSkeletonUIRegistry.js]
+skip-if = !(os == 'win' && os_version == '10.0') # We only enable the skele UI on Win10 \ No newline at end of file
diff --git a/browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js b/browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js
new file mode 100644
index 0000000000..7b96764eba
--- /dev/null
+++ b/browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js
@@ -0,0 +1,136 @@
+ChromeUtils.defineESModuleGetters(this, {
+ WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
+});
+
+function getFirefoxExecutableFile() {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+
+ file.append(AppConstants.MOZ_APP_NAME + ".exe");
+ return file;
+}
+
+// This is copied from WindowsRegistry.sys.mjs, but extended to support
+// TYPE_BINARY, as that is how we represent doubles in the registry for
+// the skeleton UI. However, we didn't extend WindowsRegistry.sys.mjs itself,
+// because TYPE_BINARY is kind of a footgun for javascript callers - our
+// use case is just trivial (checking that the value is non-zero).
+function readRegKeyExtended(aRoot, aPath, aKey, aRegistryNode = 0) {
+ const kRegMultiSz = 7;
+ const kMode = Ci.nsIWindowsRegKey.ACCESS_READ | aRegistryNode;
+ let registry = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ try {
+ registry.open(aRoot, aPath, kMode);
+ if (registry.hasValue(aKey)) {
+ let type = registry.getValueType(aKey);
+ switch (type) {
+ case kRegMultiSz:
+ // nsIWindowsRegKey doesn't support REG_MULTI_SZ type out of the box.
+ let str = registry.readStringValue(aKey);
+ return str.split("\0").filter(v => v);
+ case Ci.nsIWindowsRegKey.TYPE_STRING:
+ return registry.readStringValue(aKey);
+ case Ci.nsIWindowsRegKey.TYPE_INT:
+ return registry.readIntValue(aKey);
+ case Ci.nsIWindowsRegKey.TYPE_BINARY:
+ return registry.readBinaryValue(aKey);
+ default:
+ throw new Error("Unsupported registry value.");
+ }
+ }
+ } catch (ex) {
+ } finally {
+ registry.close();
+ }
+ return undefined;
+}
+
+add_task(async function testWritesEnabledOnPrefChange() {
+ Services.prefs.setBoolPref("browser.startup.preXulSkeletonUI", true);
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ const firefoxPath = getFirefoxExecutableFile().path;
+ let enabled = WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox\\PreXULSkeletonUISettings",
+ `${firefoxPath}|Enabled`
+ );
+ is(enabled, 1, "Pre-XUL skeleton UI is enabled in the Windows registry");
+
+ Services.prefs.setBoolPref("browser.startup.preXulSkeletonUI", false);
+ enabled = WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox\\PreXULSkeletonUISettings",
+ `${firefoxPath}|Enabled`
+ );
+ is(enabled, 0, "Pre-XUL skeleton UI is disabled in the Windows registry");
+
+ Services.prefs.setBoolPref("browser.startup.preXulSkeletonUI", true);
+ Services.prefs.setIntPref("browser.tabs.inTitlebar", 0);
+ enabled = WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox\\PreXULSkeletonUISettings",
+ `${firefoxPath}|Enabled`
+ );
+ is(enabled, 0, "Pre-XUL skeleton UI is disabled in the Windows registry");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function testPersistsNecessaryValuesOnChange() {
+ // Enable the skeleton UI, since if it's disabled we won't persist the size values
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.startup.preXulSkeletonUI", true]],
+ });
+
+ const regKeys = [
+ "Width",
+ "Height",
+ "ScreenX",
+ "ScreenY",
+ "UrlbarCSSSpan",
+ "CssToDevPixelScaling",
+ "SpringsCSSSpan",
+ "SearchbarCSSSpan",
+ "Theme",
+ "Flags",
+ "Progress",
+ ];
+
+ // Remove all of the registry values to ensure old tests aren't giving us false
+ // positives
+ for (let key of regKeys) {
+ WindowsRegistry.removeRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox\\PreXULSkeletonUISettings",
+ key
+ );
+ }
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const firefoxPath = getFirefoxExecutableFile().path;
+ for (let key of regKeys) {
+ let value = readRegKeyExtended(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox\\PreXULSkeletonUISettings",
+ `${firefoxPath}|${key}`
+ );
+ isnot(
+ typeof value,
+ "undefined",
+ `Skeleton UI registry values should have a defined value for ${key}`
+ );
+ if (value.length) {
+ let hasNonZero = false;
+ for (var i = 0; i < value.length; i++) {
+ hasNonZero = hasNonZero || value[i];
+ }
+ ok(hasNonZero, `Value should have non-zero components for ${key}`);
+ }
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/static/browser.ini b/browser/base/content/test/static/browser.ini
new file mode 100644
index 0000000000..69f2a71723
--- /dev/null
+++ b/browser/base/content/test/static/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+# These tests can be prone to intermittent failures on slower systems.
+# Since the specific flavor doesn't matter from a correctness standpoint,
+# just skip the tests on sanitizer, debug and OS X verify builds.
+skip-if = (asan || tsan || debug || (verify && os == 'mac'))
+support-files =
+ head.js
+
+[browser_all_files_referenced.js]
+skip-if = verify && bits == 32 # Causes OOMs when run repeatedly
+[browser_misused_characters_in_strings.js]
+support-files =
+ bug1262648_string_with_newlines.dtd
+skip-if = os == 'win' && msix # Permafail on MSIX packages due to it running on files it shouldn't.
+[browser_parsable_css.js]
+support-files =
+ dummy_page.html
+skip-if = os == 'win' && msix # Permafail on MSIX packages due to it running on files it shouldn't.
+[browser_parsable_script.js]
+skip-if = ccov && os == 'linux' # https://bugzilla.mozilla.org/show_bug.cgi?id=1608081
+[browser_sentence_case_strings.js]
+[browser_title_case_menus.js]
diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js
new file mode 100644
index 0000000000..df8a1997a7
--- /dev/null
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -0,0 +1,1093 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Note to run this test similar to try server, you need to run:
+// ./mach package
+// ./mach mochitest --appname dist <path to test>
+
+// Slow on asan builds.
+requestLongerTimeout(5);
+
+var isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
+
+// This list should contain only path prefixes. It is meant to stop the test
+// from reporting things that *are* referenced, but for which the test can't
+// find any reference because the URIs are constructed programatically.
+// If you need to whitelist specific files, please use the 'whitelist' object.
+var gExceptionPaths = [
+ "resource://app/defaults/settings/blocklists/",
+ "resource://app/defaults/settings/security-state/",
+ "resource://app/defaults/settings/main/",
+ "resource://app/defaults/preferences/",
+ "resource://gre/modules/commonjs/",
+ "resource://gre/defaults/pref/",
+
+ // These chrome resources are referenced using relative paths from JS files.
+ "chrome://global/content/certviewer/components/",
+
+ // https://github.com/mozilla/activity-stream/issues/3053
+ "chrome://activity-stream/content/data/content/tippytop/images/",
+ "chrome://activity-stream/content/data/content/tippytop/favicons/",
+ // These resources are referenced by messages delivered through Remote Settings
+ "chrome://activity-stream/content/data/content/assets/remote/",
+ "chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user-cn.svg",
+ "chrome://activity-stream/content/data/content/assets/mobile-download-qr-existing-user-cn.svg",
+ "chrome://activity-stream/content/data/content/assets/person-typing.svg",
+ "chrome://browser/content/assets/moz-vpn.svg",
+ "chrome://browser/content/assets/vpn-logo.svg",
+ "chrome://browser/content/assets/focus-promo.png",
+ "chrome://browser/content/assets/klar-qr-code.svg",
+
+ // toolkit/components/pdfjs/content/build/pdf.js
+ "resource://pdf.js/web/images/",
+
+ // Exclude the form autofill path that has been moved out of the extensions to
+ // toolkit, see bug 1691821.
+ "resource://gre-resources/autofill/",
+
+ // Exclude all search-extensions because they aren't referenced by filename
+ "resource://search-extensions/",
+
+ // Exclude all services-automation because they are used through webdriver
+ "resource://gre/modules/services-automation/",
+ "resource://services-automation/ServicesAutomation.jsm",
+
+ // Paths from this folder are constructed in NetErrorParent.sys.mjs based on
+ // the type of cert or net error the user is encountering.
+ "chrome://global/content/neterror/supportpages/",
+
+ // Points to theme preview images, which are defined in browser/ but only used
+ // in toolkit/mozapps/extensions/content/aboutaddons.js.
+ "resource://usercontext-content/builtin-themes/",
+
+ // Page data schemas are referenced programmatically.
+ "chrome://browser/content/pagedata/schemas/",
+
+ // Nimbus schemas are referenced programmatically.
+ "resource://nimbus/schemas/",
+
+ // Activity stream schemas are referenced programmatically.
+ "resource://activity-stream/schemas",
+
+ // Localization file added programatically in featureCallout.jsm
+ "resource://app/localization/en-US/browser/featureCallout.ftl",
+];
+
+// These are not part of the omni.ja file, so we find them only when running
+// the test on a non-packaged build.
+if (AppConstants.platform == "macosx") {
+ gExceptionPaths.push("resource://gre/res/cursors/");
+ gExceptionPaths.push("resource://gre/res/touchbar/");
+}
+
+if (AppConstants.MOZ_BACKGROUNDTASKS) {
+ // These preferences are active only when we're in background task mode.
+ gExceptionPaths.push("resource://gre/defaults/backgroundtasks/");
+ gExceptionPaths.push("resource://app/defaults/backgroundtasks/");
+ // `BackgroundTask_id.jsm` is loaded at runtime by `app --backgroundtask id ...`.
+ gExceptionPaths.push("resource://gre/modules/backgroundtasks/");
+ gExceptionPaths.push("resource://app/modules/backgroundtasks/");
+}
+
+// Bug 1710546 https://bugzilla.mozilla.org/show_bug.cgi?id=1710546
+if (AppConstants.NIGHTLY_BUILD) {
+ gExceptionPaths.push("resource://builtin-addons/translations/");
+}
+
+if (AppConstants.NIGHTLY_BUILD) {
+ // This is nightly-only debug tool.
+ gExceptionPaths.push(
+ "chrome://browser/content/places/interactionsViewer.html"
+ );
+}
+
+// Each whitelist entry should have a comment indicating which file is
+// referencing the whitelisted file in a way that the test can't detect, or a
+// bug number to remove or use the file if it is indeed currently unreferenced.
+var whitelist = [
+ // toolkit/components/pdfjs/content/PdfStreamConverter.jsm
+ { file: "chrome://pdf.js/locale/chrome.properties" },
+ { file: "chrome://pdf.js/locale/viewer.properties" },
+
+ // security/manager/pki/resources/content/device_manager.js
+ { file: "chrome://pippki/content/load_device.xhtml" },
+
+ // The l10n build system can't package string files only for some platforms.
+ // See bug 1339424 for why this is hard to fix.
+ {
+ file: "chrome://global/locale/fallbackMenubar.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/localization/en-US/toolkit/printing/printDialogs.ftl",
+ platforms: ["linux", "macosx"],
+ },
+
+ // This file is referenced by the build system to generate the
+ // Firefox .desktop entry. See bug 1824327 (and perhaps bug 1526672)
+ {
+ file: "resource://app/localization/en-US/browser/linuxDesktopEntry.ftl",
+ },
+
+ // toolkit/content/aboutRights-unbranded.xhtml doesn't use aboutRights.css
+ { file: "chrome://global/skin/aboutRights.css", skipUnofficial: true },
+
+ // devtools/client/inspector/bin/dev-server.js
+ {
+ file: "chrome://devtools/content/inspector/markup/markup.xhtml",
+ isFromDevTools: true,
+ },
+
+ // used by devtools/client/memory/index.xhtml
+ { file: "chrome://global/content/third_party/d3/d3.js" },
+
+ // SpiderMonkey parser API, currently unused in browser/ and toolkit/
+ { file: "resource://gre/modules/reflect.sys.mjs" },
+
+ // extensions/pref/autoconfig/src/nsReadConfig.cpp
+ { file: "resource://gre/defaults/autoconfig/prefcalls.js" },
+
+ // browser/components/preferences/moreFromMozilla.js
+ // These files URLs are constructed programatically at run time.
+ {
+ file: "chrome://browser/content/preferences/more-from-mozilla-qr-code-simple.svg",
+ },
+ {
+ file: "chrome://browser/content/preferences/more-from-mozilla-qr-code-simple-cn.svg",
+ },
+
+ { file: "resource://gre/greprefs.js" },
+
+ // layout/mathml/nsMathMLChar.cpp
+ { file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties" },
+ { file: "resource://gre/res/fonts/mathfontUnicode.properties" },
+
+ // toolkit/mozapps/extensions/AddonContentPolicy.cpp
+ { file: "resource://gre/localization/en-US/toolkit/global/cspErrors.ftl" },
+
+ // The l10n build system can't package string files only for some platforms.
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/accessible.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/intl.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/platformKeys.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/accessible.properties",
+ platforms: ["macosx", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/intl.properties",
+ platforms: ["macosx", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/platformKeys.properties",
+ platforms: ["macosx", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/accessible.properties",
+ platforms: ["linux", "macosx"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/intl.properties",
+ platforms: ["linux", "macosx"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/platformKeys.properties",
+ platforms: ["linux", "macosx"],
+ },
+
+ // Files from upstream library
+ { file: "resource://pdf.js/web/debugger.js" },
+ { file: "resource://pdf.js/web/debugger.css" },
+
+ // resource://app/modules/translation/TranslationContentHandler.jsm
+ { file: "resource://app/modules/translation/BingTranslator.jsm" },
+ { file: "resource://app/modules/translation/GoogleTranslator.jsm" },
+ { file: "resource://app/modules/translation/YandexTranslator.jsm" },
+
+ // Starting from here, files in the whitelist are bugs that need fixing.
+ // Bug 1339424 (wontfix?)
+ {
+ file: "chrome://browser/locale/taskbar.properties",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1344267
+ { file: "chrome://remote/content/marionette/test_dialog.properties" },
+ { file: "chrome://remote/content/marionette/test_dialog.xhtml" },
+ { file: "chrome://remote/content/marionette/test_menupopup.xhtml" },
+ { file: "chrome://remote/content/marionette/test_no_xul.xhtml" },
+ { file: "chrome://remote/content/marionette/test.xhtml" },
+ // Bug 1348559
+ { file: "chrome://pippki/content/resetpassword.xhtml" },
+ // Bug 1337345
+ { file: "resource://gre/modules/Manifest.sys.mjs" },
+ // Bug 1494170
+ // (The references to these files are dynamically generated, so the test can't
+ // find the references)
+ {
+ file: "chrome://devtools/skin/images/aboutdebugging-firefox-aurora.svg",
+ isFromDevTools: true,
+ },
+ {
+ file: "chrome://devtools/skin/images/aboutdebugging-firefox-beta.svg",
+ isFromDevTools: true,
+ },
+ {
+ file: "chrome://devtools/skin/images/aboutdebugging-firefox-release.svg",
+ isFromDevTools: true,
+ },
+ { file: "chrome://devtools/skin/images/next.svg", isFromDevTools: true },
+
+ // Bug 1526672
+ {
+ file: "resource://app/localization/en-US/browser/touchbar/touchbar.ftl",
+ platforms: ["linux", "win"],
+ },
+ // Referenced by the webcompat system addon for localization
+ { file: "resource://gre/localization/en-US/toolkit/about/aboutCompat.ftl" },
+
+ // dom/media/mediacontrol/MediaControlService.cpp
+ { file: "resource://gre/localization/en-US/dom/media.ftl" },
+
+ // dom/xml/nsXMLPrettyPrinter.cpp
+ { file: "resource://gre/localization/en-US/dom/XMLPrettyPrint.ftl" },
+
+ // tookit/mozapps/update/BackgroundUpdate.jsm
+ {
+ file: "resource://gre/localization/en-US/toolkit/updates/backgroundupdate.ftl",
+ },
+ // Bug 1713242 - referenced by aboutThirdParty.html which is only for Windows
+ {
+ file: "resource://gre/localization/en-US/toolkit/about/aboutThirdParty.ftl",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1973834 - referenced by aboutWindowsMessages.html which is only for Windows
+ {
+ file: "resource://gre/localization/en-US/toolkit/about/aboutWindowsMessages.ftl",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1721741:
+ // (The references to these files are dynamically generated, so the test can't
+ // find the references)
+ { file: "chrome://browser/content/screenshots/copied-notification.svg" },
+
+ // toolkit/xre/MacRunFromDmgUtils.mm
+ { file: "resource://gre/localization/en-US/toolkit/global/run-from-dmg.ftl" },
+
+ // Referenced by screenshots extension
+ { file: "chrome://browser/content/screenshots/cancel.svg" },
+ { file: "chrome://browser/content/screenshots/copy.svg" },
+ { file: "chrome://browser/content/screenshots/download.svg" },
+ { file: "chrome://browser/content/screenshots/download-white.svg" },
+
+ // Bug 1824826 - Implement a view of history in Firefox View
+ { file: "resource://gre/modules/PlacesQuery.sys.mjs" },
+
+ // Should be removed in bug 1824826 when fxview-tab-list is used in Firefox View
+ { file: "resource://app/localization/en-US/browser/fxviewTabList.ftl" },
+ { file: "chrome://browser/content/firefoxview/fxview-tab-list.css" },
+ { file: "chrome://browser/content/firefoxview/fxview-tab-list.mjs" },
+ { file: "chrome://browser/content/firefoxview/fxview-tab-row.css" },
+
+ // Bug 1834176 - Imports of NetUtil can't be converted until hostutils is
+ // updated.
+ { file: "resource://gre/modules/NetUtil.sys.mjs" },
+];
+
+if (AppConstants.NIGHTLY_BUILD && AppConstants.platform != "win") {
+ // This path is refereneced in nsFxrCommandLineHandler.cpp, which is only
+ // compiled in Windows. Whitelisted this path so that non-Windows builds
+ // can access the FxR UI via --chrome rather than --fxr (which includes VR-
+ // specific functionality)
+ whitelist.push({ file: "chrome://fxr/content/fxrui.html" });
+}
+
+if (AppConstants.platform == "android") {
+ // The l10n build system can't package string files only for some platforms.
+ // Referenced by aboutGlean.html
+ whitelist.push({
+ file: "resource://gre/localization/en-US/toolkit/about/aboutGlean.ftl",
+ });
+}
+
+if (AppConstants.MOZ_UPDATE_AGENT && !AppConstants.MOZ_BACKGROUNDTASKS) {
+ // Task scheduling is only used for background updates right now.
+ whitelist.push({
+ file: "resource://gre/modules/TaskScheduler.jsm",
+ });
+}
+
+whitelist = new Set(
+ whitelist
+ .filter(
+ item =>
+ "isFromDevTools" in item == isDevtools &&
+ (!item.skipUnofficial || !AppConstants.MOZILLA_OFFICIAL) &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))
+ )
+ .map(item => item.file)
+);
+
+const ignorableWhitelist = new Set([
+ // The following files are outside of the omni.ja file, so we only catch them
+ // when testing on a non-packaged build.
+
+ // toolkit/mozapps/extensions/nsBlocklistService.js
+ "resource://app/blocklist.xml",
+
+ // dom/media/gmp/GMPParent.cpp
+ "resource://gre/gmp-clearkey/0.1/manifest.json",
+
+ // Bug 1351669 - obsolete test file
+ "resource://gre/res/test.properties",
+]);
+for (let entry of ignorableWhitelist) {
+ whitelist.add(entry);
+}
+
+if (!isDevtools) {
+ // services/sync/modules/service.sys.mjs
+ for (let module of [
+ "addons.sys.mjs",
+ "bookmarks.sys.mjs",
+ "forms.sys.mjs",
+ "history.sys.mjs",
+ "passwords.sys.mjs",
+ "prefs.sys.mjs",
+ "tabs.sys.mjs",
+ "extension-storage.sys.mjs",
+ ]) {
+ whitelist.add("resource://services-sync/engines/" + module);
+ }
+ // resource://devtools/shared/worker/loader.js,
+ // resource://devtools/shared/loader/builtin-modules.js
+ if (!AppConstants.ENABLE_WEBDRIVER) {
+ whitelist.add("resource://gre/modules/jsdebugger.sys.mjs");
+ }
+}
+
+if (AppConstants.MOZ_CODE_COVERAGE) {
+ whitelist.add(
+ "chrome://remote/content/marionette/PerTestCoverageUtils.sys.mjs"
+ );
+}
+
+const gInterestingCategories = new Set([
+ "agent-style-sheets",
+ "addon-provider-module",
+ "webextension-modules",
+ "webextension-scripts",
+ "webextension-schemas",
+ "webextension-scripts-addon",
+ "webextension-scripts-content",
+ "webextension-scripts-devtools",
+]);
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+);
+var gChromeMap = new Map();
+var gOverrideMap = new Map();
+var gComponentsSet = new Set();
+
+// In this map when the value is a Set of URLs, the file is referenced if any
+// of the files in the Set is referenced.
+// When the value is null, the file is referenced unconditionally.
+// When the value is a string, "whitelist-direct" means that we have not found
+// any reference in the code, but have a matching whitelist entry for this file.
+// "whitelist" means that the file is indirectly whitelisted, ie. a whitelisted
+// file causes this file to be referenced.
+var gReferencesFromCode = new Map();
+
+var resHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+var gResourceMap = [];
+function trackResourcePrefix(prefix) {
+ let uri = Services.io.newURI("resource://" + prefix + "/");
+ gResourceMap.unshift([prefix, resHandler.resolveURI(uri)]);
+}
+trackResourcePrefix("gre");
+trackResourcePrefix("app");
+
+function getBaseUriForChromeUri(chromeUri) {
+ let chromeFile = chromeUri + "nonexistentfile.reallynothere";
+ let uri = Services.io.newURI(chromeFile);
+ let fileUri = gChromeReg.convertChromeURL(uri);
+ return fileUri.resolve(".");
+}
+
+function trackChromeUri(uri) {
+ gChromeMap.set(getBaseUriForChromeUri(uri), uri);
+}
+
+// formautofill registers resource://formautofill/ and
+// chrome://formautofill/content/ dynamically at runtime.
+// Bug 1480276 is about addressing this without this hard-coding.
+trackResourcePrefix("autofill");
+trackChromeUri("chrome://formautofill/content/");
+
+function parseManifest(manifestUri) {
+ return fetchFile(manifestUri.spec).then(data => {
+ for (let line of data.split("\n")) {
+ let [type, ...argv] = line.split(/\s+/);
+ if (type == "content" || type == "skin" || type == "locale") {
+ let chromeUri = `chrome://${argv[0]}/${type}/`;
+ // The webcompat reporter's locale directory may not exist if
+ // the addon is preffed-off, and since it's a hack until we
+ // get bz1425104 landed, we'll just skip it for now.
+ if (chromeUri === "chrome://report-site-issue/locale/") {
+ gChromeMap.set("chrome://report-site-issue/locale/", true);
+ } else {
+ trackChromeUri(chromeUri);
+ }
+ } else if (type == "override" || type == "overlay") {
+ // Overlays aren't really overrides, but behave the same in
+ // that the overlay is only referenced if the original xul
+ // file is referenced somewhere.
+ let os = "os=" + Services.appinfo.OS;
+ if (!argv.some(s => s.startsWith("os=") && s != os)) {
+ gOverrideMap.set(
+ Services.io.newURI(argv[1]).specIgnoringRef,
+ Services.io.newURI(argv[0]).specIgnoringRef
+ );
+ }
+ } else if (type == "category" && gInterestingCategories.has(argv[0])) {
+ gReferencesFromCode.set(argv[2], null);
+ } else if (type == "resource") {
+ trackResourcePrefix(argv[0]);
+ } else if (type == "component") {
+ gComponentsSet.add(argv[1]);
+ }
+ }
+ });
+}
+
+// If the given URI is a webextension manifest, extract files used by
+// any of its APIs (scripts, icons, style sheets, theme images).
+// Returns the passed in URI if the manifest is not a webextension
+// manifest, null otherwise.
+async function parseJsonManifest(uri) {
+ uri = Services.io.newURI(convertToCodeURI(uri.spec));
+
+ let raw = await fetchFile(uri.spec);
+ let data;
+ try {
+ data = JSON.parse(raw);
+ } catch (ex) {
+ return uri;
+ }
+
+ // Simplistic test for whether this is a webextension manifest:
+ if (data.manifest_version !== 2) {
+ return uri;
+ }
+
+ if (data.background?.scripts) {
+ for (let bgscript of data.background.scripts) {
+ gReferencesFromCode.set(uri.resolve(bgscript), null);
+ }
+ }
+
+ if (data.icons) {
+ for (let icon of Object.values(data.icons)) {
+ gReferencesFromCode.set(uri.resolve(icon), null);
+ }
+ }
+
+ if (data.experiment_apis) {
+ for (let api of Object.values(data.experiment_apis)) {
+ if (api.parent && api.parent.script) {
+ let script = uri.resolve(api.parent.script);
+ gReferencesFromCode.set(script, null);
+ }
+
+ if (api.schema) {
+ gReferencesFromCode.set(uri.resolve(api.schema), null);
+ }
+ }
+ }
+
+ if (data.theme_experiment && data.theme_experiment.stylesheet) {
+ let stylesheet = uri.resolve(data.theme_experiment.stylesheet);
+ gReferencesFromCode.set(stylesheet, null);
+ }
+
+ for (let themeKey of ["theme", "dark_theme"]) {
+ if (data?.[themeKey]?.images?.additional_backgrounds) {
+ for (let background of data[themeKey].images.additional_backgrounds) {
+ gReferencesFromCode.set(uri.resolve(background), null);
+ }
+ }
+ }
+
+ return null;
+}
+
+function addCodeReference(url, fromURI) {
+ let from = convertToCodeURI(fromURI.spec);
+
+ // Ignore self references.
+ if (url == from) {
+ return;
+ }
+
+ let ref;
+ if (gReferencesFromCode.has(url)) {
+ ref = gReferencesFromCode.get(url);
+ if (ref === null) {
+ return;
+ }
+ } else {
+ ref = new Set();
+ gReferencesFromCode.set(url, ref);
+ }
+ ref.add(from);
+}
+
+function listCodeReferences(refs) {
+ let refList = [];
+ if (refs) {
+ for (let ref of refs) {
+ refList.push(ref);
+ }
+ }
+ return refList.join(",");
+}
+
+function parseCSSFile(fileUri) {
+ return fetchFile(fileUri.spec).then(data => {
+ for (let line of data.split("\n")) {
+ let urls = line.match(/url\([^()]+\)/g);
+ if (!urls) {
+ // @import rules can take a string instead of a url.
+ let importMatch = line.match(/@import ['"]?([^'"]*)['"]?/);
+ if (importMatch && importMatch[1]) {
+ let url = Services.io.newURI(importMatch[1], null, fileUri).spec;
+ addCodeReference(convertToCodeURI(url), fileUri);
+ }
+ continue;
+ }
+
+ for (let url of urls) {
+ // Remove the url(" prefix and the ") suffix.
+ url = url
+ .replace(/url\(([^)]*)\)/, "$1")
+ .replace(/^"(.*)"$/, "$1")
+ .replace(/^'(.*)'$/, "$1");
+ if (url.startsWith("data:")) {
+ continue;
+ }
+
+ try {
+ url = Services.io.newURI(url, null, fileUri).specIgnoringRef;
+ addCodeReference(convertToCodeURI(url), fileUri);
+ } catch (e) {
+ ok(false, "unexpected error while resolving this URI: " + url);
+ }
+ }
+ }
+ });
+}
+
+function parseCodeFile(fileUri) {
+ return fetchFile(fileUri.spec).then(data => {
+ let baseUri;
+ for (let line of data.split("\n")) {
+ let urls = line.match(
+ /["'`]chrome:\/\/[a-zA-Z0-9-]+\/(content|skin|locale)\/[^"'` ]*["'`]/g
+ );
+
+ if (!urls) {
+ urls = line.match(/["']resource:\/\/[^"']+["']/g);
+ if (
+ urls &&
+ isDevtools &&
+ /baseURI: "resource:\/\/devtools\//.test(line)
+ ) {
+ baseUri = Services.io.newURI(urls[0].slice(1, -1));
+ continue;
+ }
+ }
+
+ if (!urls) {
+ urls = line.match(/[a-z0-9_\/-]+\.ftl/i);
+ if (urls) {
+ urls = urls[0];
+ let grePrefix = Services.io.newURI(
+ "resource://gre/localization/en-US/"
+ );
+ let appPrefix = Services.io.newURI(
+ "resource://app/localization/en-US/"
+ );
+
+ let grePrefixUrl = Services.io.newURI(urls, null, grePrefix).spec;
+ let appPrefixUrl = Services.io.newURI(urls, null, appPrefix).spec;
+
+ addCodeReference(grePrefixUrl, fileUri);
+ addCodeReference(appPrefixUrl, fileUri);
+ continue;
+ }
+ }
+
+ if (!urls) {
+ // If there's no absolute chrome URL, look for relative ones in
+ // src and href attributes.
+ let match = line.match("(?:src|href)=[\"']([^$&\"']+)");
+ if (match && match[1]) {
+ let url = Services.io.newURI(match[1], null, fileUri).spec;
+ addCodeReference(convertToCodeURI(url), fileUri);
+ }
+
+ // This handles `import` lines which may be multi-line.
+ // We have an ESLint rule, `import/no-unassigned-import` which prevents
+ // using bare `import "foo.js"`, so we don't need to handle that case
+ // here.
+ match = line.match(/from\W*['"](.*?)['"]/);
+ if (match?.[1]) {
+ let url = match[1];
+ url = Services.io.newURI(url, null, baseUri || fileUri).spec;
+ url = convertToCodeURI(url);
+ addCodeReference(url, fileUri);
+ }
+
+ if (isDevtools) {
+ let rules = [
+ ["devtools/client/locales", "chrome://devtools/locale"],
+ ["devtools/shared/locales", "chrome://devtools-shared/locale"],
+ [
+ "devtools/shared/platform",
+ "resource://devtools/shared/platform/chrome",
+ ],
+ ["devtools", "resource://devtools"],
+ ];
+
+ match = line.match(/["']((?:devtools)\/[^\\#"']+)["']/);
+ if (match && match[1]) {
+ let path = match[1];
+ for (let rule of rules) {
+ if (path.startsWith(rule[0] + "/")) {
+ path = path.replace(rule[0], rule[1]);
+ if (!/\.(properties|js|jsm|mjs|json|css)$/.test(path)) {
+ path += ".js";
+ }
+ addCodeReference(path, fileUri);
+ break;
+ }
+ }
+ }
+
+ match = line.match(/require\(['"](\.[^'"]+)['"]\)/);
+ if (match && match[1]) {
+ let url = match[1];
+ url = Services.io.newURI(url, null, baseUri || fileUri).spec;
+ url = convertToCodeURI(url);
+ if (!/\.(properties|js|jsm|mjs|json|css)$/.test(url)) {
+ url += ".js";
+ }
+ if (url.startsWith("resource://")) {
+ addCodeReference(url, fileUri);
+ } else {
+ // if we end up with a chrome:// url here, it's likely because
+ // a baseURI to a resource:// path has been defined in another
+ // .js file that is loaded in the same scope, we can't detect it.
+ }
+ }
+ }
+ continue;
+ }
+
+ for (let url of urls) {
+ // Remove quotes.
+ url = url.slice(1, -1);
+ // Remove ? or \ trailing characters.
+ if (url.endsWith("\\")) {
+ url = url.slice(0, -1);
+ }
+
+ let pos = url.indexOf("?");
+ if (pos != -1) {
+ url = url.slice(0, pos);
+ }
+
+ // Make urls like chrome://browser/skin/ point to an actual file,
+ // and remove the ref if any.
+ try {
+ url = Services.io.newURI(url).specIgnoringRef;
+ } catch (e) {
+ continue;
+ }
+
+ if (
+ isDevtools &&
+ line.includes("require(") &&
+ !/\.(properties|js|jsm|mjs|json|css)$/.test(url)
+ ) {
+ url += ".js";
+ }
+
+ addCodeReference(url, fileUri);
+ }
+ }
+ });
+}
+
+function convertToCodeURI(fileUri) {
+ let baseUri = fileUri;
+ let path = "";
+ while (true) {
+ let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
+ if (slashPos <= 0) {
+ // File not accessible from chrome protocol, try resource://
+ for (let res of gResourceMap) {
+ if (fileUri.startsWith(res[1])) {
+ return fileUri.replace(res[1], "resource://" + res[0] + "/");
+ }
+ }
+ // Give up and return the original URL.
+ return fileUri;
+ }
+ path = baseUri.slice(slashPos + 1) + path;
+ baseUri = baseUri.slice(0, slashPos + 1);
+ if (gChromeMap.has(baseUri)) {
+ return gChromeMap.get(baseUri) + path;
+ }
+ }
+}
+
+async function chromeFileExists(aURI) {
+ try {
+ return await PerfTestHelpers.checkURIExists(aURI);
+ } catch (e) {
+ todo(false, `Failed to check if ${aURI} exists: ${e}`);
+ return false;
+ }
+}
+
+function findChromeUrlsFromArray(array, prefix) {
+ // Find the first character of the prefix...
+ for (
+ let index = 0;
+ (index = array.indexOf(prefix.charCodeAt(0), index)) != -1;
+ ++index
+ ) {
+ // Then ensure we actually have the whole prefix.
+ let found = true;
+ for (let i = 1; i < prefix.length; ++i) {
+ if (array[index + i] != prefix.charCodeAt(i)) {
+ found = false;
+ break;
+ }
+ }
+ if (!found) {
+ continue;
+ }
+
+ // C strings are null terminated, but " also terminates urls
+ // (nsIndexedToHTML.cpp contains an HTML fragment with several chrome urls)
+ // Let's also terminate the string on the # character to skip references.
+ let end = Math.min(
+ array.indexOf(0, index),
+ array.indexOf('"'.charCodeAt(0), index),
+ array.indexOf(")".charCodeAt(0), index),
+ array.indexOf("#".charCodeAt(0), index)
+ );
+ let string = "";
+ for (; index < end; ++index) {
+ string += String.fromCharCode(array[index]);
+ }
+
+ // Only keep strings that look like real chrome or resource urls.
+ if (
+ /chrome:\/\/[a-zA-Z09-]+\/(content|skin|locale)\//.test(string) ||
+ /resource:\/\/[a-zA-Z09-]*\/.*\.[a-z]+/.test(string)
+ ) {
+ gReferencesFromCode.set(string, null);
+ }
+ }
+}
+
+add_task(async function checkAllTheFiles() {
+ TestUtils.assertPackagedBuild();
+
+ const libxul = await IOUtils.read(PathUtils.xulLibraryPath);
+ findChromeUrlsFromArray(libxul, "chrome://");
+ findChromeUrlsFromArray(libxul, "resource://");
+ // Handle NS_LITERAL_STRING.
+ let uint16 = new Uint16Array(libxul.buffer);
+ findChromeUrlsFromArray(uint16, "chrome://");
+ findChromeUrlsFromArray(uint16, "resource://");
+
+ const kCodeExtensions = [
+ ".xml",
+ ".xsl",
+ ".mjs",
+ ".js",
+ ".jsm",
+ ".json",
+ ".html",
+ ".xhtml",
+ ];
+
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let uris = await generateURIsFromDirTree(
+ appDir,
+ [
+ ".css",
+ ".manifest",
+ ".jpg",
+ ".png",
+ ".gif",
+ ".svg",
+ ".ftl",
+ ".dtd",
+ ".properties",
+ ].concat(kCodeExtensions)
+ );
+
+ // Parse and remove all manifests from the list.
+ // NOTE that this must be done before filtering out devtools paths
+ // so that all chrome paths can be recorded.
+ let manifestURIs = [];
+ let jsonManifests = [];
+ uris = uris.filter(uri => {
+ let path = uri.pathQueryRef;
+ if (path.endsWith(".manifest")) {
+ manifestURIs.push(uri);
+ return false;
+ } else if (path.endsWith("/manifest.json")) {
+ jsonManifests.push(uri);
+ return false;
+ }
+
+ return true;
+ });
+
+ // Wait for all manifest to be parsed
+ await PerfTestHelpers.throttledMapPromises(manifestURIs, parseManifest);
+
+ for (let jsm of Components.manager.getComponentJSMs()) {
+ gReferencesFromCode.set(jsm, null);
+ }
+ for (let esModule of Components.manager.getComponentESModules()) {
+ gReferencesFromCode.set(esModule, null);
+ }
+
+ // manifest.json is a common name, it is used for WebExtension manifests
+ // but also for other things. To tell them apart, we have to actually
+ // read the contents. This will populate gExtensionRoots with all
+ // embedded extension APIs, and return any manifest.json files that aren't
+ // webextensions.
+ let nonWebextManifests = (
+ await Promise.all(jsonManifests.map(parseJsonManifest))
+ ).filter(uri => !!uri);
+ uris.push(...nonWebextManifests);
+
+ // We build a list of promises that get resolved when their respective
+ // files have loaded and produced no errors.
+ let allPromises = [];
+
+ for (let uri of uris) {
+ let path = uri.pathQueryRef;
+ if (path.endsWith(".css")) {
+ allPromises.push([parseCSSFile, uri]);
+ } else if (kCodeExtensions.some(ext => path.endsWith(ext))) {
+ allPromises.push([parseCodeFile, uri]);
+ }
+ }
+
+ // Wait for all the files to have actually loaded:
+ await PerfTestHelpers.throttledMapPromises(allPromises, ([task, uri]) =>
+ task(uri)
+ );
+
+ // Keep only chrome:// files, and filter out either the devtools paths or
+ // the non-devtools paths:
+ let devtoolsPrefixes = [
+ "chrome://devtools",
+ "resource://devtools/",
+ "resource://devtools-client-jsonview/",
+ "resource://devtools-client-shared/",
+ "resource://app/modules/devtools",
+ "resource://gre/modules/devtools",
+ "resource://app/localization/en-US/startup/aboutDevTools.ftl",
+ "resource://app/localization/en-US/devtools/",
+ ];
+ let hasDevtoolsPrefix = uri =>
+ devtoolsPrefixes.some(prefix => uri.startsWith(prefix));
+ let chromeFiles = [];
+ for (let uri of uris) {
+ uri = convertToCodeURI(uri.spec);
+ if (
+ (uri.startsWith("chrome://") || uri.startsWith("resource://")) &&
+ isDevtools == hasDevtoolsPrefix(uri)
+ ) {
+ chromeFiles.push(uri);
+ }
+ }
+
+ if (isDevtools) {
+ // chrome://devtools/skin/devtools-browser.css is included from browser.xhtml
+ gReferencesFromCode.set(AppConstants.BROWSER_CHROME_URL, null);
+ // devtools' css is currently included from browser.css, see bug 1204810.
+ gReferencesFromCode.set("chrome://browser/skin/browser.css", null);
+ }
+
+ let isUnreferenced = file => {
+ if (gExceptionPaths.some(e => file.startsWith(e))) {
+ return false;
+ }
+ if (gReferencesFromCode.has(file)) {
+ let refs = gReferencesFromCode.get(file);
+ if (refs === null) {
+ return false;
+ }
+ for (let ref of refs) {
+ if (isDevtools) {
+ if (
+ ref.startsWith("resource://app/components/") ||
+ (file.startsWith("chrome://") && ref.startsWith("resource://"))
+ ) {
+ return false;
+ }
+ }
+
+ if (gReferencesFromCode.has(ref)) {
+ let refType = gReferencesFromCode.get(ref);
+ if (
+ refType === null || // unconditionally referenced
+ refType == "whitelist" ||
+ refType == "whitelist-direct"
+ ) {
+ return false;
+ }
+ }
+ }
+ }
+ return !gOverrideMap.has(file) || isUnreferenced(gOverrideMap.get(file));
+ };
+
+ let unreferencedFiles = chromeFiles;
+
+ let removeReferenced = useWhitelist => {
+ let foundReference = false;
+ unreferencedFiles = unreferencedFiles.filter(f => {
+ let rv = isUnreferenced(f);
+ if (rv && f.startsWith("resource://app/")) {
+ rv = isUnreferenced(f.replace("resource://app/", "resource:///"));
+ }
+ if (rv && /^resource:\/\/(?:app|gre)\/components\/[^/]+\.js$/.test(f)) {
+ rv = !gComponentsSet.has(f.replace(/.*\//, ""));
+ }
+ if (!rv) {
+ foundReference = true;
+ if (useWhitelist) {
+ info(
+ "indirectly whitelisted file: " +
+ f +
+ " used from " +
+ listCodeReferences(gReferencesFromCode.get(f))
+ );
+ }
+ gReferencesFromCode.set(f, useWhitelist ? "whitelist" : null);
+ }
+ return rv;
+ });
+ return foundReference;
+ };
+ // First filter out the files that are referenced.
+ while (removeReferenced(false)) {
+ // As long as removeReferenced returns true, some files have been marked
+ // as referenced, so we need to run it again.
+ }
+ // Marked as referenced the files that have been explicitly whitelisted.
+ unreferencedFiles = unreferencedFiles.filter(file => {
+ if (whitelist.has(file)) {
+ whitelist.delete(file);
+ gReferencesFromCode.set(file, "whitelist-direct");
+ return false;
+ }
+ return true;
+ });
+ // Run the process again, this time when more files are marked as referenced,
+ // it's a consequence of the whitelist.
+ while (removeReferenced(true)) {
+ // As long as removeReferenced returns true, we need to run it again.
+ }
+
+ unreferencedFiles.sort();
+
+ if (isDevtools) {
+ // Bug 1351878 - handle devtools resource files
+ unreferencedFiles = unreferencedFiles.filter(file => {
+ if (file.startsWith("resource://")) {
+ info("unreferenced devtools resource file: " + file);
+ return false;
+ }
+ return true;
+ });
+ }
+
+ is(unreferencedFiles.length, 0, "there should be no unreferenced files");
+ for (let file of unreferencedFiles) {
+ let refs = gReferencesFromCode.get(file);
+ if (refs === undefined) {
+ ok(false, "unreferenced file: " + file);
+ } else {
+ let refList = listCodeReferences(refs);
+ let msg = "file only referenced from unreferenced files: " + file;
+ if (refList) {
+ msg += " referenced from " + refList;
+ }
+ ok(false, msg);
+ }
+ }
+
+ for (let file of whitelist) {
+ if (ignorableWhitelist.has(file)) {
+ info("ignored unused whitelist entry: " + file);
+ } else {
+ ok(false, "unused whitelist entry: " + file);
+ }
+ }
+
+ for (let [file, refs] of gReferencesFromCode) {
+ if (
+ isDevtools != devtoolsPrefixes.some(prefix => file.startsWith(prefix))
+ ) {
+ continue;
+ }
+
+ if (
+ (file.startsWith("chrome://") || file.startsWith("resource://")) &&
+ !(await chromeFileExists(file))
+ ) {
+ // Ignore chrome prefixes that have been automatically expanded.
+ let pathParts =
+ file.match("chrome://([^/]+)/content/([^/.]+).xul") ||
+ file.match("chrome://([^/]+)/skin/([^/.]+).css");
+ if (pathParts && pathParts[1] == pathParts[2]) {
+ continue;
+ }
+
+ // TODO: bug 1349010 - add a whitelist and make this reliable enough
+ // that we could make the test fail when this catches something new.
+ let refList = listCodeReferences(refs);
+ let msg = "missing file: " + file;
+ if (refList) {
+ msg += " referenced from " + refList;
+ }
+ info(msg);
+ }
+ }
+});
diff --git a/browser/base/content/test/static/browser_misused_characters_in_strings.js b/browser/base/content/test/static/browser_misused_characters_in_strings.js
new file mode 100644
index 0000000000..42be3b4392
--- /dev/null
+++ b/browser/base/content/test/static/browser_misused_characters_in_strings.js
@@ -0,0 +1,276 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' issues to remain, while we
+ * detect newly occurring issues in shipping files. It is a list of objects
+ * specifying conditions under which an error should be ignored.
+ *
+ * As each issue is found in the exceptions list, it is removed from the list.
+ * At the end of the test, there is an assertion that all items have been
+ * removed from the exceptions list, thus ensuring there are no stale
+ * entries. */
+let gExceptionsList = [
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapRectBoundsError",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapCircleWrongNumberOfCoords",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapCircleNegativeRadius",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapPolyWrongNumberOfCoords",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapPolyOddNumberOfCoords",
+ type: "double-quote",
+ },
+ {
+ file: "dom.properties",
+ key: "PatternAttributeCompileFailure",
+ type: "single-quote",
+ },
+ // dom.properties is packaged twice so we need to have two exceptions for this string.
+ {
+ file: "dom.properties",
+ key: "PatternAttributeCompileFailure",
+ type: "single-quote",
+ },
+ {
+ file: "dom.properties",
+ key: "ImportMapExternalNotSupported",
+ type: "single-quote",
+ },
+ // dom.properties is packaged twice so we need to have two exceptions for this string.
+ {
+ file: "dom.properties",
+ key: "ImportMapExternalNotSupported",
+ type: "single-quote",
+ },
+];
+
+/**
+ * Check if an error should be ignored due to matching one of the exceptions
+ * defined in gExceptionsList.
+ *
+ * @param filepath The URI spec of the locale file
+ * @param key The key of the entity that is being checked
+ * @param type The type of error that has been found
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(filepath, key, type) {
+ for (let index in gExceptionsList) {
+ let exceptionItem = gExceptionsList[index];
+ if (
+ filepath.endsWith(exceptionItem.file) &&
+ key == exceptionItem.key &&
+ type == exceptionItem.type
+ ) {
+ gExceptionsList.splice(index, 1);
+ return true;
+ }
+ }
+ return false;
+}
+
+function testForError(filepath, key, str, pattern, type, helpText) {
+ if (str.match(pattern) && !ignoredError(filepath, key, type)) {
+ ok(false, `${filepath} with key=${key} has a misused ${type}. ${helpText}`);
+ }
+}
+
+function testForErrors(filepath, key, str) {
+ testForError(
+ filepath,
+ key,
+ str,
+ /(\w|^)'\w/,
+ "apostrophe",
+ "Strings with apostrophes should use foo\u2019s instead of foo's."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /\w\u2018\w/,
+ "incorrect-apostrophe",
+ "Strings with apostrophes should use foo\u2019s instead of foo\u2018s."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /'.+'/,
+ "single-quote",
+ "Single-quoted strings should use Unicode \u2018foo\u2019 instead of 'foo'."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /"/,
+ "double-quote",
+ 'Double-quoted strings should use Unicode \u201cfoo\u201d instead of "foo".'
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /\.\.\./,
+ "ellipsis",
+ "Strings with an ellipsis should use the Unicode \u2026 character instead of three periods."
+ );
+}
+
+async function getAllTheFiles(extension) {
+ let appDirGreD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let appDirXCurProcD = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ if (appDirGreD.contains(appDirXCurProcD)) {
+ return generateURIsFromDirTree(appDirGreD, [extension]);
+ }
+ if (appDirXCurProcD.contains(appDirGreD)) {
+ return generateURIsFromDirTree(appDirXCurProcD, [extension]);
+ }
+ let urisGreD = await generateURIsFromDirTree(appDirGreD, [extension]);
+ let urisXCurProcD = await generateURIsFromDirTree(appDirXCurProcD, [
+ extension,
+ ]);
+ return Array.from(new Set(urisGreD.concat(urisXCurProcD)));
+}
+
+add_task(async function checkAllTheProperties() {
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let uris = await getAllTheFiles(".properties");
+ ok(
+ uris.length,
+ `Found ${uris.length} .properties files to scan for misused characters`
+ );
+
+ for (let uri of uris) {
+ let bundle = Services.strings.createBundle(uri.spec);
+
+ for (let entity of bundle.getSimpleEnumeration()) {
+ testForErrors(uri.spec, entity.key, entity.value);
+ }
+ }
+});
+
+var checkDTD = async function (aURISpec) {
+ let rawContents = await fetchFile(aURISpec);
+ // The regular expression below is adapted from:
+ // https://hg.mozilla.org/mozilla-central/file/68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8/python/compare-locales/compare_locales/parser.py#l233
+ let entities = rawContents.match(
+ /<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/g
+ );
+ if (!entities) {
+ // Some files have no entities defined.
+ return;
+ }
+ for (let entity of entities) {
+ let [, key, str] = entity.match(
+ /<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/
+ );
+ // The matched string includes the enclosing quotation marks,
+ // we need to slice them off.
+ str = str.slice(1, -1);
+ testForErrors(aURISpec, key, str);
+ }
+};
+
+add_task(async function checkAllTheDTDs() {
+ let uris = await getAllTheFiles(".dtd");
+ ok(
+ uris.length,
+ `Found ${uris.length} .dtd files to scan for misused characters`
+ );
+ for (let uri of uris) {
+ await checkDTD(uri.spec);
+ }
+
+ // This support DTD file supplies a string with a newline to make sure
+ // the regex in checkDTD works correctly for that case.
+ let dtdLocation = gTestPath.replace(
+ /\/[^\/]*$/i,
+ "/bug1262648_string_with_newlines.dtd"
+ );
+ await checkDTD(dtdLocation);
+});
+
+add_task(async function checkAllTheFluents() {
+ let uris = await getAllTheFiles(".ftl");
+ let { FluentParser, Visitor } = ChromeUtils.import(
+ "resource://testing-common/FluentSyntax.jsm"
+ );
+
+ class TextElementVisitor extends Visitor {
+ constructor() {
+ super();
+ let domParser = new DOMParser();
+ domParser.forceEnableDTD();
+
+ this.domParser = domParser;
+ this.uri = null;
+ this.id = null;
+ this.attr = null;
+ }
+
+ visitMessage(node) {
+ this.id = node.id.name;
+ this.attr = null;
+ this.genericVisit(node);
+ }
+
+ visitTerm(node) {
+ this.id = node.id.name;
+ this.attr = null;
+ this.genericVisit(node);
+ }
+
+ visitAttribute(node) {
+ this.attr = node.id.name;
+ this.genericVisit(node);
+ }
+
+ get key() {
+ if (this.attr) {
+ return `${this.id}.${this.attr}`;
+ }
+ return this.id;
+ }
+
+ visitTextElement(node) {
+ const stripped_val = this.domParser.parseFromString(
+ "<!DOCTYPE html>" + node.value,
+ "text/html"
+ ).documentElement.textContent;
+ testForErrors(this.uri, this.key, stripped_val);
+ }
+ }
+
+ const ftlParser = new FluentParser({ withSpans: false });
+ const visitor = new TextElementVisitor();
+
+ for (let uri of uris) {
+ let rawContents = await fetchFile(uri.spec);
+ let ast = ftlParser.parse(rawContents);
+
+ visitor.uri = uri.spec;
+ visitor.visit(ast);
+ }
+});
+
+add_task(async function ensureExceptionsListIsEmpty() {
+ is(gExceptionsList.length, 0, "No remaining exceptions exist");
+});
diff --git a/browser/base/content/test/static/browser_parsable_css.js b/browser/base/content/test/static/browser_parsable_css.js
new file mode 100644
index 0000000000..6ff480fddc
--- /dev/null
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -0,0 +1,590 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' CSS issues to remain, while we
+ * detect newly occurring issues in shipping CSS. It is a list of objects
+ * specifying conditions under which an error should be ignored.
+ *
+ * Every property of the objects in it needs to consist of a regular expression
+ * matching the offending error. If an object has multiple regex criteria, they
+ * ALL need to match an error in order for that error not to cause a test
+ * failure. */
+let whitelist = [
+ // CodeMirror is imported as-is, see bug 1004423.
+ { sourceName: /codemirror\.css$/i, isFromDevTools: true },
+ {
+ sourceName: /devtools\/content\/debugger\/src\/components\/([A-z\/]+).css/i,
+ isFromDevTools: true,
+ },
+ // Highlighter CSS uses a UA-only pseudo-class, see bug 985597.
+ {
+ sourceName: /highlighters\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
+ isFromDevTools: true,
+ },
+ // UA-only media features.
+ {
+ sourceName: /\b(autocomplete-item)\.css$/,
+ errorMessage: /Expected media feature name but found \u2018-moz.*/i,
+ isFromDevTools: false,
+ platforms: ["windows"],
+ },
+ {
+ sourceName:
+ /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua)\.css$/i,
+ errorMessage: /Unknown pseudo-class.*-moz-/i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName:
+ /\b(scrollbars|xul|html|mathml|ua|forms|svg|manageDialog|autocomplete-item-shared|formautofill)\.css$/i,
+ errorMessage: /Unknown property.*-moz-/i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName: /(scrollbars|xul)\.css$/i,
+ errorMessage: /Unknown pseudo-class.*-moz-/i,
+ isFromDevTools: false,
+ },
+ // Reserved to UA sheets unless layout.css.overflow-clip-box.enabled flipped to true.
+ {
+ sourceName: /(?:res|gre-resources)\/forms\.css$/i,
+ errorMessage: /Unknown property.*overflow-clip-box/i,
+ isFromDevTools: false,
+ },
+ // These variables are declared somewhere else, and error when we load the
+ // files directly. They're all marked intermittent because their appearance
+ // in the error console seems to not be consistent.
+ {
+ sourceName: /jsonview\/css\/general\.css$/i,
+ intermittent: true,
+ errorMessage: /Property contained reference to invalid variable.*color/i,
+ isFromDevTools: true,
+ },
+ // PDF.js uses a property that is currently only supported in chrome.
+ {
+ sourceName: /web\/viewer\.css$/i,
+ errorMessage:
+ /Unknown property ‘text-size-adjust’\. {2}Declaration dropped\./i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName: /overlay\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
+ isFromDevTools: false,
+ },
+];
+
+if (!Services.prefs.getBoolPref("layout.css.color-mix.enabled")) {
+ // Reserved to UA sheets unless layout.css.color-mix.enabled flipped to true.
+ whitelist.push({
+ sourceName: /\b(autocomplete-item)\.css$/,
+ errorMessage: /Expected color but found \u2018color-mix\u2019./i,
+ isFromDevTools: false,
+ platforms: ["windows"],
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.math-depth.enabled")) {
+ // mathml.css UA sheet rule for math-depth.
+ whitelist.push({
+ sourceName: /\b(scrollbars|mathml)\.css$/i,
+ errorMessage: /Unknown property .*\bmath-depth\b/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.math-style.enabled")) {
+ // mathml.css UA sheet rule for math-style.
+ whitelist.push({
+ sourceName: /(?:res|gre-resources)\/mathml\.css$/i,
+ errorMessage: /Unknown property .*\bmath-style\b/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.scroll-anchoring.enabled")) {
+ whitelist.push({
+ sourceName: /webconsole\.css$/i,
+ errorMessage: /Unknown property .*\boverflow-anchor\b/i,
+ isFromDevTools: true,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.forced-colors.enabled")) {
+ whitelist.push({
+ sourceName: /pdf\.js\/web\/viewer\.css$/,
+ errorMessage: /Expected media feature name but found ‘forced-colors’*/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.forced-color-adjust.enabled")) {
+ // PDF.js uses a property that is currently not enabled.
+ whitelist.push({
+ sourceName: /web\/viewer\.css$/i,
+ errorMessage:
+ /Unknown property ‘forced-color-adjust’\. {2}Declaration dropped\./i,
+ isFromDevTools: false,
+ });
+}
+
+let propNameWhitelist = [
+ // These custom properties are retrieved directly from CSSOM
+ // in videocontrols.xml to get pre-defined style instead of computed
+ // dimensions, which is why they are not referenced by CSS.
+ { propName: "--clickToPlay-width", isFromDevTools: false },
+ { propName: "--playButton-width", isFromDevTools: false },
+ { propName: "--muteButton-width", isFromDevTools: false },
+ { propName: "--castingButton-width", isFromDevTools: false },
+ { propName: "--closedCaptionButton-width", isFromDevTools: false },
+ { propName: "--fullscreenButton-width", isFromDevTools: false },
+ { propName: "--durationSpan-width", isFromDevTools: false },
+ { propName: "--durationSpan-width-long", isFromDevTools: false },
+ { propName: "--positionDurationBox-width", isFromDevTools: false },
+ { propName: "--positionDurationBox-width-long", isFromDevTools: false },
+
+ // These variables are used in a shorthand, but the CSS parser deletes the values
+ // when expanding the shorthands. See https://github.com/w3c/csswg-drafts/issues/2515
+ { propName: "--bezier-diagonal-color", isFromDevTools: true },
+
+ // This variable is used from CSS embedded in JS in adjustableTitle.js
+ { propName: "--icon-url", isFromDevTools: false },
+
+ // These are referenced from devtools files.
+ {
+ propName: "--browser-stack-z-index-devtools-splitter",
+ isFromDevTools: false,
+ },
+ { propName: "--browser-stack-z-index-rdm-toolbar", isFromDevTools: false },
+];
+
+// Add suffix to stylesheets' URI so that we always load them here and
+// have them parsed. Add a random number so that even if we run this
+// test multiple times, it would be unlikely to affect each other.
+const kPathSuffix = "?always-parse-css-" + Math.random();
+
+function dumpWhitelistItem(item) {
+ return JSON.stringify(item, (key, value) => {
+ return value instanceof RegExp ? value.toString() : value;
+ });
+}
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in whitelist
+ *
+ * @param aErrorObject the error to check
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(aErrorObject) {
+ for (let whitelistItem of whitelist) {
+ let matches = true;
+ let catchAll = true;
+ for (let prop of ["sourceName", "errorMessage"]) {
+ if (whitelistItem.hasOwnProperty(prop)) {
+ catchAll = false;
+ if (!whitelistItem[prop].test(aErrorObject[prop] || "")) {
+ matches = false;
+ break;
+ }
+ }
+ }
+ if (catchAll) {
+ ok(
+ false,
+ "A whitelist item is catching all errors. " +
+ dumpWhitelistItem(whitelistItem)
+ );
+ continue;
+ }
+ if (matches) {
+ whitelistItem.used = true;
+ let { sourceName, errorMessage } = aErrorObject;
+ info(
+ `Ignored error "${errorMessage}" on ${sourceName} ` +
+ "because of whitelist item " +
+ dumpWhitelistItem(whitelistItem)
+ );
+ return true;
+ }
+ }
+ return false;
+}
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+);
+var gChromeMap = new Map();
+
+var resHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+var gResourceMap = [];
+function trackResourcePrefix(prefix) {
+ let uri = Services.io.newURI("resource://" + prefix + "/");
+ gResourceMap.unshift([prefix, resHandler.resolveURI(uri)]);
+}
+trackResourcePrefix("gre");
+trackResourcePrefix("app");
+
+function getBaseUriForChromeUri(chromeUri) {
+ let chromeFile = chromeUri + "nonexistentfile.reallynothere";
+ let uri = Services.io.newURI(chromeFile);
+ let fileUri = gChromeReg.convertChromeURL(uri);
+ return fileUri.resolve(".");
+}
+
+function parseManifest(manifestUri) {
+ return fetchFile(manifestUri.spec).then(data => {
+ for (let line of data.split("\n")) {
+ let [type, ...argv] = line.split(/\s+/);
+ if (type == "content" || type == "skin") {
+ let chromeUri = `chrome://${argv[0]}/${type}/`;
+ gChromeMap.set(getBaseUriForChromeUri(chromeUri), chromeUri);
+ } else if (type == "resource") {
+ trackResourcePrefix(argv[0]);
+ }
+ }
+ });
+}
+
+function convertToCodeURI(fileUri) {
+ let baseUri = fileUri;
+ let path = "";
+ while (true) {
+ let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
+ if (slashPos <= 0) {
+ // File not accessible from chrome protocol, try resource://
+ for (let res of gResourceMap) {
+ if (fileUri.startsWith(res[1])) {
+ return fileUri.replace(res[1], "resource://" + res[0] + "/");
+ }
+ }
+ // Give up and return the original URL.
+ return fileUri;
+ }
+ path = baseUri.slice(slashPos + 1) + path;
+ baseUri = baseUri.slice(0, slashPos + 1);
+ if (gChromeMap.has(baseUri)) {
+ return gChromeMap.get(baseUri) + path;
+ }
+ }
+}
+
+function messageIsCSSError(msg) {
+ // Only care about CSS errors generated by our iframe:
+ if (
+ msg instanceof Ci.nsIScriptError &&
+ msg.category.includes("CSS") &&
+ msg.sourceName.endsWith(kPathSuffix)
+ ) {
+ let sourceName = msg.sourceName.slice(0, -kPathSuffix.length);
+ let msgInfo = { sourceName, errorMessage: msg.errorMessage };
+ // Check if this error is whitelisted in whitelist
+ if (!ignoredError(msgInfo)) {
+ ok(false, `Got error message for ${sourceName}: ${msg.errorMessage}`);
+ return true;
+ }
+ }
+ return false;
+}
+
+let imageURIsToReferencesMap = new Map();
+let customPropsToReferencesMap = new Map();
+
+function neverMatches(mediaList) {
+ const perPlatformMediaQueryMap = {
+ macosx: ["(-moz-platform: macos)"],
+ win: [
+ "(-moz-platform: windows)",
+ "(-moz-platform: windows-win7)",
+ "(-moz-platform: windows-win8)",
+ "(-moz-platform: windows-win10)",
+ ],
+ linux: ["(-moz-platform: linux)"],
+ android: ["(-moz-platform: android)"],
+ };
+ for (let platform in perPlatformMediaQueryMap) {
+ const inThisPlatform = platform === AppConstants.platform;
+ for (const media of perPlatformMediaQueryMap[platform]) {
+ if (inThisPlatform && mediaList.mediaText == "not " + media) {
+ // This query can't match on this platform.
+ return true;
+ }
+ if (!inThisPlatform && mediaList.mediaText == media) {
+ // This query only matches on another platform that isn't ours.
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function processCSSRules(container) {
+ for (let rule of container.cssRules) {
+ if (rule.media && neverMatches(rule.media)) {
+ continue;
+ }
+ if (rule.styleSheet) {
+ processCSSRules(rule.styleSheet); // @import
+ continue;
+ }
+ if (rule.cssRules) {
+ processCSSRules(rule); // @supports, @media, @layer (block), @keyframes
+ continue;
+ }
+ if (!rule.style) {
+ continue; // @layer (statement), @font-feature-values, @counter-style
+ }
+ // Extract urls from the css text.
+ // Note: CSSRule.style.cssText always has double quotes around URLs even
+ // when the original CSS file didn't.
+ let cssText = rule.style.cssText;
+ let urls = cssText.match(/url\("[^"]*"\)/g);
+ // Extract props by searching all "--" preceded by "var(" or a non-word
+ // character.
+ let props = cssText.match(/(var\(|\W|^)(--[\w\-]+)/g);
+ if (!urls && !props) {
+ continue;
+ }
+
+ for (let url of urls || []) {
+ // Remove the url(" prefix and the ") suffix.
+ url = url.replace(/url\("(.*)"\)/, "$1");
+ if (url.startsWith("data:")) {
+ continue;
+ }
+
+ // Make the url absolute and remove the ref.
+ let baseURI = Services.io.newURI(rule.parentStyleSheet.href);
+ url = Services.io.newURI(url, null, baseURI).specIgnoringRef;
+
+ // Store the image url along with the css file referencing it.
+ let baseUrl = baseURI.spec.split("?always-parse-css")[0];
+ if (!imageURIsToReferencesMap.has(url)) {
+ imageURIsToReferencesMap.set(url, new Set([baseUrl]));
+ } else {
+ imageURIsToReferencesMap.get(url).add(baseUrl);
+ }
+ }
+
+ for (let prop of props || []) {
+ if (prop.startsWith("var(")) {
+ prop = prop.substring(4);
+ let prevValue = customPropsToReferencesMap.get(prop) || 0;
+ customPropsToReferencesMap.set(prop, prevValue + 1);
+ } else {
+ // Remove the extra non-word character captured by the regular
+ // expression if needed.
+ if (prop[0] != "-") {
+ prop = prop.substring(1);
+ }
+ if (!customPropsToReferencesMap.has(prop)) {
+ customPropsToReferencesMap.set(prop, undefined);
+ }
+ }
+ }
+ }
+}
+
+function chromeFileExists(aURI) {
+ let available = 0;
+ try {
+ let channel = NetUtil.newChannel({
+ uri: aURI,
+ loadUsingSystemPrincipal: true,
+ });
+ let stream = channel.open();
+ let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sstream.init(stream);
+ available = sstream.available();
+ sstream.close();
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ dump("Checking " + aURI + ": " + e + "\n");
+ console.error(e);
+ }
+ }
+ return available > 0;
+}
+
+add_task(async function checkAllTheCSS() {
+ // Since we later in this test use Services.console.getMessageArray(),
+ // better to not have some messages from previous tests in the array.
+ Services.console.reset();
+
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let uris = await generateURIsFromDirTree(appDir, [".css", ".manifest"]);
+
+ // Create a clean iframe to load all the files into. This needs to live at a
+ // chrome URI so that it's allowed to load and parse any styles.
+ let testFile = getRootDirectory(gTestPath) + "dummy_page.html";
+ let { HiddenFrame } = ChromeUtils.importESModule(
+ "resource://gre/modules/HiddenFrame.sys.mjs"
+ );
+ let hiddenFrame = new HiddenFrame();
+ let win = await hiddenFrame.get();
+ let iframe = win.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:iframe"
+ );
+ win.document.documentElement.appendChild(iframe);
+ let iframeLoaded = BrowserTestUtils.waitForEvent(iframe, "load", true);
+ iframe.contentWindow.location = testFile;
+ await iframeLoaded;
+ let doc = iframe.contentWindow.document;
+ iframe.contentWindow.docShell.cssErrorReportingEnabled = true;
+
+ // Parse and remove all manifests from the list.
+ // NOTE that this must be done before filtering out devtools paths
+ // so that all chrome paths can be recorded.
+ let manifestURIs = [];
+ uris = uris.filter(uri => {
+ if (uri.pathQueryRef.endsWith(".manifest")) {
+ manifestURIs.push(uri);
+ return false;
+ }
+ return true;
+ });
+ // Wait for all manifest to be parsed
+ await PerfTestHelpers.throttledMapPromises(manifestURIs, parseManifest);
+
+ // filter out either the devtools paths or the non-devtools paths:
+ let isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
+ let devtoolsPathBits = ["devtools"];
+ uris = uris.filter(
+ uri => isDevtools == devtoolsPathBits.some(path => uri.spec.includes(path))
+ );
+
+ let loadCSS = chromeUri =>
+ new Promise(resolve => {
+ let linkEl, onLoad, onError;
+ onLoad = e => {
+ processCSSRules(linkEl.sheet);
+ resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ onError = e => {
+ ok(
+ false,
+ "Loading " + linkEl.getAttribute("href") + " threw an error!"
+ );
+ resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ linkEl = doc.createElement("link");
+ linkEl.setAttribute("rel", "stylesheet");
+ linkEl.setAttribute("type", "text/css");
+ linkEl.addEventListener("load", onLoad);
+ linkEl.addEventListener("error", onError);
+ linkEl.setAttribute("href", chromeUri + kPathSuffix);
+ doc.head.appendChild(linkEl);
+ });
+
+ // We build a list of promises that get resolved when their respective
+ // files have loaded and produced no errors.
+ const kInContentCommonCSS = "chrome://global/skin/in-content/common.css";
+ let allPromises = uris
+ .map(uri => convertToCodeURI(uri.spec))
+ .filter(uri => uri !== kInContentCommonCSS);
+
+ // Make sure chrome://global/skin/in-content/common.css is loaded before other
+ // stylesheets in order to guarantee the --in-content variables can be
+ // correctly referenced.
+ if (allPromises.length !== uris.length) {
+ await loadCSS(kInContentCommonCSS);
+ }
+
+ // Wait for all the files to have actually loaded:
+ await PerfTestHelpers.throttledMapPromises(allPromises, loadCSS);
+
+ // Check if all the files referenced from CSS actually exist.
+ // Files in browser/ should never be referenced outside browser/.
+ for (let [image, references] of imageURIsToReferencesMap) {
+ if (!chromeFileExists(image)) {
+ for (let ref of references) {
+ ok(false, "missing " + image + " referenced from " + ref);
+ }
+ }
+
+ let imageHost = image.split("/")[2];
+ if (imageHost == "browser") {
+ for (let ref of references) {
+ let refHost = ref.split("/")[2];
+ if (!["activity-stream", "browser"].includes(refHost)) {
+ ok(
+ false,
+ "browser file " + image + " referenced outside browser in " + ref
+ );
+ }
+ }
+ }
+ }
+
+ // Check if all the properties that are defined are referenced.
+ for (let [prop, refCount] of customPropsToReferencesMap) {
+ if (!refCount) {
+ let ignored = false;
+ for (let item of propNameWhitelist) {
+ if (item.propName == prop && isDevtools == item.isFromDevTools) {
+ item.used = true;
+ if (
+ !item.platforms ||
+ item.platforms.includes(AppConstants.platform)
+ ) {
+ ignored = true;
+ }
+ break;
+ }
+ }
+ if (!ignored) {
+ ok(false, "custom property `" + prop + "` is not referenced");
+ }
+ }
+ }
+
+ let messages = Services.console.getMessageArray();
+ // Count errors (the test output will list actual issues for us, as well
+ // as the ok(false) in messageIsCSSError.
+ let errors = messages.filter(messageIsCSSError);
+ is(
+ errors.length,
+ 0,
+ "All the styles (" + allPromises.length + ") loaded without errors."
+ );
+
+ // Confirm that all whitelist rules have been used.
+ function checkWhitelist(list) {
+ for (let item of list) {
+ if (
+ !item.used &&
+ isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform)) &&
+ !item.intermittent
+ ) {
+ ok(false, "Unused whitelist item: " + dumpWhitelistItem(item));
+ }
+ }
+ }
+ checkWhitelist(whitelist);
+ checkWhitelist(propNameWhitelist);
+
+ // Clean up to avoid leaks:
+ doc.head.innerHTML = "";
+ doc = null;
+ iframe.remove();
+ iframe = null;
+ win = null;
+ hiddenFrame.destroy();
+ hiddenFrame = null;
+ imageURIsToReferencesMap = null;
+ customPropsToReferencesMap = null;
+});
diff --git a/browser/base/content/test/static/browser_parsable_script.js b/browser/base/content/test/static/browser_parsable_script.js
new file mode 100644
index 0000000000..982ef2f91a
--- /dev/null
+++ b/browser/base/content/test/static/browser_parsable_script.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' JS issues to remain, while we
+ * detect newly occurring issues in shipping JS. It is a list of regexes
+ * matching files which have errors:
+ */
+
+requestLongerTimeout(2);
+
+const kWhitelist = new Set([
+ /browser\/content\/browser\/places\/controller.js$/,
+]);
+
+const kESModuleList = new Set([
+ /browser\/lockwise-card.js$/,
+ /browser\/monitor-card.js$/,
+ /browser\/proxy-card.js$/,
+ /browser\/vpn-card.js$/,
+ /toolkit\/content\/global\/certviewer\/components\/.*\.js$/,
+ /toolkit\/content\/global\/certviewer\/.*\.js$/,
+ /chrome\/pdfjs\/content\/web\/.*\.js$/,
+]);
+
+// Normally we would use reflect.jsm to get Reflect.parse. However, if
+// we do that, then all the AST data is allocated in reflect.jsm's
+// zone. That exposes a bug in our GC. The GC collects reflect.jsm's
+// zone but not the zone in which our test code lives (since no new
+// data is being allocated in it). The cross-compartment wrappers in
+// our zone that point to the AST data never get collected, and so the
+// AST data itself is never collected. We need to GC both zones at
+// once to fix the problem.
+const init = Cc["@mozilla.org/jsreflect;1"].createInstance();
+init();
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in kWhitelist
+ *
+ * @param uri the uri to check against the whitelist
+ * @return true if the uri should be skipped, false otherwise.
+ */
+function uriIsWhiteListed(uri) {
+ for (let whitelistItem of kWhitelist) {
+ if (whitelistItem.test(uri.spec)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Check if a URI should be parsed as an ES module.
+ *
+ * @param uri the uri to check against the ES module list
+ * @return true if the uri should be parsed as a module, otherwise parse it as a script.
+ */
+function uriIsESModule(uri) {
+ if (uri.filePath.endsWith(".mjs")) {
+ return true;
+ }
+
+ for (let whitelistItem of kESModuleList) {
+ if (whitelistItem.test(uri.spec)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function parsePromise(uri, parseTarget) {
+ let promise = new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function () {
+ if (this.readyState == this.DONE) {
+ let scriptText = this.responseText;
+ try {
+ info(`Checking ${parseTarget} ${uri}`);
+ let parseOpts = {
+ source: uri,
+ target: parseTarget,
+ };
+ Reflect.parse(scriptText, parseOpts);
+ resolve(true);
+ } catch (ex) {
+ let errorMsg = "Script error reading " + uri + ": " + ex;
+ ok(false, errorMsg);
+ resolve(false);
+ }
+ }
+ };
+ xhr.onerror = error => {
+ ok(false, "XHR error reading " + uri + ": " + error);
+ resolve(false);
+ };
+ xhr.overrideMimeType("application/javascript");
+ xhr.send(null);
+ });
+ return promise;
+}
+
+add_task(async function checkAllTheJS() {
+ // In debug builds, even on a fast machine, collecting the file list may take
+ // more than 30 seconds, and parsing all files may take four more minutes.
+ // For this reason, this test must be explictly requested in debug builds by
+ // using the "--setpref parse=<filter>" argument to mach. You can specify:
+ // - A case-sensitive substring of the file name to test (slow).
+ // - A single absolute URI printed out by a previous run (fast).
+ // - An empty string to run the test on all files (slowest).
+ let parseRequested = Services.prefs.prefHasUserValue("parse");
+ let parseValue = parseRequested && Services.prefs.getCharPref("parse");
+ if (SpecialPowers.isDebugBuild) {
+ if (!parseRequested) {
+ ok(
+ true,
+ "Test disabled on debug build. To run, execute: ./mach" +
+ " mochitest-browser --setpref parse=<case_sensitive_filter>" +
+ " browser/base/content/test/general/browser_parsable_script.js"
+ );
+ return;
+ }
+ // Request a 15 minutes timeout (30 seconds * 30) for debug builds.
+ requestLongerTimeout(30);
+ }
+
+ let uris;
+ // If an absolute URI is specified on the command line, use it immediately.
+ if (parseValue && parseValue.includes(":")) {
+ uris = [NetUtil.newURI(parseValue)];
+ } else {
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let startTimeMs = Date.now();
+ info("Collecting URIs");
+ uris = await generateURIsFromDirTree(appDir, [".js", ".jsm", ".mjs"]);
+ info("Collected URIs in " + (Date.now() - startTimeMs) + "ms");
+
+ // Apply the filter specified on the command line, if any.
+ if (parseValue) {
+ uris = uris.filter(uri => {
+ if (uri.spec.includes(parseValue)) {
+ return true;
+ }
+ info("Not checking filtered out " + uri.spec);
+ return false;
+ });
+ }
+ }
+
+ // We create an array of promises so we can parallelize all our parsing
+ // and file loading activity:
+ await PerfTestHelpers.throttledMapPromises(uris, uri => {
+ if (uriIsWhiteListed(uri)) {
+ info("Not checking whitelisted " + uri.spec);
+ return undefined;
+ }
+ let target = "script";
+ if (uriIsESModule(uri)) {
+ target = "module";
+ }
+ return parsePromise(uri.spec, target);
+ });
+ ok(true, "All files parsed");
+});
diff --git a/browser/base/content/test/static/browser_sentence_case_strings.js b/browser/base/content/test/static/browser_sentence_case_strings.js
new file mode 100644
index 0000000000..e995f76b1a
--- /dev/null
+++ b/browser/base/content/test/static/browser_sentence_case_strings.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test file checks that our en-US builds use sentence case strings
+ * where appropriate. It's not exhaustive - some panels will show different
+ * items in different states, and this test doesn't iterate all of them.
+ */
+
+/* global PanelUI */
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+const { AppMenuNotifications } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppMenuNotifications.sys.mjs"
+);
+
+// These are brand names, proper names, or other things that we expect to
+// not abide exactly to sentence case. NAMES is for single words, and PHRASES
+// is for words in a specific order.
+const NAMES = new Set(["Mozilla", "Nightly", "Firefox"]);
+const PHRASES = new Set(["Troubleshoot Mode…"]);
+
+let gCUITestUtils = new CustomizableUITestUtils(window);
+let gLocalization = new Localization(["browser/newtab/asrouter.ftl"], true);
+
+/**
+ * This recursive function will take the current main or subview, find all of
+ * the buttons that navigate to subviews inside it, and click each one
+ * individually. Upon entering the new view, we recurse. When the subviews
+ * within a view have been exhausted, we go back up a level.
+ *
+ * @generator
+ * @param {<xul:panelview>} parentView The view to start scanning for
+ * subviews.
+ * @yields {<xul:panelview>} Each found <xul:panelview>, in depth-first search
+ * order.
+ */
+async function* iterateSubviews(parentView) {
+ let navButtons = Array.from(
+ // Ensure that only enabled buttons are tested
+ parentView.querySelectorAll(".subviewbutton-nav:not([disabled])")
+ );
+ if (!navButtons) {
+ return;
+ }
+
+ for (let button of navButtons) {
+ info("Click " + button.id);
+ let panel = parentView.closest("panel");
+ let panelmultiview = parentView.closest("panelmultiview");
+ let promiseViewShown = BrowserTestUtils.waitForEvent(panel, "ViewShown");
+ button.click();
+ let viewShownEvent = await promiseViewShown;
+
+ yield viewShownEvent.originalTarget;
+
+ info("Shown " + viewShownEvent.originalTarget.id);
+ yield* iterateSubviews(viewShownEvent.originalTarget);
+ promiseViewShown = BrowserTestUtils.waitForEvent(parentView, "ViewShown");
+ panelmultiview.goBack();
+ await promiseViewShown;
+ }
+}
+
+/**
+ * Given a <xul:panelview>, look for <xul:toolbarbutton> descendants, extract
+ * any relevant strings from them, and check to see if they are in sentence
+ * case. By default, labels, textContent, and toolTipText (including dynamic
+ * toolTipText) are checked.
+ *
+ * @param {<xul:panelview>} view The <xul:panelview> to check.
+ */
+function checkToolbarButtons(view) {
+ let toolbarbuttons = view.querySelectorAll("toolbarbutton");
+ info("Checking toolbarbuttons in subview with id " + view.id);
+
+ for (let toolbarbutton of toolbarbuttons) {
+ let strings = [
+ toolbarbutton.label,
+ toolbarbutton.textContent,
+ toolbarbutton.toolTipText,
+ GetDynamicShortcutTooltipText(toolbarbutton.id),
+ ];
+ info("Checking toolbarbutton " + toolbarbutton.id);
+ for (let string of strings) {
+ checkSentenceCase(string, toolbarbutton.id);
+ }
+ }
+}
+
+function checkSubheaders(view) {
+ let subheaders = view.querySelectorAll("h2");
+ info("Checking subheaders in subview with id " + view.id);
+
+ for (let subheader of subheaders) {
+ checkSentenceCase(subheader.textContent, subheader.id);
+ }
+}
+
+async function checkUpdateBanner(view) {
+ let banner = view.querySelector("#appMenu-proton-update-banner");
+
+ const notifications = [
+ "update-downloading",
+ "update-available",
+ "update-manual",
+ "update-unsupported",
+ "update-restart",
+ ];
+
+ for (const notification of notifications) {
+ // Forcibly remove the label in order to wait for the new label.
+ banner.removeAttribute("label");
+
+ let labelPromise = BrowserTestUtils.waitForMutationCondition(
+ banner,
+ { attributes: true, attributeFilter: ["label"] },
+ () => !!banner.getAttribute("label")
+ );
+
+ AppMenuNotifications.showNotification(notification);
+
+ await labelPromise;
+
+ checkSentenceCase(banner.label, banner.id);
+
+ AppMenuNotifications.removeNotification(/.*/);
+ }
+}
+
+/**
+ * Asserts whether or not a string matches sentence case.
+ *
+ * @param {String} string The string to check for sentence case.
+ * @param {String} elementID The ID of the element being tested. This is
+ * mainly used for the assertion message to make it easier to debug
+ * failures, but items without IDs will not be checked (as these are
+ * likely using dynamic strings, like bookmarked page titles).
+ */
+function checkSentenceCase(string, elementID) {
+ if (!string || !elementID) {
+ return;
+ }
+
+ info("Testing string: " + string);
+
+ let words = string.trim().split(/\s+/);
+
+ // We expect that the first word is always capitalized. If it isn't,
+ // there's no need to keep checking the rest of the string, since we're
+ // going to fail the assertion.
+ let result = hasExpectedCapitalization(words[0], true);
+ if (result) {
+ for (let wordIndex = 1; wordIndex < words.length; ++wordIndex) {
+ let word = words[wordIndex];
+
+ if (word) {
+ if (isPartOfPhrase(words, wordIndex)) {
+ result = hasExpectedCapitalization(word, true);
+ } else {
+ let isName = NAMES.has(word);
+ result = hasExpectedCapitalization(word, isName);
+ }
+ if (!result) {
+ break;
+ }
+ }
+ }
+ }
+
+ Assert.ok(result, `${string} for ${elementID} should have sentence casing.`);
+}
+
+/**
+ * Returns true if a word is part of a phrase defined in the PHRASES set.
+ * The function will see if the word is contained within any of the defined
+ * PHRASES, and will then scan back and forward within the words array to
+ * to see if the word is indeed part of the phrase in context.
+ *
+ * @param {Array} words The full array of words being checked by the caller.
+ * @param {Number} wordIndex The index of the word being checked within the
+ * words array.
+ * @return {Boolean}
+ */
+function isPartOfPhrase(words, wordIndex) {
+ let word = words[wordIndex];
+
+ info(`Checking if ${word} is part of a phrase`);
+
+ for (let phrase of PHRASES) {
+ let phraseFragments = phrase.split(" ");
+ let fragmentIndex = phraseFragments.indexOf(word);
+
+ // If we didn't find the word within this phrase, the candidate phrase
+ // has more words than what we're analyzing, or the word doesn't have
+ // enough words before it to match the candidate phrase, then move on.
+ if (
+ fragmentIndex == -1 ||
+ words.length - phraseFragments.length < 0 ||
+ fragmentIndex > wordIndex
+ ) {
+ continue;
+ }
+
+ let wordsSlice = words.slice(
+ wordIndex - fragmentIndex,
+ wordIndex + phraseFragments.length
+ );
+ let matches = wordsSlice.every((w, index) => {
+ return phraseFragments[index] === w;
+ });
+
+ if (matches) {
+ info(`${word} is part of phrase ${phrase}`);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Tests that the strings under the AppMenu are in sentence case.
+ */
+add_task(async function test_sentence_case_appmenu() {
+ // Some of these panels are lazy, so it's necessary to open them in
+ // order for them to be inserted into the DOM.
+ await gCUITestUtils.openMainMenu();
+ registerCleanupFunction(async () => {
+ await gCUITestUtils.hideMainMenu();
+ });
+
+ checkToolbarButtons(PanelUI.mainView);
+ checkSubheaders(PanelUI.mainView);
+
+ for await (const view of iterateSubviews(PanelUI.mainView)) {
+ checkToolbarButtons(view);
+ checkSubheaders(view);
+ }
+
+ await checkUpdateBanner(PanelUI.mainView);
+});
+
+/**
+ * Tests that the strings under the All Tabs panel are in sentence case.
+ */
+add_task(async function test_sentence_case_all_tabs_panel() {
+ gTabsPanel.init();
+
+ const allTabsView = document.getElementById("allTabsMenu-allTabsView");
+ let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
+ allTabsView,
+ "ViewShown"
+ );
+ gTabsPanel.showAllTabsPanel();
+ await allTabsPopupShownPromise;
+
+ registerCleanupFunction(async () => {
+ let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(
+ allTabsView.panelMultiView,
+ "PanelMultiViewHidden"
+ );
+ gTabsPanel.hideAllTabsPanel();
+ await allTabsPopupHiddenPromise;
+ });
+
+ checkToolbarButtons(gTabsPanel.allTabsView);
+ checkSubheaders(gTabsPanel.allTabsView);
+
+ for await (const view of iterateSubviews(gTabsPanel.allTabsView)) {
+ checkToolbarButtons(view);
+ checkSubheaders(view);
+ }
+});
diff --git a/browser/base/content/test/static/browser_title_case_menus.js b/browser/base/content/test/static/browser_title_case_menus.js
new file mode 100644
index 0000000000..9251db057b
--- /dev/null
+++ b/browser/base/content/test/static/browser_title_case_menus.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test file checks that our en-US builds use APA-style Title Case strings
+ * where appropriate.
+ */
+
+// MINOR_WORDS are words that are okay to not be capitalized when they're
+// mid-string.
+//
+// Source: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case
+const MINOR_WORDS = [
+ "a",
+ "an",
+ "and",
+ "as",
+ "at",
+ "but",
+ "by",
+ "for",
+ "if",
+ "in",
+ "nor",
+ "of",
+ "off",
+ "on",
+ "or",
+ "per",
+ "so",
+ "the",
+ "to",
+ "up",
+ "via",
+ "yet",
+];
+
+/**
+ * Returns a generator that will yield all of the <xul:menupopups>
+ * beneath <xul:menu> elements within a given <xul:menubar>. Each
+ * <xul:menupopup> will have the "popupshowing" and "popupshown"
+ * event fired on them to give them an opportunity to fully populate
+ * themselves before being yielded.
+ *
+ * @generator
+ * @param {<xul:menubar>} menubar The <xul:menubar> to get <xul:menupopup>s
+ * for.
+ * @yields {<xul:menupopup>} The next <xul:menupopup> under the <xul:menubar>.
+ */
+async function* iterateMenuPopups(menubar) {
+ let menus = menubar.querySelectorAll("menu");
+
+ for (let menu of menus) {
+ for (let menupopup of menu.querySelectorAll("menupopup")) {
+ // We fake the popupshowing and popupshown events to give the menupopups
+ // an opportunity to fully populate themselves. We don't actually open
+ // the menupopups because this is not possible on macOS.
+ menupopup.dispatchEvent(
+ new MouseEvent("popupshowing", { bubbles: true })
+ );
+ menupopup.dispatchEvent(new MouseEvent("popupshown", { bubbles: true }));
+
+ yield menupopup;
+
+ // Just for good measure, we'll fire the popuphiding/popuphidden events
+ // after we close the menupopups.
+ menupopup.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true }));
+ menupopup.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true }));
+ }
+ }
+}
+
+/**
+ * Given a <xul:menupopup>, checks all of the child elements with label
+ * properties to see if those labels are Title Cased. Skips any elements that
+ * have an empty or undefined label property.
+ *
+ * @param {<xul:menupopup>} menupopup The <xul:menupopup> to check.
+ */
+function checkMenuItems(menupopup) {
+ info("Checking menupopup with id " + menupopup.id);
+ for (let child of menupopup.children) {
+ if (child.label) {
+ info("Checking menupopup child with id " + child.id);
+ checkTitleCase(child.label, child.id);
+ }
+ }
+}
+
+/**
+ * Given a string, checks that the string is in Title Case.
+ *
+ * @param {String} string The string to check.
+ * @param {String} elementID The ID of the element associated with the string.
+ * This is included in the assertion message.
+ */
+function checkTitleCase(string, elementID) {
+ if (!string || !elementID /* document this */) {
+ return;
+ }
+
+ let words = string.trim().split(/\s+/);
+
+ // We extract the first word, and always expect it to be capitalized,
+ // even if it's a short word like one of MINOR_WORDS.
+ let firstWord = words.shift();
+ let result = hasExpectedCapitalization(firstWord, true);
+ if (result) {
+ for (let word of words) {
+ if (word) {
+ let expectCapitalized = !MINOR_WORDS.includes(word);
+ result = hasExpectedCapitalization(word, expectCapitalized);
+ if (!result) {
+ break;
+ }
+ }
+ }
+ }
+
+ Assert.ok(result, `${string} for ${elementID} should have Title Casing.`);
+}
+
+/**
+ * On Windows, macOS and GTK/KDE Linux, menubars are expected to be in Title
+ * Case in order to feel native. This test iterates the menuitem labels of the
+ * main menubar to ensure the en-US strings are all in Title Case.
+ *
+ * We use APA-style Title Case for the menubar, rather than Photon-style Title
+ * Case (https://design.firefox.com/photon/copy/capitalization.html) to match
+ * the native platform conventions.
+ */
+add_task(async function apa_test_title_case_menubar() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let menuToolbar = newWin.document.getElementById("main-menubar");
+
+ for await (const menupopup of iterateMenuPopups(menuToolbar)) {
+ checkMenuItems(menupopup);
+ }
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+/**
+ * This test iterates the menuitem labels of the macOS dock menu for the
+ * application to ensure the en-US strings are all in Title Case.
+ */
+add_task(async function apa_test_title_case_macos_dock_menu() {
+ if (AppConstants.platform != "macosx") {
+ return;
+ }
+
+ let hiddenWindow = Services.appShell.hiddenDOMWindow;
+ Assert.ok(hiddenWindow, "Could get at hidden window");
+ let menupopup = hiddenWindow.document.getElementById("menu_mac_dockmenu");
+ checkMenuItems(menupopup);
+});
diff --git a/browser/base/content/test/static/bug1262648_string_with_newlines.dtd b/browser/base/content/test/static/bug1262648_string_with_newlines.dtd
new file mode 100644
index 0000000000..86cbefa5bd
--- /dev/null
+++ b/browser/base/content/test/static/bug1262648_string_with_newlines.dtd
@@ -0,0 +1,3 @@
+<!ENTITY foo.bar "This string
+contains
+newlines!">
diff --git a/browser/base/content/test/static/dummy_page.html b/browser/base/content/test/static/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/base/content/test/static/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/base/content/test/static/head.js b/browser/base/content/test/static/head.js
new file mode 100644
index 0000000000..d9b978e853
--- /dev/null
+++ b/browser/base/content/test/static/head.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* Shorthand constructors to construct an nsI(Local)File and zip reader: */
+const LocalFile = new Components.Constructor(
+ "@mozilla.org/file/local;1",
+ Ci.nsIFile,
+ "initWithPath"
+);
+const ZipReader = new Components.Constructor(
+ "@mozilla.org/libjar/zip-reader;1",
+ "nsIZipReader",
+ "open"
+);
+
+const IS_ALPHA = /^[a-z]+$/i;
+
+var { PerfTestHelpers } = ChromeUtils.importESModule(
+ "resource://testing-common/PerfTestHelpers.sys.mjs"
+);
+
+/**
+ * Returns a promise that is resolved with a list of files that have one of the
+ * extensions passed, represented by their nsIURI objects, which exist inside
+ * the directory passed.
+ *
+ * @param dir the directory which to scan for files (nsIFile)
+ * @param extensions the extensions of files we're interested in (Array).
+ */
+function generateURIsFromDirTree(dir, extensions) {
+ if (!Array.isArray(extensions)) {
+ extensions = [extensions];
+ }
+ let dirQueue = [dir.path];
+ return (async function () {
+ let rv = [];
+ while (dirQueue.length) {
+ let nextDir = dirQueue.shift();
+ let { subdirs, files } = await iterateOverPath(nextDir, extensions);
+ dirQueue.push(...subdirs);
+ rv.push(...files);
+ }
+ return rv;
+ })();
+}
+
+/**
+ * Iterate over the children of |path| and find subdirectories and files with
+ * the given extension.
+ *
+ * This function recurses into ZIP and JAR archives as well.
+ *
+ * @param {string} path The path to check.
+ * @param {string[]} extensions The file extensions we're interested in.
+ *
+ * @returns {Promise<object>}
+ * A promise that resolves to an object containing the following
+ * properties:
+ * - files: an array of nsIURIs corresponding to
+ * files that match the extensions passed
+ * - subdirs: an array of paths for subdirectories we need to recurse
+ * into (handled by generateURIsFromDirTree above)
+ */
+async function iterateOverPath(path, extensions) {
+ const children = await IOUtils.getChildren(path);
+
+ const files = [];
+ const subdirs = [];
+
+ for (const entry of children) {
+ let stat;
+ try {
+ stat = await IOUtils.stat(entry);
+ } catch (error) {
+ if (error.name === "NotFoundError") {
+ // Ignore symlinks from prior builds to subsequently removed files
+ continue;
+ }
+ throw error;
+ }
+
+ if (stat.type === "directory") {
+ subdirs.push(entry);
+ } else if (extensions.some(extension => entry.endsWith(extension))) {
+ if (await IOUtils.exists(entry)) {
+ const spec = PathUtils.toFileURI(entry);
+ files.push(Services.io.newURI(spec));
+ }
+ } else if (
+ entry.endsWith(".ja") ||
+ entry.endsWith(".jar") ||
+ entry.endsWith(".zip") ||
+ entry.endsWith(".xpi")
+ ) {
+ const file = new LocalFile(entry);
+ for (const extension of extensions) {
+ files.push(...generateEntriesFromJarFile(file, extension));
+ }
+ }
+ }
+
+ return { files, subdirs };
+}
+
+/* Helper function to generate a URI spec (NB: not an nsIURI yet!)
+ * given an nsIFile object */
+function getURLForFile(file) {
+ let fileHandler = Services.io.getProtocolHandler("file");
+ fileHandler = fileHandler.QueryInterface(Ci.nsIFileProtocolHandler);
+ return fileHandler.getURLSpecFromActualFile(file);
+}
+
+/**
+ * A generator that generates nsIURIs for particular files found in jar files
+ * like omni.ja.
+ *
+ * @param jarFile an nsIFile object for the jar file that needs checking.
+ * @param extension the extension we're interested in.
+ */
+function* generateEntriesFromJarFile(jarFile, extension) {
+ let zr = new ZipReader(jarFile);
+ const kURIStart = getURLForFile(jarFile);
+
+ for (let entry of zr.findEntries("*" + extension + "$")) {
+ // Ignore the JS cache which is stored in omni.ja
+ if (entry.startsWith("jsloader") || entry.startsWith("jssubloader")) {
+ continue;
+ }
+ let entryURISpec = "jar:" + kURIStart + "!/" + entry;
+ yield Services.io.newURI(entryURISpec);
+ }
+ zr.close();
+}
+
+function fetchFile(uri) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "text";
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function () {
+ if (this.readyState != this.DONE) {
+ return;
+ }
+ try {
+ resolve(this.responseText);
+ } catch (ex) {
+ ok(false, `Script error reading ${uri}: ${ex}`);
+ resolve("");
+ }
+ };
+ xhr.onerror = error => {
+ ok(false, `XHR error reading ${uri}: ${error}`);
+ resolve("");
+ };
+ xhr.send(null);
+ });
+}
+
+/**
+ * Returns whether or not a word (presumably in en-US) is capitalized per
+ * expectations.
+ *
+ * @param {String} word The single word to check.
+ * @param {boolean} expectCapitalized True if the word should be capitalized.
+ * @returns {boolean} True if the word matches the expected capitalization.
+ */
+function hasExpectedCapitalization(word, expectCapitalized) {
+ let firstChar = word[0];
+ if (!IS_ALPHA.test(firstChar)) {
+ return true;
+ }
+
+ let isCapitalized = firstChar == firstChar.toLocaleUpperCase("en-US");
+ return isCapitalized == expectCapitalized;
+}
diff --git a/browser/base/content/test/statuspanel/browser.ini b/browser/base/content/test/statuspanel/browser.ini
new file mode 100644
index 0000000000..998c5ab3b3
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_show_statuspanel_idn.js]
+skip-if = verify
+[browser_show_statuspanel_twice.js]
diff --git a/browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js b/browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js
new file mode 100644
index 0000000000..62e35448f0
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = encodeURI(
+ `data:text/html;charset=utf-8,<a id="foo" href="http://nic.xn--rhqv96g/">abc</a><span id="bar">def</span>`
+);
+const TEST_STATUS_TEXT = "nic.\u4E16\u754C";
+
+/**
+ * Test that if the StatusPanel displays an IDN
+ * (Bug 1450538).
+ */
+add_task(async function test_show_statuspanel_twice() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URL
+ );
+
+ let promise = promiseStatusPanelShown(window, TEST_STATUS_TEXT);
+ SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.links[0].focus();
+ });
+ await promise;
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js b/browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js
new file mode 100644
index 0000000000..6ed9b6d3a8
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_URL = "http://example.com";
+
+/**
+ * Test that if the StatusPanel is shown for a link, and then
+ * hidden, that it can be shown again for that same link.
+ * (Bug 1445455).
+ */
+add_task(async function test_show_statuspanel_twice() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ win.XULBrowserWindow.overLink = TEST_URL;
+ win.StatusPanel.update();
+ await promiseStatusPanelShown(win, TEST_URL);
+
+ win.XULBrowserWindow.overLink = "";
+ win.StatusPanel.update();
+ await promiseStatusPanelHidden(win);
+
+ win.XULBrowserWindow.overLink = TEST_URL;
+ win.StatusPanel.update();
+ await promiseStatusPanelShown(win, TEST_URL);
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/statuspanel/head.js b/browser/base/content/test/statuspanel/head.js
new file mode 100644
index 0000000000..23df2e6271
--- /dev/null
+++ b/browser/base/content/test/statuspanel/head.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Returns a Promise that resolves when a StatusPanel for a
+ * window has finished being shown. Also asserts that the
+ * text content of the StatusPanel matches a value.
+ *
+ * @param win (browser window)
+ * The window that the StatusPanel belongs to.
+ * @param value (string)
+ * The value that the StatusPanel should show.
+ * @returns Promise
+ */
+async function promiseStatusPanelShown(win, value) {
+ let panel = win.StatusPanel.panel;
+ info("Waiting to show panel");
+ await BrowserTestUtils.waitForEvent(panel, "transitionend", e => {
+ return (
+ e.propertyName === "opacity" &&
+ win.getComputedStyle(e.target).opacity == "1"
+ );
+ });
+
+ Assert.equal(win.StatusPanel._labelElement.value, value);
+}
+
+/**
+ * Returns a Promise that resolves when a StatusPanel for a
+ * window has finished being hidden.
+ *
+ * @param win (browser window)
+ * The window that the StatusPanel belongs to.
+ */
+async function promiseStatusPanelHidden(win) {
+ let panel = win.StatusPanel.panel;
+ info("Waiting to hide panel");
+ await new Promise(resolve => {
+ let l = e => {
+ if (
+ e.propertyName === "opacity" &&
+ win.getComputedStyle(e.target).opacity == "0"
+ ) {
+ info("Panel hid after " + e.type + " event");
+ panel.removeEventListener("transitionend", l);
+ panel.removeEventListener("transitioncancel", l);
+ is(
+ getComputedStyle(panel).display,
+ "none",
+ "Should be hidden for good"
+ );
+ resolve();
+ }
+ };
+ panel.addEventListener("transitionend", l);
+ panel.addEventListener("transitioncancel", l);
+ });
+}
diff --git a/browser/base/content/test/sync/browser.ini b/browser/base/content/test/sync/browser.ini
new file mode 100644
index 0000000000..e7f6f889a0
--- /dev/null
+++ b/browser/base/content/test/sync/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_contextmenu_sendpage.js]
+[browser_contextmenu_sendtab.js]
+[browser_fxa_badge.js]
+[browser_fxa_web_channel.js]
+https_first_disabled = true
+support-files=
+ browser_fxa_web_channel.html
+[browser_sync.js]
+[browser_synced_tabs_view.js]
diff --git a/browser/base/content/test/sync/browser_contextmenu_sendpage.js b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
new file mode 100644
index 0000000000..503d246f6a
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -0,0 +1,465 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const fxaDevices = [
+ {
+ id: 1,
+ name: "Foo",
+ availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "baz" },
+ lastAccessTime: Date.now(),
+ },
+ {
+ id: 2,
+ name: "Bar",
+ availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "boo" },
+ lastAccessTime: Date.now() + 60000, // add 30min
+ },
+ {
+ id: 3,
+ name: "Baz",
+ clientRecord: "bar",
+ lastAccessTime: Date.now() + 120000, // add 60min
+ }, // Legacy send tab target (no availableCommands).
+ { id: 4, name: "Homer" }, // Incompatible target.
+];
+
+add_setup(async function () {
+ await promiseSyncReady();
+ await Services.search.init();
+ // gSync.init() is called in a requestIdleCallback. Force its initialization.
+ gSync.init();
+ sinon
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = fxaDevices.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sinon.stub(Weave.Service.clientsEngine, "getClientType").returns("desktop");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+});
+
+add_task(async function test_page_contextmenu() {
+ const sandbox = setupSendTabMocks({ fxaDevices });
+
+ await openContentContextMenu("#moztext", "context-sendpagetodevice");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ false,
+ "Send page to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send page to device is enabled"
+ );
+ checkPopup([
+ { label: "Bar" },
+ { label: "Foo" },
+ "----",
+ { label: "Send to All Devices" },
+ { label: "Manage Devices..." },
+ ]);
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_link_contextmenu() {
+ const sandbox = setupSendTabMocks({ fxaDevices });
+ let expectation = sandbox
+ .mock(gSync)
+ .expects("sendTabToDevice")
+ .once()
+ .withExactArgs(
+ "https://www.example.org/",
+ [fxaDevices[1]],
+ "Click on me!!"
+ );
+
+ // Add a link to the page
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let a = content.document.createElement("a");
+ a.href = "https://www.example.org";
+ a.id = "testingLink";
+ a.textContent = "Click on me!!";
+ content.document.body.appendChild(a);
+ });
+
+ let contextMenu = await openContentContextMenu(
+ "#testingLink",
+ "context-sendlinktodevice",
+ "context-sendlinktodevice-popup"
+ );
+
+ let expectedArray = ["context-openlinkintab"];
+
+ if (
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ ContextualIdentityService.getPublicIdentities().length
+ ) {
+ expectedArray.push("context-openlinkinusercontext-menu");
+ }
+
+ expectedArray.push(
+ "context-openlink",
+ "context-openlinkprivate",
+ "context-sep-open",
+ "context-bookmarklink",
+ "context-savelink",
+ "context-savelinktopocket",
+ "context-copylink",
+ "context-sendlinktodevice",
+ "context-sep-sendlinktodevice",
+ "context-searchselect",
+ "frame-sep"
+ );
+
+ if (
+ Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
+ (Services.prefs.getBoolPref("devtools.everOpened", false) ||
+ Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0)
+ ) {
+ expectedArray.push("context-inspect-a11y");
+ }
+
+ expectedArray.push("context-inspect");
+
+ let menu = document.getElementById("contentAreaContextMenu");
+
+ for (let i = 0, j = 0; i < menu.children.length; i++) {
+ let item = menu.children[i];
+ if (item.hidden) {
+ continue;
+ }
+ Assert.equal(
+ item.id,
+ expectedArray[j],
+ "Ids in context menu match expected values"
+ );
+ j++;
+ }
+
+ is(
+ document.getElementById("context-sendlinktodevice").hidden,
+ false,
+ "Send link to device is shown"
+ );
+ is(
+ document.getElementById("context-sendlinktodevice").disabled,
+ false,
+ "Send link to device is enabled"
+ );
+ contextMenu.activateItem(
+ document
+ .getElementById("context-sendlinktodevice-popup")
+ .querySelector("menuitem")
+ );
+ await hideContentContextMenu();
+
+ expectation.verify();
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_no_remote_clients() {
+ const sandbox = setupSendTabMocks({ fxaDevices: [] });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+ checkPopup();
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_one_remote_client() {
+ const sandbox = setupSendTabMocks({
+ fxaDevices: [
+ {
+ id: 1,
+ name: "Foo",
+ availableCommands: {
+ "https://identity.mozilla.com/cmd/open-uri": "baz",
+ },
+ },
+ ],
+ });
+
+ await openContentContextMenu("#moztext", "context-sendpagetodevice");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ false,
+ "Send page to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send page to device is enabled"
+ );
+ checkPopup([{ label: "Foo" }]);
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_not_sendable() {
+ const sandbox = setupSendTabMocks({ fxaDevices, isSendableURI: false });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ true,
+ "Send page to device is disabled"
+ );
+ checkPopup();
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_not_synced_yet() {
+ const sandbox = setupSendTabMocks({ fxaDevices: null });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ true,
+ "Send page to device is disabled"
+ );
+ checkPopup();
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_sync_not_ready_configured() {
+ const sandbox = setupSendTabMocks({ syncReady: false });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ true,
+ "Send page to device is disabled"
+ );
+ checkPopup();
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_sync_not_ready_other_state() {
+ const sandbox = setupSendTabMocks({
+ syncReady: false,
+ state: UIState.STATUS_NOT_VERIFIED,
+ });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send page to device is enabled"
+ );
+ checkPopup();
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_unconfigured() {
+ const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_CONFIGURED });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send page to device is enabled"
+ );
+ checkPopup();
+
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_not_verified() {
+ const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_VERIFIED });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send page to device is enabled"
+ );
+ checkPopup();
+
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_login_failed() {
+ const sandbox = setupSendTabMocks({ state: UIState.STATUS_LOGIN_FAILED });
+
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send page to device is enabled"
+ );
+ checkPopup();
+
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_fxa_disabled() {
+ const getter = sinon.stub(gSync, "FXA_ENABLED").get(() => false);
+ gSync.onFxaDisabled(); // Would have been called on gSync initialization if FXA_ENABLED had been set.
+ await openContentContextMenu("#moztext");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ true,
+ "Send page to device is hidden"
+ );
+ await hideContentContextMenu();
+ getter.restore();
+ [...document.querySelectorAll(".sync-ui-item")].forEach(
+ e => (e.hidden = false)
+ );
+});
+
+// We are not going to bother testing the visibility of context-sendlinktodevice
+// since it uses the exact same code.
+// However, browser_contextmenu.js contains tests that verify its presence.
+
+add_task(async function teardown() {
+ Weave.Service.clientsEngine.getClientByFxaDeviceId.restore();
+ Weave.Service.clientsEngine.getClientType.restore();
+ gBrowser.removeCurrentTab();
+});
+
+function checkPopup(expectedItems = null) {
+ const popup = document.getElementById("context-sendpagetodevice-popup");
+ if (!expectedItems) {
+ is(popup.state, "closed", "Popup should be hidden.");
+ return;
+ }
+ const menuItems = popup.children;
+ for (let i = 0; i < menuItems.length; i++) {
+ const menuItem = menuItems[i];
+ const expectedItem = expectedItems[i];
+ if (expectedItem === "----") {
+ is(menuItem.nodeName, "menuseparator", "Found a separator");
+ continue;
+ }
+ is(menuItem.nodeName, "menuitem", "Found a menu item");
+ // Bug workaround, menuItem.label "…" encoding is different than ours.
+ is(
+ menuItem.label.normalize("NFKC"),
+ expectedItem.label,
+ "Correct menu item label"
+ );
+ is(
+ menuItem.disabled,
+ !!expectedItem.disabled,
+ "Correct menu item disabled state"
+ );
+ }
+ // check the length last - the above loop might have given us other clues...
+ is(
+ menuItems.length,
+ expectedItems.length,
+ "Popup has the expected children count."
+ );
+}
+
+async function openContentContextMenu(selector, openSubmenuId = null) {
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+ is(contextMenu.state, "closed", "checking if popup is closed");
+
+ const awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ selector,
+ 0,
+ 0,
+ {
+ type: "contextmenu",
+ button: 2,
+ shiftkey: false,
+ centered: true,
+ },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+
+ if (openSubmenuId) {
+ const menu = document.getElementById(openSubmenuId);
+ const menuPopup = menu.menupopup;
+ const menuPopupPromise = BrowserTestUtils.waitForEvent(
+ menuPopup,
+ "popupshown"
+ );
+ menu.openMenu(true);
+ await menuPopupPromise;
+ }
+ return contextMenu;
+}
+
+async function hideContentContextMenu() {
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+ const awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+}
diff --git a/browser/base/content/test/sync/browser_contextmenu_sendtab.js b/browser/base/content/test/sync/browser_contextmenu_sendtab.js
new file mode 100644
index 0000000000..4922869c1d
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js
@@ -0,0 +1,362 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kForceOverflowWidthPx = 450;
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/general/head.js",
+ this
+);
+
+const fxaDevices = [
+ {
+ id: 1,
+ name: "Foo",
+ availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "baz" },
+ lastAccessTime: Date.now(),
+ },
+ {
+ id: 2,
+ name: "Bar",
+ availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "boo" },
+ lastAccessTime: Date.now() + 60000, // add 30min
+ },
+ {
+ id: 3,
+ name: "Baz",
+ clientRecord: "bar",
+ lastAccessTime: Date.now() + 120000, // add 60min
+ }, // Legacy send tab target (no availableCommands).
+ { id: 4, name: "Homer" }, // Incompatible target.
+];
+
+let [testTab] = gBrowser.visibleTabs;
+
+function updateTabContextMenu(tab = gBrowser.selectedTab) {
+ let menu = document.getElementById("tabContextMenu");
+ var evt = new Event("");
+ tab.dispatchEvent(evt);
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test
+ gBrowser.selectedTab.focus();
+ menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
+ is(
+ window.TabContextMenu.contextTab,
+ tab,
+ "TabContextMenu context is the expected tab"
+ );
+ menu.hidePopup();
+}
+
+add_setup(async function () {
+ await promiseSyncReady();
+ await Services.search.init();
+ // gSync.init() is called in a requestIdleCallback. Force its initialization.
+ gSync.init();
+ sinon
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = fxaDevices.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sinon.stub(Weave.Service.clientsEngine, "getClientType").returns("desktop");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+ registerCleanupFunction(() => {
+ gBrowser.removeCurrentTab();
+ });
+ is(gBrowser.visibleTabs.length, 2, "there are two visible tabs");
+});
+
+add_task(async function test_sendTabToDevice_callsFlushLogFile() {
+ const sandbox = setupSendTabMocks({ fxaDevices });
+ updateTabContextMenu(testTab);
+ await openTabContextMenu("context_sendTabToDevice");
+ let promiseObserved = promiseObserver("service:log-manager:flush-log-file");
+
+ await activateMenuItem();
+ await promiseObserved;
+ ok(true, "Got flush-log-file observer message");
+
+ await closeConfirmationHint();
+ sandbox.restore();
+});
+
+async function checkForConfirmationHint(targetId) {
+ const sandbox = setupSendTabMocks({ fxaDevices });
+ updateTabContextMenu(testTab);
+
+ await openTabContextMenu("context_sendTabToDevice");
+ await activateMenuItem();
+ is(
+ ConfirmationHint._panel.anchorNode.id,
+ targetId,
+ `Hint anchored to ${targetId}`
+ );
+ await closeConfirmationHint();
+ sandbox.restore();
+}
+
+add_task(async function test_sendTabToDevice_showsConfirmationHint_fxa() {
+ // We need to change the fxastatus from "not_configured" to show the FxA button.
+ is(
+ document.documentElement.getAttribute("fxastatus"),
+ "not_configured",
+ "FxA button is hidden"
+ );
+ document.documentElement.setAttribute("fxastatus", "foo");
+ await checkForConfirmationHint("fxa-toolbar-menu-button");
+ document.documentElement.setAttribute("fxastatus", "not_configured");
+});
+
+add_task(
+ async function test_sendTabToDevice_showsConfirmationHint_onOverflowMenu() {
+ // We need to change the fxastatus from "not_configured" to show the FxA button.
+ is(
+ document.documentElement.getAttribute("fxastatus"),
+ "not_configured",
+ "FxA button is hidden"
+ );
+ document.documentElement.setAttribute("fxastatus", "foo");
+
+ let navbar = document.getElementById("nav-bar");
+
+ // Resize the window so that the account button is in the overflow menu.
+ let originalWidth = window.outerWidth;
+ window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
+ await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing"));
+
+ await checkForConfirmationHint("PanelUI-menu-button");
+ document.documentElement.setAttribute("fxastatus", "not_configured");
+
+ window.resizeTo(originalWidth, window.outerHeight);
+ await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing"));
+ CustomizableUI.reset();
+ }
+);
+
+add_task(async function test_sendTabToDevice_showsConfirmationHint_appMenu() {
+ // If fxastatus is "not_configured" then the FxA button is hidden, and we
+ // should use the appMenu.
+ is(
+ document.documentElement.getAttribute("fxastatus"),
+ "not_configured",
+ "FxA button is hidden"
+ );
+ await checkForConfirmationHint("PanelUI-menu-button");
+});
+
+add_task(async function test_tab_contextmenu() {
+ const sandbox = setupSendTabMocks({ fxaDevices });
+ let expectation = sandbox
+ .mock(gSync)
+ .expects("sendTabToDevice")
+ .once()
+ .withExactArgs(
+ "about:mozilla",
+ [fxaDevices[1]],
+ "The Book of Mozilla, 6:27"
+ )
+ .returns(true);
+
+ updateTabContextMenu(testTab);
+ await openTabContextMenu("context_sendTabToDevice");
+ is(
+ document.getElementById("context_sendTabToDevice").hidden,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context_sendTabToDevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+
+ await activateMenuItem();
+ await closeConfirmationHint();
+
+ expectation.verify();
+ sandbox.restore();
+});
+
+add_task(async function test_tab_contextmenu_unconfigured() {
+ const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_CONFIGURED });
+
+ updateTabContextMenu(testTab);
+ is(
+ document.getElementById("context_sendTabToDevice").hidden,
+ true,
+ "Send tab to device is hidden"
+ );
+ is(
+ document.getElementById("context_sendTabToDevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_tab_contextmenu_not_sendable() {
+ const sandbox = setupSendTabMocks({ fxaDevices, isSendableURI: false });
+
+ updateTabContextMenu(testTab);
+ is(
+ document.getElementById("context_sendTabToDevice").hidden,
+ true,
+ "Send tab to device is hidden"
+ );
+ is(
+ document.getElementById("context_sendTabToDevice").disabled,
+ true,
+ "Send tab to device is disabled"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_tab_contextmenu_not_synced_yet() {
+ const sandbox = setupSendTabMocks({ fxaDevices: null });
+
+ updateTabContextMenu(testTab);
+ is(
+ document.getElementById("context_sendTabToDevice").hidden,
+ true,
+ "Send tab to device is hidden"
+ );
+ is(
+ document.getElementById("context_sendTabToDevice").disabled,
+ true,
+ "Send tab to device is disabled"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_tab_contextmenu_sync_not_ready_configured() {
+ const sandbox = setupSendTabMocks({ syncReady: false });
+
+ updateTabContextMenu(testTab);
+ is(
+ document.getElementById("context_sendTabToDevice").hidden,
+ true,
+ "Send tab to device is hidden"
+ );
+ is(
+ document.getElementById("context_sendTabToDevice").disabled,
+ true,
+ "Send tab to device is disabled"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_tab_contextmenu_sync_not_ready_other_state() {
+ const sandbox = setupSendTabMocks({
+ syncReady: false,
+ state: UIState.STATUS_NOT_VERIFIED,
+ });
+
+ updateTabContextMenu(testTab);
+ is(
+ document.getElementById("context_sendTabToDevice").hidden,
+ true,
+ "Send tab to device is hidden"
+ );
+ is(
+ document.getElementById("context_sendTabToDevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_tab_contextmenu_fxa_disabled() {
+ const getter = sinon.stub(gSync, "FXA_ENABLED").get(() => false);
+ // Simulate onFxaDisabled() being called on window open.
+ gSync.onFxaDisabled();
+
+ updateTabContextMenu(testTab);
+ is(
+ document.getElementById("context_sendTabToDevice").hidden,
+ true,
+ "Send tab to device is hidden"
+ );
+
+ getter.restore();
+ [...document.querySelectorAll(".sync-ui-item")].forEach(
+ e => (e.hidden = false)
+ );
+});
+
+add_task(async function teardown() {
+ Weave.Service.clientsEngine.getClientByFxaDeviceId.restore();
+ Weave.Service.clientsEngine.getClientType.restore();
+});
+
+async function openTabContextMenu(openSubmenuId = null) {
+ const contextMenu = document.getElementById("tabContextMenu");
+ is(contextMenu.state, "closed", "checking if popup is closed");
+
+ const awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await awaitPopupShown;
+
+ if (openSubmenuId) {
+ const menuPopup = document.getElementById(openSubmenuId).menupopup;
+ const menuPopupPromise = BrowserTestUtils.waitForEvent(
+ menuPopup,
+ "popupshown"
+ );
+ menuPopup.openPopup();
+ await menuPopupPromise;
+ }
+}
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ };
+ Services.obs.addObserver(obs, topic);
+ });
+}
+
+function waitForConfirmationHint() {
+ return BrowserTestUtils.waitForEvent(ConfirmationHint._panel, "popuphidden");
+}
+
+async function activateMenuItem() {
+ let popupHidden = BrowserTestUtils.waitForEvent(
+ document.getElementById("tabContextMenu"),
+ "popuphidden"
+ );
+ let hintShown = BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popupshown"
+ );
+ let menuitem = document
+ .getElementById("context_sendTabToDevicePopupMenu")
+ .querySelector("menuitem");
+ menuitem.closest("menupopup").activateItem(menuitem);
+ await popupHidden;
+ await hintShown;
+}
+
+async function closeConfirmationHint() {
+ let hintHidden = BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popuphidden"
+ );
+ ConfirmationHint._panel.hidePopup();
+ await hintHidden;
+}
diff --git a/browser/base/content/test/sync/browser_fxa_badge.js b/browser/base/content/test/sync/browser_fxa_badge.js
new file mode 100644
index 0000000000..227d778d6c
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_badge.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppMenuNotifications } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppMenuNotifications.sys.mjs"
+);
+
+add_task(async function test_unconfigured_no_badge() {
+ const oldUIState = UIState.get;
+
+ UIState.get = () => ({
+ status: UIState.STATUS_NOT_CONFIGURED,
+ });
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ checkFxABadge(false);
+
+ UIState.get = oldUIState;
+});
+
+add_task(async function test_signedin_no_badge() {
+ const oldUIState = UIState.get;
+
+ UIState.get = () => ({
+ status: UIState.STATUS_SIGNED_IN,
+ lastSync: new Date(),
+ email: "foo@bar.com",
+ });
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ checkFxABadge(false);
+
+ UIState.get = oldUIState;
+});
+
+add_task(async function test_unverified_badge_shown() {
+ const oldUIState = UIState.get;
+
+ UIState.get = () => ({
+ status: UIState.STATUS_NOT_VERIFIED,
+ email: "foo@bar.com",
+ });
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ checkFxABadge(true);
+
+ UIState.get = oldUIState;
+});
+
+add_task(async function test_loginFailed_badge_shown() {
+ const oldUIState = UIState.get;
+
+ UIState.get = () => ({
+ status: UIState.STATUS_LOGIN_FAILED,
+ email: "foo@bar.com",
+ });
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ checkFxABadge(true);
+
+ UIState.get = oldUIState;
+});
+
+function checkFxABadge(shouldBeShown) {
+ let fxaButton = document.getElementById("fxa-toolbar-menu-button");
+ let isShown =
+ fxaButton.hasAttribute("badge-status") ||
+ fxaButton
+ .querySelector(".toolbarbutton-badge")
+ .classList.contains("feature-callout");
+ is(isShown, shouldBeShown, "Fxa badge shown matches expected value.");
+}
diff --git a/browser/base/content/test/sync/browser_fxa_web_channel.html b/browser/base/content/test/sync/browser_fxa_web_channel.html
new file mode 100644
index 0000000000..927b3523e9
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>fxa_web_channel_test</title>
+</head>
+<body>
+<script>
+ var webChannelId = "account_updates_test";
+
+ window.onload = function() {
+ var testName = window.location.search.replace(/^\?/, "");
+
+ switch (testName) {
+ case "profile_change":
+ test_profile_change();
+ break;
+ case "login":
+ test_login();
+ break;
+ case "can_link_account":
+ test_can_link_account();
+ break;
+ case "logout":
+ test_logout();
+ break;
+ case "delete":
+ test_delete();
+ break;
+ case "firefox_view":
+ test_firefox_view();
+ break;
+ }
+ };
+
+ function test_profile_change() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "profile:change",
+ data: {
+ uid: "abc123",
+ },
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_login() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:login",
+ data: {
+ authAt: Date.now(),
+ email: "testuser@testuser.com",
+ keyFetchToken: "key_fetch_token",
+ sessionToken: "session_token",
+ uid: "uid",
+ unwrapBKey: "unwrap_b_key",
+ verified: true,
+ },
+ messageId: 1,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_can_link_account() {
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ // echo any responses from the browser back to the tests on the
+ // fxaccounts_webchannel_response_echo WebChannel. The tests are
+ // listening for events and do the appropriate checks.
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "fxaccounts_webchannel_response_echo",
+ message: e.detail.message,
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }, true);
+
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:can_link_account",
+ data: {
+ email: "testuser@testuser.com",
+ },
+ messageId: 2,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_logout() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:logout",
+ data: {
+ uid: "uid",
+ },
+ messageId: 3,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_delete() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:delete",
+ data: {
+ uid: "uid",
+ },
+ messageId: 4,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_firefox_view() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:firefox_view",
+ data: {
+ uid: "uid",
+ },
+ messageId: 5,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/sync/browser_fxa_web_channel.js b/browser/base/content/test/sync/browser_fxa_web_channel.js
new file mode 100644
index 0000000000..903dd317ac
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
+ return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
+});
+
+ChromeUtils.defineESModuleGetters(this, {
+ WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
+});
+
+var { FxAccountsWebChannel } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsWebChannel.sys.mjs"
+);
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_HTTP_PATH = "http://example.com";
+const TEST_BASE_URL =
+ TEST_HTTP_PATH +
+ "/browser/browser/base/content/test/sync/browser_fxa_web_channel.html";
+const TEST_CHANNEL_ID = "account_updates_test";
+
+var gTests = [
+ {
+ desc: "FxA Web Channel - should receive message about profile changes",
+ async run() {
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ });
+ let promiseObserver = new Promise((resolve, reject) => {
+ makeObserver(
+ FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
+ function (subject, topic, data) {
+ Assert.equal(data, "abc123");
+ client.tearDown();
+ resolve();
+ }
+ );
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?profile_change",
+ },
+ async function () {
+ await promiseObserver;
+ }
+ );
+ },
+ },
+ {
+ desc: "fxa web channel - login messages should notify the fxAccounts object",
+ async run() {
+ let promiseLogin = new Promise((resolve, reject) => {
+ let login = accountData => {
+ Assert.equal(typeof accountData.authAt, "number");
+ Assert.equal(accountData.email, "testuser@testuser.com");
+ Assert.equal(accountData.keyFetchToken, "key_fetch_token");
+ Assert.equal(accountData.sessionToken, "session_token");
+ Assert.equal(accountData.uid, "uid");
+ Assert.equal(accountData.unwrapBKey, "unwrap_b_key");
+ Assert.equal(accountData.verified, true);
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ login,
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?login",
+ },
+ async function () {
+ await promiseLogin;
+ }
+ );
+ },
+ },
+ {
+ desc: "fxa web channel - can_link_account messages should respond",
+ async run() {
+ let properUrl = TEST_BASE_URL + "?can_link_account";
+
+ let promiseEcho = new Promise((resolve, reject) => {
+ let webChannelOrigin = Services.io.newURI(properUrl);
+ // responses sent to content are echoed back over the
+ // `fxaccounts_webchannel_response_echo` channel. Ensure the
+ // fxaccounts:can_link_account message is responded to.
+ let echoWebChannel = new WebChannel(
+ "fxaccounts_webchannel_response_echo",
+ webChannelOrigin
+ );
+ echoWebChannel.listen((webChannelId, message, target) => {
+ Assert.equal(message.command, "fxaccounts:can_link_account");
+ Assert.equal(message.messageId, 2);
+ Assert.equal(message.data.ok, true);
+
+ client.tearDown();
+ echoWebChannel.stopListening();
+
+ resolve();
+ });
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ shouldAllowRelink(acctName) {
+ return acctName === "testuser@testuser.com";
+ },
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: properUrl,
+ },
+ async function () {
+ await promiseEcho;
+ }
+ );
+ },
+ },
+ {
+ desc: "fxa web channel - logout messages should notify the fxAccounts object",
+ async run() {
+ let promiseLogout = new Promise((resolve, reject) => {
+ let logout = uid => {
+ Assert.equal(uid, "uid");
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout,
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?logout",
+ },
+ async function () {
+ await promiseLogout;
+ }
+ );
+ },
+ },
+ {
+ desc: "fxa web channel - delete messages should notify the fxAccounts object",
+ async run() {
+ let promiseDelete = new Promise((resolve, reject) => {
+ let logout = uid => {
+ Assert.equal(uid, "uid");
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout,
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?delete",
+ },
+ async function () {
+ await promiseDelete;
+ }
+ );
+ },
+ },
+ {
+ desc: "fxa web channel - firefox_view messages should call the openFirefoxView helper",
+ async run() {
+ let wasCalled = false;
+ let promiseMessageHandled = new Promise((resolve, reject) => {
+ let openFirefoxView = (browser, entryPoint) => {
+ wasCalled = true;
+ Assert.ok(
+ !!browser.ownerGlobal,
+ "openFirefoxView called with a browser argument"
+ );
+ Assert.equal(
+ typeof browser.ownerGlobal.FirefoxViewHandler.openTab,
+ "function",
+ "We can reach the openTab method"
+ );
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ openFirefoxView,
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?firefox_view",
+ },
+ async function () {
+ await promiseMessageHandled;
+ }
+ );
+ Assert.ok(wasCalled, "openFirefoxView did get called");
+ },
+ },
+]; // gTests
+
+function makeObserver(aObserveTopic, aObserveFunc) {
+ let callback = function (aSubject, aTopic, aData) {
+ if (aTopic == aObserveTopic) {
+ removeMe();
+ aObserveFunc(aSubject, aTopic, aData);
+ }
+ };
+
+ function removeMe() {
+ Services.obs.removeObserver(callback, aObserveTopic);
+ }
+
+ Services.obs.addObserver(callback, aObserveTopic);
+ return removeMe;
+}
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(
+ "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess"
+ );
+});
+
+function test() {
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref(
+ "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
+ false
+ );
+
+ (async function () {
+ for (let testCase of gTests) {
+ info("Running: " + testCase.desc);
+ await testCase.run();
+ }
+ })().then(finish, ex => {
+ Assert.ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
diff --git a/browser/base/content/test/sync/browser_sync.js b/browser/base/content/test/sync/browser_sync.js
new file mode 100644
index 0000000000..a8354b7f10
--- /dev/null
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -0,0 +1,751 @@
+/* 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_setup(async function () {
+ // gSync.init() is called in a requestIdleCallback. Force its initialization.
+ gSync.init();
+ // This preference gets set the very first time that the FxA menu gets opened,
+ // which can cause a state write to occur, which can confuse this test, since
+ // when in the signed-out state, we need to set the state _before_ opening
+ // the FxA menu (since the panel cannot be opened) in the signed out state.
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.toolbar.accessed", true]],
+ });
+});
+
+add_task(async function test_ui_state_notification_calls_updateAllUI() {
+ let called = false;
+ let updateAllUI = gSync.updateAllUI;
+ gSync.updateAllUI = () => {
+ called = true;
+ };
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ ok(called);
+
+ gSync.updateAllUI = updateAllUI;
+});
+
+add_task(async function test_navBar_button_visibility() {
+ const button = document.getElementById("fxa-toolbar-menu-button");
+ ok(button.closest("#nav-bar"), "button is in the #nav-bar");
+
+ const state = {
+ status: UIState.STATUS_NOT_CONFIGURED,
+ syncEnabled: true,
+ };
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.is_hidden(button),
+ "Button should be hidden with STATUS_NOT_CONFIGURED"
+ );
+
+ state.email = "foo@bar.com";
+ state.status = UIState.STATUS_NOT_VERIFIED;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Check button visibility with STATUS_NOT_VERIFIED"
+ );
+
+ state.status = UIState.STATUS_LOGIN_FAILED;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Check button visibility with STATUS_LOGIN_FAILED"
+ );
+
+ state.status = UIState.STATUS_SIGNED_IN;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Check button visibility with STATUS_SIGNED_IN"
+ );
+
+ state.syncEnabled = false;
+ gSync.updateAllUI(state);
+ is(
+ BrowserTestUtils.is_visible(button),
+ true,
+ "Check button visibility when signed in, but sync disabled"
+ );
+});
+
+add_task(async function test_overflow_navBar_button_visibility() {
+ const button = document.getElementById("fxa-toolbar-menu-button");
+
+ let overflowPanel = document.getElementById("widget-overflow");
+ overflowPanel.setAttribute("animate", "false");
+ let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+ let originalWindowWidth = window.outerWidth;
+
+ registerCleanupFunction(function () {
+ overflowPanel.removeAttribute("animate");
+ window.resizeTo(originalWindowWidth, window.outerHeight);
+ return TestUtils.waitForCondition(
+ () => !navbar.hasAttribute("overflowing")
+ );
+ });
+
+ window.resizeTo(450, window.outerHeight);
+
+ await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing"));
+ ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
+
+ let chevron = document.getElementById("nav-bar-overflow-button");
+ let shownPanelPromise = BrowserTestUtils.waitForEvent(
+ overflowPanel,
+ "popupshown"
+ );
+ chevron.click();
+ await shownPanelPromise;
+
+ ok(button, "fxa-toolbar-menu-button was found");
+
+ const state = {
+ status: UIState.STATUS_NOT_CONFIGURED,
+ syncEnabled: true,
+ };
+ gSync.updateAllUI(state);
+
+ ok(
+ BrowserTestUtils.is_hidden(button),
+ "Button should be hidden with STATUS_NOT_CONFIGURED"
+ );
+
+ let hidePanelPromise = BrowserTestUtils.waitForEvent(
+ overflowPanel,
+ "popuphidden"
+ );
+ chevron.click();
+ await hidePanelPromise;
+});
+
+add_task(async function setupForPanelTests() {
+ /* Proton hides the FxA toolbar button when in the nav-bar and unconfigured.
+ To test the panel in all states, we move it to the tabstrip toolbar where
+ it will always be visible.
+ */
+ CustomizableUI.addWidgetToArea(
+ "fxa-toolbar-menu-button",
+ CustomizableUI.AREA_TABSTRIP
+ );
+
+ // make sure it gets put back at the end of the tests
+ registerCleanupFunction(() => {
+ CustomizableUI.addWidgetToArea(
+ "fxa-toolbar-menu-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ });
+});
+
+add_task(async function test_ui_state_signedin() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ const relativeDateAnchor = new Date();
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ lastSync: new Date(),
+ syncing: false,
+ };
+
+ const origRelativeTimeFormat = gSync.relativeTimeFormat;
+ gSync.relativeTimeFormat = {
+ formatBestUnit(date) {
+ return origRelativeTimeFormat.formatBestUnit(date, {
+ now: relativeDateAnchor,
+ });
+ },
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ checkMenuBarItem("sync-syncnowitem");
+ checkPanelHeader();
+ checkFxaToolbarButtonPanel({
+ headerTitle: "Manage account",
+ headerDescription: "foo@bar.com",
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-connect-device-button",
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ "PanelUI-fxa-menu-account-signout-button",
+ ],
+ disabledItems: [],
+ hiddenItems: ["PanelUI-fxa-menu-setup-sync-button"],
+ });
+ checkFxAAvatar("signedin");
+ gSync.relativeTimeFormat = origRelativeTimeFormat;
+ await closeFxaPanel();
+
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ description: "foo@bar.com",
+ titleHidden: true,
+ hideFxAText: true,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_syncing_panel_closed() {
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ lastSync: new Date(),
+ syncing: true,
+ };
+
+ gSync.updateAllUI(state);
+
+ checkSyncNowButtons(true);
+
+ // Be good citizens and remove the "syncing" state.
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ lastSync: new Date(),
+ syncing: false,
+ });
+ // Because we switch from syncing to non-syncing, and there's a timeout involved.
+ await promiseObserver("test:browser-sync:activity-stop");
+});
+
+add_task(async function test_ui_state_syncing_panel_open() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ lastSync: new Date(),
+ syncing: false,
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ checkSyncNowButtons(false);
+
+ state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ lastSync: new Date(),
+ syncing: true,
+ };
+
+ gSync.updateAllUI(state);
+
+ checkSyncNowButtons(true);
+
+ // Be good citizens and remove the "syncing" state.
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ lastSync: new Date(),
+ syncing: false,
+ });
+ // Because we switch from syncing to non-syncing, and there's a timeout involved.
+ await promiseObserver("test:browser-sync:activity-stop");
+
+ await closeFxaPanel();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_ui_state_panel_open_after_syncing() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ lastSync: new Date(),
+ syncing: true,
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ checkSyncNowButtons(true);
+
+ // Be good citizens and remove the "syncing" state.
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ lastSync: new Date(),
+ syncing: false,
+ });
+ // Because we switch from syncing to non-syncing, and there's a timeout involved.
+ await promiseObserver("test:browser-sync:activity-stop");
+
+ await closeFxaPanel();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_ui_state_unconfigured() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let state = {
+ status: UIState.STATUS_NOT_CONFIGURED,
+ };
+
+ gSync.updateAllUI(state);
+
+ checkMenuBarItem("sync-setup");
+
+ checkFxAAvatar("not_configured");
+
+ let signedOffLabel = gSync.fluentStrings.formatValueSync(
+ "appmenu-fxa-signed-in-label"
+ );
+
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ description: signedOffLabel,
+ titleHidden: true,
+ hideFxAText: false,
+ });
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_syncdisabled() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: false,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ checkMenuBarItem("sync-enable");
+ checkPanelHeader();
+ checkFxaToolbarButtonPanel({
+ headerTitle: "Manage account",
+ headerDescription: "foo@bar.com",
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-connect-device-button",
+ "PanelUI-fxa-menu-setup-sync-button",
+ "PanelUI-fxa-menu-account-signout-button",
+ ],
+ disabledItems: [],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+ checkFxAAvatar("signedin");
+ await closeFxaPanel();
+
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ description: "foo@bar.com",
+ titleHidden: true,
+ hideFxAText: true,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_unverified() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let state = {
+ status: UIState.STATUS_NOT_VERIFIED,
+ email: "foo@bar.com",
+ syncing: false,
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ const expectedLabel = gSync.fluentStrings.formatValueSync(
+ "account-finish-account-setup"
+ );
+
+ checkMenuBarItem("sync-unverifieditem");
+ checkPanelHeader();
+ checkFxaToolbarButtonPanel({
+ headerTitle: expectedLabel,
+ headerDescription: state.email,
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-setup-sync-button",
+ "PanelUI-fxa-menu-account-signout-button",
+ ],
+ disabledItems: ["PanelUI-fxa-menu-connect-device-button"],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+ checkFxAAvatar("unverified");
+ await closeFxaPanel();
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ description: state.email,
+ title: expectedLabel,
+ titleHidden: false,
+ hideFxAText: true,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_loginFailed() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let state = {
+ status: UIState.STATUS_LOGIN_FAILED,
+ email: "foo@bar.com",
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ const expectedLabel = gSync.fluentStrings.formatValueSync(
+ "account-disconnected2"
+ );
+
+ checkMenuBarItem("sync-reauthitem");
+ checkPanelHeader();
+ checkFxaToolbarButtonPanel({
+ headerTitle: expectedLabel,
+ headerDescription: state.email,
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-setup-sync-button",
+ "PanelUI-fxa-menu-account-signout-button",
+ ],
+ disabledItems: ["PanelUI-fxa-menu-connect-device-button"],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+ checkFxAAvatar("login-failed");
+ await closeFxaPanel();
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ description: state.email,
+ title: expectedLabel,
+ titleHidden: false,
+ hideFxAText: true,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_app_menu_fxa_disabled() {
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ Services.prefs.setBoolPref("identity.fxaccounts.enabled", true);
+ newWin.gSync.onFxaDisabled();
+
+ let menuButton = newWin.document.getElementById("PanelUI-menu-button");
+ menuButton.click();
+ await BrowserTestUtils.waitForEvent(newWin.PanelUI.mainView, "ViewShown");
+
+ [...newWin.document.querySelectorAll(".sync-ui-item")].forEach(
+ e => (e.hidden = false)
+ );
+
+ let hidden = BrowserTestUtils.waitForEvent(
+ newWin.document,
+ "popuphidden",
+ true
+ );
+ newWin.PanelUI.hide();
+ await hidden;
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+add_task(
+ // Can't open the history menu in tests on Mac.
+ () => AppConstants.platform != "mac",
+ async function test_history_menu_fxa_disabled() {
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ Services.prefs.setBoolPref("identity.fxaccounts.enabled", true);
+ newWin.gSync.onFxaDisabled();
+
+ const historyMenubarItem = window.document.getElementById("history-menu");
+ const historyMenu = window.document.getElementById("historyMenuPopup");
+ const syncedTabsItem = historyMenu.querySelector("#sync-tabs-menuitem");
+ const menuShown = BrowserTestUtils.waitForEvent(historyMenu, "popupshown");
+ historyMenubarItem.openMenu(true);
+ await menuShown;
+
+ Assert.equal(
+ syncedTabsItem.hidden,
+ true,
+ "Synced Tabs item should not be displayed when FxAccounts is disabled"
+ );
+ const menuHidden = BrowserTestUtils.waitForEvent(
+ historyMenu,
+ "popuphidden"
+ );
+ historyMenu.hidePopup();
+ await menuHidden;
+ await BrowserTestUtils.closeWindow(newWin);
+ }
+);
+
+function checkPanelUIStatusBar({
+ description,
+ title,
+ titleHidden,
+ hideFxAText,
+}) {
+ checkAppMenuFxAText(hideFxAText);
+ let appMenuHeaderTitle = PanelMultiView.getViewNode(
+ document,
+ "appMenu-header-title"
+ );
+ let appMenuHeaderDescription = PanelMultiView.getViewNode(
+ document,
+ "appMenu-header-description"
+ );
+ is(
+ appMenuHeaderDescription.value,
+ description,
+ "app menu description has correct value"
+ );
+ is(appMenuHeaderTitle.hidden, titleHidden, "title has correct hidden status");
+ if (!titleHidden) {
+ is(appMenuHeaderTitle.value, title, "title has correct value");
+ }
+}
+
+function checkMenuBarItem(expectedShownItemId) {
+ checkItemsVisibilities(
+ [
+ "sync-setup",
+ "sync-enable",
+ "sync-syncnowitem",
+ "sync-reauthitem",
+ "sync-unverifieditem",
+ ],
+ expectedShownItemId
+ );
+}
+
+function checkPanelHeader() {
+ let fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
+ is(
+ fxaPanelView.getAttribute("title"),
+ gSync.fluentStrings.formatValueSync("appmenu-fxa-header2"),
+ "Panel title is correct"
+ );
+}
+
+function checkSyncNowButtons(syncing, tooltip = null) {
+ const syncButtons = document.querySelectorAll(".syncNowBtn");
+
+ for (const syncButton of syncButtons) {
+ is(
+ syncButton.getAttribute("syncstatus"),
+ syncing ? "active" : "",
+ "button active has the right value"
+ );
+ if (tooltip) {
+ is(
+ syncButton.getAttribute("tooltiptext"),
+ tooltip,
+ "button tooltiptext is set to the right value"
+ );
+ }
+ }
+
+ const syncLabels = document.querySelectorAll(".syncnow-label");
+
+ for (const syncLabel of syncLabels) {
+ if (syncing) {
+ is(
+ syncLabel.getAttribute("data-l10n-id"),
+ syncLabel.getAttribute("syncing-data-l10n-id"),
+ "label is set to the right value"
+ );
+ } else {
+ is(
+ syncLabel.getAttribute("data-l10n-id"),
+ syncLabel.getAttribute("sync-now-data-l10n-id"),
+ "label is set to the right value"
+ );
+ }
+ }
+}
+
+async function checkFxaToolbarButtonPanel({
+ headerTitle,
+ headerDescription,
+ enabledItems,
+ disabledItems,
+ hiddenItems,
+}) {
+ is(
+ document.getElementById("fxa-menu-header-title").value,
+ headerTitle,
+ "has correct title"
+ );
+ is(
+ document.getElementById("fxa-menu-header-description").value,
+ headerDescription,
+ "has correct description"
+ );
+
+ for (const id of enabledItems) {
+ const el = document.getElementById(id);
+ is(el.hasAttribute("disabled"), false, id + " is enabled");
+ }
+
+ for (const id of disabledItems) {
+ const el = document.getElementById(id);
+ is(el.getAttribute("disabled"), "true", id + " is disabled");
+ }
+
+ for (const id of hiddenItems) {
+ const el = document.getElementById(id);
+ is(el.getAttribute("hidden"), "true", id + " is hidden");
+ }
+}
+
+async function checkFxABadged() {
+ const button = document.getElementById("fxa-toolbar-menu-button");
+ await BrowserTestUtils.waitForCondition(() => {
+ return button.querySelector("label.feature-callout");
+ });
+ const badge = button.querySelector("label.feature-callout");
+ ok(badge, "expected feature-callout style badge");
+ ok(BrowserTestUtils.is_visible(badge), "expected the badge to be visible");
+}
+
+// fxaStatus is one of 'not_configured', 'unverified', 'login-failed', or 'signedin'.
+function checkFxAAvatar(fxaStatus) {
+ // Unhide the panel so computed styles can be read
+ document.querySelector("#appMenu-popup").hidden = false;
+
+ const avatarContainers = [document.getElementById("fxa-avatar-image")];
+ for (const avatar of avatarContainers) {
+ const avatarURL = getComputedStyle(avatar).listStyleImage;
+ const expected = {
+ not_configured: 'url("chrome://browser/skin/fxa/avatar-empty.svg")',
+ unverified: 'url("chrome://browser/skin/fxa/avatar.svg")',
+ signedin: 'url("chrome://browser/skin/fxa/avatar.svg")',
+ "login-failed": 'url("chrome://browser/skin/fxa/avatar.svg")',
+ };
+ ok(
+ avatarURL == expected[fxaStatus],
+ `expected avatar URL to be ${expected[fxaStatus]}, got ${avatarURL}`
+ );
+ }
+}
+
+function checkAppMenuFxAText(hideStatus) {
+ let fxaText = document.getElementById("appMenu-fxa-text");
+ let isHidden = fxaText.hidden || fxaText.style.visibility == "collapse";
+ ok(isHidden == hideStatus, "FxA text has correct hidden state");
+}
+
+// Only one item visible at a time.
+function checkItemsVisibilities(itemsIds, expectedShownItemId) {
+ for (let id of itemsIds) {
+ if (id == expectedShownItemId) {
+ ok(
+ !document.getElementById(id).hidden,
+ "menuitem " + id + " should be visible"
+ );
+ } else {
+ ok(
+ document.getElementById(id).hidden,
+ "menuitem " + id + " should be hidden"
+ );
+ }
+ }
+}
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ };
+ Services.obs.addObserver(obs, topic);
+ });
+}
+
+async function openTabAndFxaPanel() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+ await openFxaPanel();
+}
+
+async function openFxaPanel() {
+ let fxaButton = document.getElementById("fxa-toolbar-menu-button");
+ fxaButton.click();
+
+ let fxaView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
+ await BrowserTestUtils.waitForEvent(fxaView, "ViewShown");
+}
+
+async function closeFxaPanel() {
+ let fxaView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ fxaView.closest("panel").hidePopup();
+ await hidden;
+}
+
+async function openMainPanel() {
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ menuButton.click();
+ await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown");
+}
+
+async function closeTabAndMainPanel() {
+ await gCUITestUtils.hideMainMenu();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
diff --git a/browser/base/content/test/sync/browser_synced_tabs_view.js b/browser/base/content/test/sync/browser_synced_tabs_view.js
new file mode 100644
index 0000000000..eb1203825e
--- /dev/null
+++ b/browser/base/content/test/sync/browser_synced_tabs_view.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function promiseLayout() {
+ // Wait for layout to have happened.
+ return new Promise(resolve =>
+ requestAnimationFrame(() => requestAnimationFrame(resolve))
+ );
+}
+
+add_setup(async function () {
+ registerCleanupFunction(() => CustomizableUI.reset());
+});
+
+async function withOpenSyncPanel(cb) {
+ let promise = BrowserTestUtils.waitForEvent(
+ window,
+ "ViewShown",
+ true,
+ e => e.target.id == "PanelUI-remotetabs"
+ ).then(e => e.target.closest("panel"));
+
+ let panel;
+ try {
+ gSync.openSyncedTabsPanel();
+ panel = await promise;
+ is(panel.state, "open", "Panel should have opened.");
+ await cb(panel);
+ } finally {
+ panel?.hidePopup();
+ }
+}
+
+add_task(async function test_button_in_bookmarks_toolbar() {
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_BOOKMARKS);
+ CustomizableUI.setToolbarVisibility(CustomizableUI.AREA_BOOKMARKS, "never");
+ await promiseLayout();
+
+ await withOpenSyncPanel(async panel => {
+ is(
+ panel.anchorNode.closest("toolbarbutton"),
+ PanelUI.menuButton,
+ "Should have anchored on the menu button because the sync button isn't visible."
+ );
+ });
+});
+
+add_task(async function test_button_in_navbar() {
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_NAVBAR, 0);
+ await promiseLayout();
+ await withOpenSyncPanel(async panel => {
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "sync-button",
+ "Should have anchored on the sync button itself."
+ );
+ });
+});
+
+add_task(async function test_button_in_overflow() {
+ CustomizableUI.addWidgetToArea(
+ "sync-button",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
+ 0
+ );
+ await promiseLayout();
+ await withOpenSyncPanel(async panel => {
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "nav-bar-overflow-button",
+ "Should have anchored on the overflow button."
+ );
+ });
+});
diff --git a/browser/base/content/test/sync/head.js b/browser/base/content/test/sync/head.js
new file mode 100644
index 0000000000..10ffb2a2d2
--- /dev/null
+++ b/browser/base/content/test/sync/head.js
@@ -0,0 +1,34 @@
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+function promiseSyncReady() {
+ let service = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ return service.whenLoaded();
+}
+
+function setupSendTabMocks({
+ fxaDevices = null,
+ state = UIState.STATUS_SIGNED_IN,
+ isSendableURI = true,
+}) {
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => fxaDevices);
+ sandbox.stub(UIState, "get").returns({
+ status: state,
+ syncEnabled: true,
+ });
+ if (isSendableURI) {
+ sandbox.stub(BrowserUtils, "getShareableURL").returnsArg(0);
+ } else {
+ sandbox.stub(BrowserUtils, "getShareableURL").returns(null);
+ }
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox.stub(fxAccounts.commands.sendTab, "send").resolves({ failed: [] });
+ return sandbox;
+}
diff --git a/browser/base/content/test/tabMediaIndicator/almostSilentAudioTrack.webm b/browser/base/content/test/tabMediaIndicator/almostSilentAudioTrack.webm
new file mode 100644
index 0000000000..0b8f8f746f
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/almostSilentAudioTrack.webm
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/audio.ogg b/browser/base/content/test/tabMediaIndicator/audio.ogg
new file mode 100644
index 0000000000..bed764fbf1
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/audio.ogg
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/audioEndedDuringPlaying.webm b/browser/base/content/test/tabMediaIndicator/audioEndedDuringPlaying.webm
new file mode 100644
index 0000000000..4f82b5da76
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/audioEndedDuringPlaying.webm
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/browser.ini b/browser/base/content/test/tabMediaIndicator/browser.ini
new file mode 100644
index 0000000000..a2ebf058fc
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+subsuite = media-bc
+tags = audiochannel
+support-files =
+ almostSilentAudioTrack.webm
+ audio.ogg
+ audioEndedDuringPlaying.webm
+ file_almostSilentAudioTrack.html
+ file_autoplay_media.html
+ file_empty.html
+ file_mediaPlayback.html
+ file_mediaPlayback2.html
+ file_mediaPlaybackFrame.html
+ file_mediaPlaybackFrame2.html
+ file_silentAudioTrack.html
+ file_webAudio.html
+ gizmo.mp4
+ head.js
+ noaudio.webm
+ silentAudioTrack.webm
+
+[browser_destroy_iframe.js]
+https_first_disabled = true
+[browser_mediaPlayback.js]
+[browser_mediaPlayback_mute.js]
+[browser_mediaplayback_audibility_change.js]
+[browser_mute.js]
+[browser_mute2.js]
+[browser_mute_webAudio.js]
+[browser_sound_indicator_silent_video.js]
+[browser_webAudio_hideSoundPlayingIcon.js]
+[browser_webAudio_silentData.js]
+[browser_webaudio_audibility_change.js]
diff --git a/browser/base/content/test/tabMediaIndicator/browser_destroy_iframe.js b/browser/base/content/test/tabMediaIndicator/browser_destroy_iframe.js
new file mode 100644
index 0000000000..f977d1d664
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_destroy_iframe.js
@@ -0,0 +1,50 @@
+const EMPTY_PAGE_URL = GetTestWebBasedURL("file_empty.html");
+const AUTPLAY_PAGE_URL = GetTestWebBasedURL("file_autoplay_media.html");
+const CORS_AUTPLAY_PAGE_URL = GetTestWebBasedURL(
+ "file_autoplay_media.html",
+ true
+);
+
+/**
+ * When an iframe that has audible media gets destroyed, if there is no other
+ * audible playing media existing in the page, then the sound indicator should
+ * disappear.
+ */
+add_task(async function testDestroyAudibleIframe() {
+ const iframesURL = [AUTPLAY_PAGE_URL, CORS_AUTPLAY_PAGE_URL];
+ for (let iframeURL of iframesURL) {
+ info(`open a tab, create an iframe and load an autoplay media page inside`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ EMPTY_PAGE_URL
+ );
+ await createIframeAndLoadURL(tab, iframeURL);
+
+ info(`sound indicator should appear because of audible playing media`);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear after destroying iframe`);
+ await removeIframe(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+function createIframeAndLoadURL(tab, url) {
+ // eslint-disable-next-line no-shadow
+ return SpecialPowers.spawn(tab.linkedBrowser, [url], async url => {
+ const iframe = content.document.createElement("iframe");
+ content.document.body.appendChild(iframe);
+ iframe.src = url;
+ info(`load ${url} for iframe`);
+ await new Promise(r => (iframe.onload = r));
+ });
+}
+
+function removeIframe(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.document.getElementsByTagName("iframe")[0].remove();
+ });
+}
diff --git a/browser/base/content/test/tabMediaIndicator/browser_mediaPlayback.js b/browser/base/content/test/tabMediaIndicator/browser_mediaPlayback.js
new file mode 100644
index 0000000000..9a5f457403
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mediaPlayback.js
@@ -0,0 +1,42 @@
+const PAGE = GetTestWebBasedURL("file_mediaPlayback.html");
+const FRAME = GetTestWebBasedURL("file_mediaPlaybackFrame.html");
+
+function wait_for_event(browser, event) {
+ return BrowserTestUtils.waitForEvent(browser, event, false, e => {
+ is(
+ e.originalTarget,
+ browser,
+ "Event must be dispatched to correct browser."
+ );
+ ok(!e.cancelable, "The event should not be cancelable");
+ return true;
+ });
+}
+
+async function test_on_browser(url, browser) {
+ info(`run test for ${url}`);
+ const startPromise = wait_for_event(browser, "DOMAudioPlaybackStarted");
+ BrowserTestUtils.loadURIString(browser, url);
+ await startPromise;
+ await wait_for_event(browser, "DOMAudioPlaybackStopped");
+}
+
+add_task(async function test_page() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ test_on_browser.bind(undefined, PAGE)
+ );
+});
+
+add_task(async function test_frame() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ test_on_browser.bind(undefined, FRAME)
+ );
+});
diff --git a/browser/base/content/test/tabMediaIndicator/browser_mediaPlayback_mute.js b/browser/base/content/test/tabMediaIndicator/browser_mediaPlayback_mute.js
new file mode 100644
index 0000000000..05999a37cd
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mediaPlayback_mute.js
@@ -0,0 +1,118 @@
+const PAGE = GetTestWebBasedURL("file_mediaPlayback2.html");
+const FRAME = GetTestWebBasedURL("file_mediaPlaybackFrame2.html");
+
+function wait_for_event(browser, event) {
+ return BrowserTestUtils.waitForEvent(browser, event, false, e => {
+ is(
+ e.originalTarget,
+ browser,
+ "Event must be dispatched to correct browser."
+ );
+ return true;
+ });
+}
+
+function test_audio_in_browser() {
+ function get_audio_element() {
+ var doc = content.document;
+ var list = doc.getElementsByTagName("audio");
+ if (list.length == 1) {
+ return list[0];
+ }
+
+ // iframe?
+ list = doc.getElementsByTagName("iframe");
+
+ var iframe = list[0];
+ list = iframe.contentDocument.getElementsByTagName("audio");
+ return list[0];
+ }
+
+ var audio = get_audio_element();
+ return {
+ computedVolume: audio.computedVolume,
+ computedMuted: audio.computedMuted,
+ };
+}
+
+async function test_on_browser(url, browser) {
+ BrowserTestUtils.loadURIString(browser, url);
+ await wait_for_event(browser, "DOMAudioPlaybackStarted");
+
+ var result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
+ is(result.computedVolume, 1, "Audio volume is 1");
+ is(result.computedMuted, false, "Audio is not muted");
+
+ ok(!browser.audioMuted, "Audio should not be muted by default");
+ browser.mute();
+ ok(browser.audioMuted, "Audio should be muted now");
+
+ await wait_for_event(browser, "DOMAudioPlaybackStopped");
+
+ result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
+ is(result.computedVolume, 0, "Audio volume is 0 when muted");
+ is(result.computedMuted, true, "Audio is muted");
+}
+
+async function test_visibility(url, browser) {
+ BrowserTestUtils.loadURIString(browser, url);
+ await wait_for_event(browser, "DOMAudioPlaybackStarted");
+
+ var result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
+ is(result.computedVolume, 1, "Audio volume is 1");
+ is(result.computedMuted, false, "Audio is not muted");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ function () {}
+ );
+
+ ok(!browser.audioMuted, "Audio should not be muted by default");
+ browser.mute();
+ ok(browser.audioMuted, "Audio should be muted now");
+
+ await wait_for_event(browser, "DOMAudioPlaybackStopped");
+
+ result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
+ is(result.computedVolume, 0, "Audio volume is 0 when muted");
+ is(result.computedMuted, true, "Audio is muted");
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.useAudioChannelService.testing", true]],
+ });
+});
+
+add_task(async function test_page() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ test_on_browser.bind(undefined, PAGE)
+ );
+});
+
+add_task(async function test_frame() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ test_on_browser.bind(undefined, FRAME)
+ );
+});
+
+add_task(async function test_frame() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ test_visibility.bind(undefined, PAGE)
+ );
+});
diff --git a/browser/base/content/test/tabMediaIndicator/browser_mediaplayback_audibility_change.js b/browser/base/content/test/tabMediaIndicator/browser_mediaplayback_audibility_change.js
new file mode 100644
index 0000000000..87186ca838
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mediaplayback_audibility_change.js
@@ -0,0 +1,258 @@
+/**
+ * When media changes its audible state, the sound indicator should be
+ * updated as well, which should appear only when web audio is audible.
+ */
+add_task(async function testUpdateSoundIndicatorWhenMediaPlaybackChanges() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+ await initMediaPlaybackDocument(tab, "audio.ogg");
+
+ info(`sound indicator should appear when audible audio starts playing`);
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when audio stops playing`);
+ await pauseMedia(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testUpdateSoundIndicatorWhenMediaBecomeSilent() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+ await initMediaPlaybackDocument(tab, "audioEndedDuringPlaying.webm");
+
+ info(`sound indicator should appear when audible audio starts playing`);
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when audio becomes silent`);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSoundIndicatorWouldWorkForMediaWithoutPreload() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+ await initMediaPlaybackDocument(tab, "audio.ogg", { preload: "none" });
+
+ info(`sound indicator should appear when audible audio starts playing`);
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when audio stops playing`);
+ await pauseMedia(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSoundIndicatorShouldDisappearAfterTabNavigation() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+ await initMediaPlaybackDocument(tab, "audio.ogg");
+
+ info(`sound indicator should appear when audible audio starts playing`);
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear after navigating tab to blank page`);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:blank");
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSoundIndicatorForAudioStream() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+ await initMediaStreamPlaybackDocument(tab);
+
+ info(`sound indicator should appear when audible audio starts playing`);
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when audio stops playing`);
+ await pauseMedia(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testPerformPlayOnMediaLoadingNewSource() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+ await initMediaPlaybackDocument(tab, "audio.ogg");
+
+ info(`sound indicator should appear when audible audio starts playing`);
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when audio stops playing`);
+ await pauseMedia(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info(`reset media src and play it again should make sound indicator appear`);
+ await assignNewSourceForAudio(tab, "audio.ogg");
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSoundIndicatorShouldDisappearWhenAbortingMedia() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+ await initMediaPlaybackDocument(tab, "audio.ogg");
+
+ info(`sound indicator should appear when audible audio starts playing`);
+ await playMedia(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when aborting audio source`);
+ await assignNewSourceForAudio(tab, "");
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testNoSoundIndicatorForMediaWithoutAudioTrack() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab({ needObserver: true });
+ await initMediaPlaybackDocument(tab, "noaudio.webm", { createVideo: true });
+
+ info(`no sound indicator should show for playing media without audio track`);
+ await playMedia(tab, { resolveOnTimeupdate: true });
+ ok(!tab.observer.hasEverUpdated(), "didn't ever update sound indicator");
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSoundIndicatorWhenChangingMediaMuted() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab({ needObserver: true });
+ await initMediaPlaybackDocument(tab, "audio.ogg", { muted: true });
+
+ info(`no sound indicator should show for playing muted media`);
+ await playMedia(tab, { resolveOnTimeupdate: true });
+ ok(!tab.observer.hasEverUpdated(), "didn't ever update sound indicator");
+
+ info(`unmuted media should make sound indicator appear`);
+ await Promise.all([
+ waitForTabSoundIndicatorAppears(tab),
+ updateMedia(tab, { muted: false }),
+ ]);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSoundIndicatorWhenChangingMediaVolume() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab({ needObserver: true });
+ await initMediaPlaybackDocument(tab, "audio.ogg", { volume: 0.0 });
+
+ info(`no sound indicator should show for playing volume zero media`);
+ await playMedia(tab, { resolveOnTimeupdate: true });
+ ok(!tab.observer.hasEverUpdated(), "didn't ever update sound indicator");
+
+ info(`unmuted media by setting volume should make sound indicator appear`);
+ await Promise.all([
+ waitForTabSoundIndicatorAppears(tab),
+ updateMedia(tab, { volume: 1.0 }),
+ ]);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Following are helper functions
+ */
+function initMediaPlaybackDocument(
+ tab,
+ fileName,
+ { preload, createVideo, muted = false, volume = 1.0 } = {}
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [fileName, preload, createVideo, muted, volume],
+ // eslint-disable-next-line no-shadow
+ async (fileName, preload, createVideo, muted, volume) => {
+ if (createVideo) {
+ content.media = content.document.createElement("video");
+ } else {
+ content.media = content.document.createElement("audio");
+ }
+ if (preload) {
+ content.media.preload = preload;
+ }
+ content.media.muted = muted;
+ content.media.volume = volume;
+ content.media.src = fileName;
+ }
+ );
+}
+
+function initMediaStreamPlaybackDocument(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ content.media = content.document.createElement("audio");
+ content.media.srcObject =
+ new content.AudioContext().createMediaStreamDestination().stream;
+ });
+}
+
+function playMedia(tab, { resolveOnTimeupdate } = {}) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [resolveOnTimeupdate],
+ // eslint-disable-next-line no-shadow
+ async resolveOnTimeupdate => {
+ await content.media.play();
+ if (resolveOnTimeupdate) {
+ await new Promise(r => (content.media.ontimeupdate = r));
+ }
+ }
+ );
+}
+
+function pauseMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ content.media.pause();
+ });
+}
+
+function assignNewSourceForAudio(tab, fileName) {
+ // eslint-disable-next-line no-shadow
+ return SpecialPowers.spawn(tab.linkedBrowser, [fileName], async fileName => {
+ content.media.src = "";
+ content.media.removeAttribute("src");
+ content.media.src = fileName;
+ });
+}
+
+function updateMedia(tab, { muted, volume } = {}) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [muted, volume],
+ // eslint-disable-next-line no-shadow
+ (muted, volume) => {
+ if (muted != undefined) {
+ content.media.muted = muted;
+ }
+ if (volume != undefined) {
+ content.media.volume = volume;
+ }
+ }
+ );
+}
diff --git a/browser/base/content/test/tabMediaIndicator/browser_mute.js b/browser/base/content/test/tabMediaIndicator/browser_mute.js
new file mode 100644
index 0000000000..826e06c3db
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mute.js
@@ -0,0 +1,19 @@
+const PAGE = "data:text/html,page";
+
+function test_on_browser(browser) {
+ ok(!browser.audioMuted, "Audio should not be muted by default");
+ browser.mute();
+ ok(browser.audioMuted, "Audio should be muted now");
+ browser.unmute();
+ ok(!browser.audioMuted, "Audio should be unmuted now");
+}
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ test_on_browser
+ );
+});
diff --git a/browser/base/content/test/tabMediaIndicator/browser_mute2.js b/browser/base/content/test/tabMediaIndicator/browser_mute2.js
new file mode 100644
index 0000000000..5e845454d3
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mute2.js
@@ -0,0 +1,32 @@
+const PAGE = "data:text/html,page";
+
+async function test_on_browser(browser) {
+ ok(!browser.audioMuted, "Audio should not be muted by default");
+ browser.mute();
+ ok(browser.audioMuted, "Audio should be muted now");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ test_on_browser2
+ );
+
+ browser.unmute();
+ ok(!browser.audioMuted, "Audio should be unmuted now");
+}
+
+function test_on_browser2(browser) {
+ ok(!browser.audioMuted, "Audio should not be muted by default");
+}
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ test_on_browser
+ );
+});
diff --git a/browser/base/content/test/tabMediaIndicator/browser_mute_webAudio.js b/browser/base/content/test/tabMediaIndicator/browser_mute_webAudio.js
new file mode 100644
index 0000000000..6f3e5f222f
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mute_webAudio.js
@@ -0,0 +1,75 @@
+// The tab closing code leaves an uncaught rejection. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+ const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+ );
+ PromiseTestUtils.expectUncaughtRejection(/is no longer, usable/);
+}
+
+const PAGE = GetTestWebBasedURL("file_webAudio.html");
+
+function start_webAudio() {
+ var startButton = content.document.getElementById("start");
+ if (!startButton) {
+ ok(false, "Can't get the start button!");
+ }
+
+ startButton.click();
+}
+
+function stop_webAudio() {
+ var stopButton = content.document.getElementById("stop");
+ if (!stopButton) {
+ ok(false, "Can't get the stop button!");
+ }
+
+ stopButton.click();
+}
+
+add_task(async function setup_test_preference() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.useAudioChannelService.testing", true],
+ ["media.block-autoplay-until-in-foreground", true],
+ ],
+ });
+});
+
+add_task(async function mute_web_audio() {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- tab should be audible -");
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info("- mute browser -");
+ ok(!tab.linkedBrowser.audioMuted, "Audio should not be muted by default");
+ let tabContent = tab.querySelector(".tab-content");
+ await hoverIcon(tabContent);
+ await clickIcon(tab.overlayIcon);
+ ok(tab.linkedBrowser.audioMuted, "Audio should be muted now");
+
+ info("- stop web audip -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], stop_webAudio);
+
+ info("- start web audio -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], start_webAudio);
+
+ info("- unmute browser -");
+ ok(tab.linkedBrowser.audioMuted, "Audio should be muted now");
+ await hoverIcon(tabContent);
+ await clickIcon(tab.overlayIcon);
+ ok(!tab.linkedBrowser.audioMuted, "Audio should be unmuted now");
+
+ info("- tab should be audible -");
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabMediaIndicator/browser_sound_indicator_silent_video.js b/browser/base/content/test/tabMediaIndicator/browser_sound_indicator_silent_video.js
new file mode 100644
index 0000000000..8f0d6961ca
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_sound_indicator_silent_video.js
@@ -0,0 +1,88 @@
+const SILENT_PAGE = GetTestWebBasedURL("file_silentAudioTrack.html");
+const ALMOST_SILENT_PAGE = GetTestWebBasedURL(
+ "file_almostSilentAudioTrack.html"
+);
+
+function check_audio_playing_state(isPlaying) {
+ let autoPlay = content.document.getElementById("autoplay");
+ if (!autoPlay) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ is(
+ autoPlay.paused,
+ !isPlaying,
+ "The playing state of autoplay audio is correct."
+ );
+
+ // wait for a while to make sure the video is playing and related event has
+ // been dispatched (if any).
+ let PLAYING_TIME_SEC = 0.5;
+ ok(PLAYING_TIME_SEC < autoPlay.duration, "The playing time is valid.");
+
+ return new Promise(resolve => {
+ autoPlay.ontimeupdate = function () {
+ if (autoPlay.currentTime > PLAYING_TIME_SEC) {
+ resolve();
+ }
+ };
+ });
+}
+
+add_task(async function should_not_show_sound_indicator_for_silent_video() {
+ info("- open new foreground tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+
+ info("- tab should not have sound indicator before playing silent video -");
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("- loading autoplay silent video -");
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, SILENT_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [true],
+ check_audio_playing_state
+ );
+
+ info("- tab should not have sound indicator after playing silent video -");
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(
+ async function should_not_show_sound_indicator_for_almost_silent_video() {
+ info("- open new foreground tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+
+ info(
+ "- tab should not have sound indicator before playing almost silent video -"
+ );
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("- loading autoplay almost silent video -");
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, ALMOST_SILENT_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [true],
+ check_audio_playing_state
+ );
+
+ info(
+ "- tab should not have sound indicator after playing almost silent video -"
+ );
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+ }
+);
diff --git a/browser/base/content/test/tabMediaIndicator/browser_webAudio_hideSoundPlayingIcon.js b/browser/base/content/test/tabMediaIndicator/browser_webAudio_hideSoundPlayingIcon.js
new file mode 100644
index 0000000000..be40f6e146
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_webAudio_hideSoundPlayingIcon.js
@@ -0,0 +1,60 @@
+/**
+ * This test is used to ensure the 'sound-playing' icon would not disappear after
+ * sites call AudioContext.resume().
+ */
+"use strict";
+
+function setup_test_preference() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.useAudioChannelService.testing", true],
+ ["browser.tabs.delayHidingAudioPlayingIconMS", 0],
+ ],
+ });
+}
+
+async function resumeAudioContext() {
+ const ac = content.ac;
+ await ac.resume();
+ ok(true, "AudioContext is resumed.");
+}
+
+async function testResumeRunningAudioContext() {
+ info(`- create new tab -`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ const browser = tab.linkedBrowser;
+
+ info(`- create audio context -`);
+ // We want the same audio context to be used across different content tasks.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.ac = new content.AudioContext();
+ const ac = content.ac;
+ const dest = ac.destination;
+ const osc = ac.createOscillator();
+ osc.connect(dest);
+ osc.start();
+ });
+
+ info(`- wait for 'sound-playing' icon showing -`);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`- resume AudioContext -`);
+ await SpecialPowers.spawn(browser, [], resumeAudioContext);
+
+ info(`- 'sound-playing' icon should still exist -`);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`- remove tab -`);
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function start_test() {
+ info("- setup test preference -");
+ await setup_test_preference();
+
+ info("- start testing -");
+ await testResumeRunningAudioContext();
+});
diff --git a/browser/base/content/test/tabMediaIndicator/browser_webAudio_silentData.js b/browser/base/content/test/tabMediaIndicator/browser_webAudio_silentData.js
new file mode 100644
index 0000000000..5831d3c0ce
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_webAudio_silentData.js
@@ -0,0 +1,57 @@
+/**
+ * This test is used to make sure we won't show the sound indicator for silent
+ * web audio.
+ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+async function waitUntilAudioContextStarts() {
+ const ac = content.ac;
+ if (ac.state == "running") {
+ return;
+ }
+
+ await new Promise(resolve => {
+ ac.onstatechange = () => {
+ if (ac.state == "running") {
+ ac.onstatechange = null;
+ resolve();
+ }
+ };
+ });
+}
+
+add_task(async function testSilentAudioContext() {
+ info(`- create new tab -`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ const browser = tab.linkedBrowser;
+
+ info(`- create audio context -`);
+ // We want the same audio context to be used across different content tasks
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.ac = new content.AudioContext();
+ const ac = content.ac;
+ const dest = ac.destination;
+ const source = new content.OscillatorNode(content.ac);
+ const gain = new content.GainNode(content.ac);
+ gain.gain.value = 0.0;
+ source.connect(gain).connect(dest);
+ source.start();
+ });
+ info(`- check AudioContext's state -`);
+ await SpecialPowers.spawn(browser, [], waitUntilAudioContextStarts);
+ ok(true, `AudioContext is running.`);
+
+ info(`- should not show sound indicator -`);
+ // If we do the next step too early, then we can't make sure whether that the
+ // reason of no showing sound indicator is because of silent web audio, or
+ // because the indicator is just not showing yet.
+ await new Promise(r => setTimeout(r, 1000));
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info(`- remove tab -`);
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabMediaIndicator/browser_webaudio_audibility_change.js b/browser/base/content/test/tabMediaIndicator/browser_webaudio_audibility_change.js
new file mode 100644
index 0000000000..8d8ce08551
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_webaudio_audibility_change.js
@@ -0,0 +1,172 @@
+const EMPTY_PAGE_URL = GetTestWebBasedURL("file_empty.html");
+
+/**
+ * When web audio changes its audible state, the sound indicator should be
+ * updated as well, which should appear only when web audio is audible.
+ */
+add_task(
+ async function testWebAudioAudibilityWouldAffectTheAppearenceOfTabSoundIndicator() {
+ info(`sound indicator should appear when web audio plays audible sound`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ EMPTY_PAGE_URL
+ );
+ await initWebAudioDocument(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when suspending web audio`);
+ await suspendWebAudio(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info(`sound indicator should appear when resuming web audio`);
+ await resumeWebAudio(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when muting web audio by docShell`);
+ await muteWebAudioByDocShell(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info(`sound indicator should appear when unmuting web audio by docShell`);
+ await unmuteWebAudioByDocShell(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when muting web audio by gain node`);
+ await muteWebAudioByGainNode(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info(`sound indicator should appear when unmuting web audio by gain node`);
+ await unmuteWebAudioByGainNode(tab);
+ await waitForTabSoundIndicatorAppears(tab);
+
+ info(`sound indicator should disappear when closing web audio`);
+ await closeWebAudio(tab);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(async function testSoundIndicatorShouldDisappearAfterTabNavigation() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+
+ info(`sound indicator should appear when audible web audio starts playing`);
+ await Promise.all([
+ initWebAudioDocument(tab),
+ waitForTabSoundIndicatorAppears(tab),
+ ]);
+
+ info(`sound indicator should disappear after navigating tab to blank page`);
+ await Promise.all([
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:blank"),
+ waitForTabSoundIndicatorDisappears(tab),
+ ]);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(
+ async function testSoundIndicatorShouldDisappearAfterWebAudioBecomesSilent() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab();
+
+ info(`sound indicator should appear when audible web audio starts playing`);
+ await Promise.all([
+ initWebAudioDocument(tab, { duration: 0.1 }),
+ waitForTabSoundIndicatorAppears(tab),
+ ]);
+
+ info(`sound indicator should disappear after web audio become silent`);
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(async function testNoSoundIndicatorWhenSimplyCreateAudioContext() {
+ info("create a tab loading media document");
+ const tab = await createBlankForegroundTab({ needObserver: true });
+
+ info(`sound indicator should not appear when simply create an AudioContext`);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ content.ac = new content.AudioContext();
+ while (content.ac.state != "running") {
+ info(`wait until web audio starts running`);
+ await new Promise(r => (content.ac.onstatechange = r));
+ }
+ });
+ ok(!tab.observer.hasEverUpdated(), "didn't ever update sound indicator");
+
+ info("remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Following are helper functions
+ */
+function initWebAudioDocument(tab, { duration } = {}) {
+ // eslint-disable-next-line no-shadow
+ return SpecialPowers.spawn(tab.linkedBrowser, [duration], async duration => {
+ content.ac = new content.AudioContext();
+ const ac = content.ac;
+ const dest = ac.destination;
+ const source = new content.OscillatorNode(ac);
+ source.start(ac.currentTime);
+ if (duration != undefined) {
+ source.stop(ac.currentTime + duration);
+ }
+ // create a gain node for future muting/unmuting
+ content.gainNode = ac.createGain();
+ source.connect(content.gainNode);
+ content.gainNode.connect(dest);
+ while (ac.state != "running") {
+ info(`wait until web audio starts running`);
+ await new Promise(r => (ac.onstatechange = r));
+ }
+ });
+}
+
+function suspendWebAudio(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ await content.ac.suspend();
+ });
+}
+
+function resumeWebAudio(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ await content.ac.resume();
+ });
+}
+
+function closeWebAudio(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ await content.ac.close();
+ });
+}
+
+function muteWebAudioByDocShell(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.docShell.allowMedia = false;
+ });
+}
+
+function unmuteWebAudioByDocShell(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.docShell.allowMedia = true;
+ });
+}
+
+function muteWebAudioByGainNode(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.gainNode.gain.setValueAtTime(0, content.ac.currentTime);
+ });
+}
+
+function unmuteWebAudioByGainNode(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.gainNode.gain.setValueAtTime(1.0, content.ac.currentTime);
+ });
+}
diff --git a/browser/base/content/test/tabMediaIndicator/file_almostSilentAudioTrack.html b/browser/base/content/test/tabMediaIndicator/file_almostSilentAudioTrack.html
new file mode 100644
index 0000000000..3ce9a68b98
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_almostSilentAudioTrack.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+</head>
+<body>
+<video id="autoplay" src="almostSilentAudioTrack.webm"></video>
+<script type="text/javascript">
+
+// In linux debug on try server, sometimes the download process would fail, so
+// we can't activate the "auto-play" or playing after receving "oncanplay".
+// Therefore, we just call play here.
+var video = document.getElementById("autoplay");
+video.loop = true;
+video.play();
+
+</script>
+</body>
diff --git a/browser/base/content/test/tabMediaIndicator/file_autoplay_media.html b/browser/base/content/test/tabMediaIndicator/file_autoplay_media.html
new file mode 100644
index 0000000000..f0dcfdab52
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_autoplay_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>autoplay media page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop autoplay></video>
+</body>
+</html>
diff --git a/browser/base/content/test/tabMediaIndicator/file_empty.html b/browser/base/content/test/tabMediaIndicator/file_empty.html
new file mode 100644
index 0000000000..13d5eeee78
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>empty page</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/tabMediaIndicator/file_mediaPlayback.html b/browser/base/content/test/tabMediaIndicator/file_mediaPlayback.html
new file mode 100644
index 0000000000..5df0bc1542
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_mediaPlayback.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<script type="text/javascript">
+var audio = new Audio();
+audio.oncanplay = function() {
+ audio.oncanplay = null;
+ audio.play();
+};
+audio.src = "audio.ogg";
+</script>
diff --git a/browser/base/content/test/tabMediaIndicator/file_mediaPlayback2.html b/browser/base/content/test/tabMediaIndicator/file_mediaPlayback2.html
new file mode 100644
index 0000000000..890b494a05
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_mediaPlayback2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<body>
+<script type="text/javascript">
+var audio = new Audio();
+audio.oncanplay = function() {
+ audio.oncanplay = null;
+ audio.play();
+};
+audio.src = "audio.ogg";
+audio.loop = true;
+audio.id = "v";
+document.body.appendChild(audio);
+</script>
+</body>
diff --git a/browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame.html b/browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame.html
new file mode 100644
index 0000000000..119db62ecc
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<iframe src="file_mediaPlayback.html"></iframe>
diff --git a/browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame2.html b/browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame2.html
new file mode 100644
index 0000000000..d96a4cd4e9
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_mediaPlaybackFrame2.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<iframe src="file_mediaPlayback2.html"></iframe>
diff --git a/browser/base/content/test/tabMediaIndicator/file_silentAudioTrack.html b/browser/base/content/test/tabMediaIndicator/file_silentAudioTrack.html
new file mode 100644
index 0000000000..afdf2c5297
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_silentAudioTrack.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+</head>
+<body>
+<video id="autoplay" src="silentAudioTrack.webm"></video>
+<script type="text/javascript">
+
+// In linux debug on try server, sometimes the download process would fail, so
+// we can't activate the "auto-play" or playing after receving "oncanplay".
+// Therefore, we just call play here.
+var video = document.getElementById("autoplay");
+video.loop = true;
+video.play();
+
+</script>
+</body>
diff --git a/browser/base/content/test/tabMediaIndicator/file_webAudio.html b/browser/base/content/test/tabMediaIndicator/file_webAudio.html
new file mode 100644
index 0000000000..f6fb5e7c07
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/file_webAudio.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+</head>
+<body>
+<pre id=state></pre>
+<button id="start" onclick="start_webaudio()">Start</button>
+<button id="stop" onclick="stop_webaudio()">Stop</button>
+<script type="text/javascript">
+ var ac = new AudioContext();
+ var dest = ac.destination;
+ var osc = ac.createOscillator();
+ osc.connect(dest);
+ osc.start();
+ document.querySelector("pre").innerText = ac.state;
+ ac.onstatechange = function() {
+ document.querySelector("pre").innerText = ac.state;
+ }
+
+ function start_webaudio() {
+ ac.resume();
+ }
+
+ function stop_webaudio() {
+ ac.suspend();
+ }
+</script>
+</body>
diff --git a/browser/base/content/test/tabMediaIndicator/gizmo.mp4 b/browser/base/content/test/tabMediaIndicator/gizmo.mp4
new file mode 100644
index 0000000000..87efad5ade
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/gizmo.mp4
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/head.js b/browser/base/content/test/tabMediaIndicator/head.js
new file mode 100644
index 0000000000..8b82e21a43
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/head.js
@@ -0,0 +1,158 @@
+/**
+ * Global variables for testing.
+ */
+const gEMPTY_PAGE_URL = GetTestWebBasedURL("file_empty.html");
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ * @param {Boolean} cors [optional]
+ * if set, then return a url with different origin
+ */
+function GetTestWebBasedURL(fileName, cors = false) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const origin = cors ? "http://example.org" : "http://example.com";
+ return (
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ fileName
+ );
+}
+
+/**
+ * Wait until tab sound indicator appears on the given tab.
+ * @param {tabbrowser} tab
+ * given tab where tab sound indicator should appear
+ */
+async function waitForTabSoundIndicatorAppears(tab) {
+ if (!tab.soundPlaying) {
+ info("Tab sound indicator doesn't appear yet");
+ await BrowserTestUtils.waitForEvent(
+ tab,
+ "TabAttrModified",
+ false,
+ event => {
+ return event.detail.changed.includes("soundplaying");
+ }
+ );
+ }
+ ok(tab.soundPlaying, "Tab sound indicator appears");
+}
+
+/**
+ * Wait until tab sound indicator disappears on the given tab.
+ * @param {tabbrowser} tab
+ * given tab where tab sound indicator should disappear
+ */
+async function waitForTabSoundIndicatorDisappears(tab) {
+ if (tab.soundPlaying) {
+ info("Tab sound indicator doesn't disappear yet");
+ await BrowserTestUtils.waitForEvent(
+ tab,
+ "TabAttrModified",
+ false,
+ event => {
+ return event.detail.changed.includes("soundplaying");
+ }
+ );
+ }
+ ok(!tab.soundPlaying, "Tab sound indicator disappears");
+}
+
+/**
+ * Return a new foreground tab loading with an empty file.
+ * @param {boolean} needObserver
+ * If true, sets an observer property on the returned tab. This property
+ * exposes `hasEverUpdated()` which will return a bool indicating if the
+ * sound indicator has ever updated.
+ */
+async function createBlankForegroundTab({ needObserver } = {}) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ gEMPTY_PAGE_URL
+ );
+ if (needObserver) {
+ tab.observer = createSoundIndicatorObserver(tab);
+ }
+ return tab;
+}
+
+function createSoundIndicatorObserver(tab) {
+ let hasEverUpdated = false;
+ let listener = event => {
+ if (event.detail.changed.includes("soundplaying")) {
+ hasEverUpdated = true;
+ }
+ };
+ tab.addEventListener("TabAttrModified", listener);
+ return {
+ hasEverUpdated: () => {
+ tab.removeEventListener("TabAttrModified", listener);
+ return hasEverUpdated;
+ },
+ };
+}
+
+/**
+ * Sythesize mouse hover on the given icon, which would sythesize `mouseover`
+ * and `mousemove` event on that. Return a promise that will be resolved when
+ * the tooptip element shows.
+ * @param {tab icon} icon
+ * the icon on which we want to mouse hover
+ * @param {tooltip element} tooltip
+ * the tab tooltip elementss
+ */
+function hoverIcon(icon, tooltip) {
+ disableNonTestMouse(true);
+
+ if (!tooltip) {
+ tooltip = document.getElementById("tabbrowser-tab-tooltip");
+ }
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+ EventUtils.synthesizeMouse(icon, 1, 1, { type: "mouseover" });
+ EventUtils.synthesizeMouse(icon, 2, 2, { type: "mousemove" });
+ EventUtils.synthesizeMouse(icon, 3, 3, { type: "mousemove" });
+ EventUtils.synthesizeMouse(icon, 4, 4, { type: "mousemove" });
+ return popupShownPromise;
+}
+
+/**
+ * Leave mouse from the given icon, which would sythesize `mouseout`
+ * and `mousemove` event on that.
+ * @param {tab icon} icon
+ * the icon on which we want to mouse hover
+ * @param {tooltip element} tooltip
+ * the tab tooltip elementss
+ */
+function leaveIcon(icon) {
+ EventUtils.synthesizeMouse(icon, 0, 0, { type: "mouseout" });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+
+ disableNonTestMouse(false);
+}
+
+/**
+ * Sythesize mouse click on the given icon.
+ * @param {tab icon} icon
+ * the icon on which we want to mouse hover
+ */
+async function clickIcon(icon) {
+ await hoverIcon(icon);
+ EventUtils.synthesizeMouseAtCenter(icon, { button: 0 });
+ leaveIcon(icon);
+}
+
+function disableNonTestMouse(disable) {
+ let utils = window.windowUtils;
+ utils.disableNonTestMouseEvents(disable);
+}
diff --git a/browser/base/content/test/tabMediaIndicator/noaudio.webm b/browser/base/content/test/tabMediaIndicator/noaudio.webm
new file mode 100644
index 0000000000..9207017fb6
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/noaudio.webm
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/silentAudioTrack.webm b/browser/base/content/test/tabMediaIndicator/silentAudioTrack.webm
new file mode 100644
index 0000000000..8e08a86c45
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/silentAudioTrack.webm
Binary files differ
diff --git a/browser/base/content/test/tabPrompts/auth-route.sjs b/browser/base/content/test/tabPrompts/auth-route.sjs
new file mode 100644
index 0000000000..4f113a8add
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/auth-route.sjs
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function handleRequest(request, response) {
+ let body;
+ // guest:guest
+ let expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
+ // correct login credentials provided
+ if (
+ request.hasHeader("Authorization") &&
+ request.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(request.httpVersion, 200, "OK, authorized");
+ response.setHeader("Content-Type", "text", false);
+
+ body = "success";
+ } else {
+ // incorrect credentials
+ response.setStatusLine(request.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+ response.setHeader("Content-Type", "text", false);
+
+ body = "failed";
+ }
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/browser/base/content/test/tabPrompts/browser.ini b/browser/base/content/test/tabPrompts/browser.ini
new file mode 100644
index 0000000000..2834b6d2af
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser.ini
@@ -0,0 +1,30 @@
+[browser_abort_when_in_modal_state.js]
+[browser_auth_spoofing_protection.js]
+support-files =
+ redirect-crossDomain.html
+ redirect-sameDomain.html
+ auth-route.sjs
+[browser_auth_spoofing_url_copy.js]
+support-files =
+ redirect-crossDomain.html
+ auth-route.sjs
+[browser_auth_spoofing_url_drag_and_drop.js]
+support-files =
+ redirect-crossDomain.html
+ redirect-sameDomain.html
+ auth-route.sjs
+[browser_beforeunload_urlbar.js]
+support-files = file_beforeunload_stop.html
+[browser_closeTabSpecificPanels.js]
+skip-if = verify && debug && (os == 'linux')
+[browser_confirmFolderUpload.js]
+[browser_contentOrigins.js]
+support-files = file_beforeunload_stop.html
+[browser_multiplePrompts.js]
+[browser_openPromptInBackgroundTab.js]
+https_first_disabled = true
+support-files = openPromptOffTimeout.html
+[browser_promptFocus.js]
+[browser_prompt_closed_window.js]
+[browser_switchTabPermissionPrompt.js]
+[browser_windowPrompt.js]
diff --git a/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js b/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js
new file mode 100644
index 0000000000..cb3a1f72d6
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+/**
+ * Check that if we're using a window-modal prompt,
+ * the next synchronous window-internal modal prompt aborts rather than
+ * leaving us in a deadlock about how to enter modal state.
+ */
+add_task(async function test_check_multiple_prompts() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.windowPromptSubDialog", true]],
+ });
+ let container = document.getElementById("window-modal-dialog");
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+
+ let firstDialogClosedPromise = new Promise(resolve => {
+ // Avoid blocking the test on the (sync) alert by sticking it in a timeout:
+ setTimeout(() => {
+ Services.prompt.alertBC(
+ window.browsingContext,
+ Ci.nsIPrompt.MODAL_TYPE_WINDOW,
+ "Some title",
+ "some message"
+ );
+ resolve();
+ }, 0);
+ });
+ let dialogWin = await dialogPromise;
+
+ // Check circumstances of opening.
+ ok(
+ !dialogWin.docShell.chromeEventHandler,
+ "Should not have embedded the dialog."
+ );
+
+ PromiseTestUtils.expectUncaughtRejection(/could not be shown/);
+ let rv = Services.prompt.confirm(
+ window,
+ "I should not appear",
+ "because another prompt was open"
+ );
+ is(rv, false, "Prompt should have been canceled.");
+
+ info("Accepting dialog");
+ dialogWin.document.querySelector("dialog").acceptDialog();
+
+ await firstDialogClosedPromise;
+
+ await BrowserTestUtils.waitForMutationCondition(
+ container,
+ { childList: true, attributes: true },
+ () => !container.hasChildNodes() && !container.open
+ );
+});
diff --git a/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js b/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js
new file mode 100644
index 0000000000..ca139df8e4
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+let TEST_PATH_AUTH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.org"
+);
+
+const CROSS_DOMAIN_URL = TEST_PATH + "redirect-crossDomain.html";
+
+const SAME_DOMAIN_URL = TEST_PATH + "redirect-sameDomain.html";
+
+const AUTH_URL = TEST_PATH_AUTH + "auth-route.sjs";
+
+/**
+ * Opens a new tab with a url that ether redirects us cross or same domain
+ *
+ * @param {Boolean} doConfirmPrompt - true if we want to test the case when the user accepts the prompt,
+ * false if we want to test the case when the user cancels the prompt.
+ * @param {Boolean} crossDomain - if true we will open a url that redirects us to a cross domain url,
+ * if false, we will open a url that redirects us to a same domain url
+ * @param {Boolean} prefEnabled true will enable "privacy.authPromptSpoofingProtection",
+ * false will disable the pref
+ */
+async function trigger401AndHandle(doConfirmPrompt, crossDomain, prefEnabled) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.authPromptSpoofingProtection", prefEnabled]],
+ });
+ let url = crossDomain ? CROSS_DOMAIN_URL : SAME_DOMAIN_URL;
+ let dialogShown = waitForDialog(doConfirmPrompt, crossDomain, prefEnabled);
+ await BrowserTestUtils.withNewTab(url, async function () {
+ await dialogShown;
+ });
+ await new Promise(resolve => {
+ Services.clearData.deleteData(
+ Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
+ resolve
+ );
+ });
+}
+
+async function waitForDialog(doConfirmPrompt, crossDomain, prefEnabled) {
+ await TestUtils.topicObserved("common-dialog-loaded");
+ let dialog = gBrowser.getTabDialogBox(gBrowser.selectedBrowser)
+ ._tabDialogManager._topDialog;
+ let dialogDocument = dialog._frame.contentDocument;
+ if (crossDomain) {
+ if (prefEnabled) {
+ Assert.equal(
+ dialog._overlay.getAttribute("hideContent"),
+ "true",
+ "Dialog overlay hides the current sites content"
+ );
+ Assert.equal(
+ window.gURLBar.value,
+ AUTH_URL,
+ "Correct location is provided by the prompt"
+ );
+ Assert.equal(
+ window.gBrowser.selectedTab.label,
+ "example.org",
+ "Tab title is manipulated"
+ );
+ // switch to another tab and make sure we dont mess up this new tabs url bar and tab title
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org:443"
+ );
+ Assert.equal(
+ window.gURLBar.value,
+ "https://example.org",
+ "No location is provided by the prompt, correct location is displayed"
+ );
+ Assert.equal(
+ window.gBrowser.selectedTab.label,
+ "mochitest index /",
+ "Tab title is not manipulated"
+ );
+ // switch back to our tab with the prompt and make sure the url bar state and tab title is still there
+ BrowserTestUtils.removeTab(tab);
+ Assert.equal(
+ window.gURLBar.value,
+ AUTH_URL,
+ "Correct location is provided by the prompt"
+ );
+ Assert.equal(
+ window.gBrowser.selectedTab.label,
+ "example.org",
+ "Tab title is manipulated"
+ );
+ // make sure a value that the user types in has a higher priority than our prompts location
+ gBrowser.selectedBrowser.userTypedValue = "user value";
+ gURLBar.setURI();
+ Assert.equal(
+ window.gURLBar.value,
+ "user value",
+ "User typed value is shown"
+ );
+ // if the user clears the url bar we again fall back to the location of the prompt if we trigger setURI by a tab switch
+ gBrowser.selectedBrowser.userTypedValue = "";
+ gURLBar.setURI(null, true);
+ Assert.equal(
+ window.gURLBar.value,
+ AUTH_URL,
+ "Correct location is provided by the prompt"
+ );
+ // Cross domain and pref is not enabled
+ } else {
+ Assert.equal(
+ dialog._overlay.getAttribute("hideContent"),
+ "",
+ "Dialog overlay does not hide the current sites content"
+ );
+ Assert.equal(
+ window.gURLBar.value,
+ CROSS_DOMAIN_URL,
+ "No location is provided by the prompt, correct location is displayed"
+ );
+ Assert.equal(
+ window.gBrowser.selectedTab.label,
+ "example.com",
+ "Tab title is not manipulated"
+ );
+ }
+ // same domain
+ } else {
+ Assert.equal(
+ dialog._overlay.getAttribute("hideContent"),
+ "",
+ "Dialog overlay does not hide the current sites content"
+ );
+ Assert.equal(
+ window.gURLBar.value,
+ SAME_DOMAIN_URL,
+ "No location is provided by the prompt, correct location is displayed"
+ );
+ Assert.equal(
+ window.gBrowser.selectedTab.label,
+ "example.com",
+ "Tab title is not manipulated"
+ );
+ }
+
+ let onDialogClosed = BrowserTestUtils.waitForEvent(
+ window,
+ "DOMModalDialogClosed"
+ );
+ if (doConfirmPrompt) {
+ dialogDocument.getElementById("loginTextbox").value = "guest";
+ dialogDocument.getElementById("password1Textbox").value = "guest";
+ dialogDocument.getElementById("commonDialog").acceptDialog();
+ } else {
+ dialogDocument.getElementById("commonDialog").cancelDialog();
+ }
+
+ // wait for the dialog to be closed to check that the URLBar state is reset
+ await onDialogClosed;
+ // Due to bug 1812014, the url bar will be clear if we have set its value to "" while the prompt was open
+ // so we trigger a tab switch again to have the uri displayed to be able to check its value
+ gURLBar.setURI(null, true);
+ Assert.equal(
+ window.gURLBar.value,
+ crossDomain ? CROSS_DOMAIN_URL : SAME_DOMAIN_URL,
+ "No location is provided by the prompt"
+ );
+ Assert.equal(
+ window.gBrowser.selectedTab.label,
+ "example.com",
+ "Tab title is not manipulated"
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.authPromptSpoofingProtection", true]],
+ });
+});
+
+/**
+ * Tests that the 401 auth spoofing mechanisms apply if the 401 is from a different base domain than the current sites,
+ * canceling the prompt
+ */
+add_task(async function testCrossDomainCancelPrefEnabled() {
+ await trigger401AndHandle(false, true, true);
+});
+
+/**
+ * Tests that the 401 auth spoofing mechanisms apply if the 401 is from a different base domain than the current sites,
+ * accepting the prompt
+ */
+add_task(async function testCrossDomainAcceptPrefEnabled() {
+ await trigger401AndHandle(true, true, true);
+});
+
+/**
+ * Tests that the 401 auth spoofing mechanisms do not apply if "privacy.authPromptSpoofingProtection" is not set to true
+ * canceling the prompt
+ */
+add_task(async function testCrossDomainCancelPrefDisabled() {
+ await trigger401AndHandle(false, true, false);
+});
+
+/**
+ * Tests that the 401 auth spoofing mechanisms do not apply if "privacy.authPromptSpoofingProtection" is not set to true,
+ * accepting the prompt
+ */
+add_task(async function testCrossDomainAcceptPrefDisabled() {
+ await trigger401AndHandle(true, true, false);
+});
+
+/**
+ * Tests that the 401 auth spoofing mechanisms are not triggered by a 401 within the same base domain as the current sites,
+ * canceling the prompt
+ */
+add_task(async function testSameDomainCancelPrefEnabled() {
+ await trigger401AndHandle(false, false, true);
+});
+
+/**
+ * Tests that the 401 auth spoofing mechanisms are not triggered by a 401 within the same base domain as the current sites,
+ * accepting the prompt
+ */
+add_task(async function testSameDomainAcceptPrefEnabled() {
+ await trigger401AndHandle(true, false, true);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js
new file mode 100644
index 0000000000..5bea05020e
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+let TEST_PATH_AUTH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.org"
+);
+
+const CROSS_DOMAIN_URL = TEST_PATH + "redirect-crossDomain.html";
+
+const AUTH_URL = TEST_PATH_AUTH + "auth-route.sjs";
+
+/**
+ * Opens a new tab with a url that redirects us cross domain
+ * tests that auth anti-spoofing mechanisms cover url copy while prompt is open
+ *
+ */
+async function trigger401AndHandle() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.authPromptSpoofingProtection", true]],
+ });
+ let dialogShown = waitForDialogAndCopyURL();
+ await BrowserTestUtils.withNewTab(CROSS_DOMAIN_URL, async function () {
+ await dialogShown;
+ });
+ await new Promise(resolve => {
+ Services.clearData.deleteData(
+ Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
+ resolve
+ );
+ });
+}
+
+async function waitForDialogAndCopyURL() {
+ await TestUtils.topicObserved("common-dialog-loaded");
+ let dialog = gBrowser.getTabDialogBox(gBrowser.selectedBrowser)
+ ._tabDialogManager._topDialog;
+ let dialogDocument = dialog._frame.contentDocument;
+
+ //select the whole URL
+ gURLBar.focus();
+ await SimpleTest.promiseClipboardChange(AUTH_URL, () => {
+ Assert.equal(gURLBar.value, AUTH_URL, "url bar copy value set");
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ });
+
+ // select only part of the URL
+ gURLBar.focus();
+ let endOfSelectionRange = AUTH_URL.indexOf("/auth-route.sjs");
+ await SimpleTest.promiseClipboardChange(
+ AUTH_URL.substring(0, endOfSelectionRange),
+ () => {
+ Assert.equal(gURLBar.value, AUTH_URL, "url bar copy value set");
+ gURLBar.selectionStart = 0;
+ gURLBar.selectionEnd = endOfSelectionRange;
+ goDoCommand("cmd_copy");
+ }
+ );
+ let onDialogClosed = BrowserTestUtils.waitForEvent(
+ window,
+ "DOMModalDialogClosed"
+ );
+ dialogDocument.getElementById("commonDialog").cancelDialog();
+
+ await onDialogClosed;
+ Assert.equal(
+ window.gURLBar.value,
+ CROSS_DOMAIN_URL,
+ "No location is provided by the prompt"
+ );
+
+ //select the whole URL after URL is reset to normal
+ gURLBar.focus();
+ await SimpleTest.promiseClipboardChange(CROSS_DOMAIN_URL, () => {
+ Assert.equal(gURLBar.value, CROSS_DOMAIN_URL, "url bar copy value set");
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ });
+}
+
+/**
+ * Tests that the 401 auth spoofing mechanisms covers the url bar copy action properly,
+ * canceling the prompt
+ */
+add_task(async function testUrlCopy() {
+ await trigger401AndHandle();
+});
diff --git a/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js
new file mode 100644
index 0000000000..564f1fa9a7
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+let TEST_PATH_AUTH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.org"
+);
+
+const CROSS_DOMAIN_URL = TEST_PATH + "redirect-crossDomain.html";
+
+const SAME_DOMAIN_URL = TEST_PATH + "redirect-sameDomain.html";
+
+const AUTH_URL = TEST_PATH_AUTH + "auth-route.sjs";
+
+/**
+ * Opens a new tab with a url that ether redirects us cross or same domain
+ *
+ * @param {Boolean} crossDomain - if true we will open a url that redirects us to a cross domain url,
+ * if false, we will open a url that redirects us to a same domain url
+ */
+async function trigger401AndHandle(crossDomain) {
+ let dialogShown = waitForDialogAndDragNDropURL(crossDomain);
+ await BrowserTestUtils.withNewTab(
+ crossDomain ? CROSS_DOMAIN_URL : SAME_DOMAIN_URL,
+ async function () {
+ await dialogShown;
+ }
+ );
+ await new Promise(resolve => {
+ Services.clearData.deleteData(
+ Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
+ resolve
+ );
+ });
+}
+
+async function waitForDialogAndDragNDropURL(crossDomain) {
+ await TestUtils.topicObserved("common-dialog-loaded");
+ let dialog = gBrowser.getTabDialogBox(gBrowser.selectedBrowser)
+ ._tabDialogManager._topDialog;
+ let dialogDocument = dialog._frame.contentDocument;
+
+ let urlbar = document.getElementById("urlbar-input");
+ let dataTran = new DataTransfer();
+ let urlEvent = new DragEvent("dragstart", { dataTransfer: dataTran });
+ let urlBarContainer = document.getElementById("urlbar-input-container");
+ urlBarContainer.click();
+ // trigger a drag event in the gUrlBar
+ urlbar.dispatchEvent(urlEvent);
+ // this should set some propperties on our event, like the url we are dragging
+ if (crossDomain) {
+ is(
+ urlEvent.dataTransfer.getData("text/plain"),
+ AUTH_URL,
+ "correct cross Domain URL is dragged over"
+ );
+ } else {
+ is(
+ urlEvent.dataTransfer.getData("text/plain"),
+ SAME_DOMAIN_URL,
+ "correct same domain URL is dragged over"
+ );
+ }
+
+ let onDialogClosed = BrowserTestUtils.waitForEvent(
+ window,
+ "DOMModalDialogClosed"
+ );
+ dialogDocument.getElementById("commonDialog").cancelDialog();
+
+ await onDialogClosed;
+}
+
+/**
+ * Tests that the 401 auth spoofing mechanisms covers the url bar drag and drop action propperly,
+ */
+add_task(async function testUrlDragAndDrop() {
+ await trigger401AndHandle(true);
+});
+
+/**
+ * Tests that the 401 auth spoofing mechanisms do not apply to the url bar drag and drop action if the 401 is not from a different base domain,
+ */
+add_task(async function testUrlDragAndDrop() {
+ await trigger401AndHandle(false);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js b/browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js
new file mode 100644
index 0000000000..cb53783c99
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+add_task(async function test_beforeunload_stay_clears_urlbar() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ const TEST_URL = TEST_ROOT + "file_beforeunload_stop.html";
+ await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
+ gURLBar.focus();
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const inputValue = "http://example.org/?q=typed";
+ gURLBar.inputField.value = inputValue.slice(0, -1);
+ EventUtils.sendString(inputValue.slice(-1));
+
+ if (CONTENT_PROMPT_SUBDIALOG) {
+ let promptOpenedPromise =
+ BrowserTestUtils.promiseAlertDialogOpen("cancel");
+ EventUtils.synthesizeKey("VK_RETURN");
+ await promptOpenedPromise;
+ await TestUtils.waitForTick();
+ } else {
+ let promptOpenedPromise = TestUtils.topicObserved(
+ "tabmodal-dialog-loaded"
+ );
+ EventUtils.synthesizeKey("VK_RETURN");
+ await promptOpenedPromise;
+ let promptElement = browser.parentNode.querySelector("tabmodalprompt");
+
+ // Click the cancel button
+ promptElement.querySelector(".tabmodalprompt-button1").click();
+ await TestUtils.waitForCondition(
+ () => promptElement.parentNode == null,
+ "tabprompt should be removed"
+ );
+ }
+
+ // Can't just compare directly with TEST_URL because the URL may be trimmed.
+ // Just need it to not be the example.org thing we typed in.
+ ok(
+ gURLBar.value.endsWith("_stop.html"),
+ "Url bar should be reset to point to the stop html file"
+ );
+ ok(
+ gURLBar.value.includes("example.com"),
+ "Url bar should be reset to example.com"
+ );
+ // Check the lock/identity icons are back:
+ is(
+ gURLBar.textbox.getAttribute("pageproxystate"),
+ "valid",
+ "Should be in valid pageproxy state."
+ );
+
+ // Now we need to get rid of the handler to avoid the prompt coming up when trying to close the
+ // tab when we exit `withNewTab`. :-)
+ await SpecialPowers.spawn(browser, [], function () {
+ content.window.onbeforeunload = null;
+ });
+ });
+});
diff --git a/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js
new file mode 100644
index 0000000000..8d789ad512
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js
@@ -0,0 +1,53 @@
+"use strict";
+
+/*
+ * This test creates multiple panels, one that has been tagged as specific to its tab's content
+ * and one that isn't. When a tab loses focus, panel specific to that tab should close.
+ * The non-specific panel should remain open.
+ *
+ */
+
+add_task(async function () {
+ let tab1 = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/#0");
+ let tab2 = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/#1");
+ let specificPanel = document.createXULElement("panel");
+ specificPanel.setAttribute("tabspecific", "true");
+ specificPanel.setAttribute("noautohide", "true");
+ let generalPanel = document.createXULElement("panel");
+ generalPanel.setAttribute("noautohide", "true");
+ let anchor = document.getElementById(CustomizableUI.AREA_NAVBAR);
+
+ anchor.appendChild(specificPanel);
+ anchor.appendChild(generalPanel);
+ is(specificPanel.state, "closed", "specificPanel starts as closed");
+ is(generalPanel.state, "closed", "generalPanel starts as closed");
+
+ let specificPanelPromise = BrowserTestUtils.waitForEvent(
+ specificPanel,
+ "popupshown"
+ );
+ specificPanel.openPopupAtScreen(210, 210);
+ await specificPanelPromise;
+ is(specificPanel.state, "open", "specificPanel has been opened");
+
+ let generalPanelPromise = BrowserTestUtils.waitForEvent(
+ generalPanel,
+ "popupshown"
+ );
+ generalPanel.openPopupAtScreen(510, 510);
+ await generalPanelPromise;
+ is(generalPanel.state, "open", "generalPanel has been opened");
+
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(
+ specificPanel.state,
+ "closed",
+ "specificPanel panel is closed after its tab loses focus"
+ );
+ is(generalPanel.state, "open", "generalPanel is still open after tab switch");
+
+ specificPanel.remove();
+ generalPanel.remove();
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
new file mode 100644
index 0000000000..62b0ed4f2b
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+/**
+ * Create a temporary test directory that will be cleaned up on test shutdown.
+ * @returns {String} - absolute directory path.
+ */
+function getTestDirectory() {
+ let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tmpDir.append("testdir");
+ if (!tmpDir.exists()) {
+ tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ registerCleanupFunction(() => {
+ tmpDir.remove(true);
+ });
+ }
+
+ let file1 = tmpDir.clone();
+ file1.append("foo.txt");
+ if (!file1.exists()) {
+ file1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+
+ let file2 = tmpDir.clone();
+ file2.append("bar.txt");
+ if (!file2.exists()) {
+ file2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+
+ return tmpDir.path;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Allow using our MockFilePicker in the content process.
+ ["dom.filesystem.pathcheck.disabled", true],
+ ["dom.webkitBlink.dirPicker.enabled", true],
+ ],
+ });
+});
+
+/**
+ * Create a file input, select a folder and wait for the upload confirmation
+ * prompt to open.
+ * @param {boolean} confirmUpload - Whether to accept (true) or cancel the
+ * prompt (false).
+ * @returns {Promise} - Resolves once the prompt has been closed.
+ */
+async function testUploadPrompt(confirmUpload) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ // Create file input element
+ await ContentTask.spawn(browser, null, () => {
+ let input = content.document.createElement("input");
+ input.id = "filepicker";
+ input.setAttribute("type", "file");
+ input.setAttribute("webkitdirectory", "");
+ content.document.body.appendChild(input);
+ });
+
+ // If we're confirming the dialog, register a "change" listener on the
+ // file input.
+ let changePromise;
+ if (confirmUpload) {
+ changePromise = ContentTask.spawn(browser, null, async () => {
+ let input = content.document.getElementById("filepicker");
+ return ContentTaskUtils.waitForEvent(input, "change").then(
+ e => e.target.files.length
+ );
+ });
+ }
+
+ // Register prompt promise
+ let promptPromise = PromptTestUtils.waitForPrompt(browser, {
+ modalType: Services.prompt.MODAL_TYPE_TAB,
+ promptType: "confirmEx",
+ });
+
+ // Open filepicker
+ let path = getTestDirectory();
+ await ContentTask.spawn(browser, { path }, args => {
+ let MockFilePicker = content.SpecialPowers.MockFilePicker;
+ MockFilePicker.init(
+ content,
+ "A Mock File Picker",
+ content.SpecialPowers.Ci.nsIFilePicker.modeGetFolder
+ );
+ MockFilePicker.useDirectory(args.path);
+
+ let input = content.document.getElementById("filepicker");
+ input.click();
+ });
+
+ // Wait for confirmation prompt
+ let prompt = await promptPromise;
+ ok(prompt, "Shown upload confirmation prompt");
+ is(prompt.ui.button0.label, "Upload", "Accept button label");
+ ok(prompt.ui.button1.hasAttribute("default"), "Cancel is default button");
+
+ // Close confirmation prompt
+ await PromptTestUtils.handlePrompt(prompt, {
+ buttonNumClick: confirmUpload ? 0 : 1,
+ });
+
+ // If we accepted, wait for the input elements "change" event
+ if (changePromise) {
+ let fileCount = await changePromise;
+ is(fileCount, 2, "Should have selected 2 files");
+ } else {
+ let fileCount = await ContentTask.spawn(browser, null, () => {
+ return content.document.getElementById("filepicker").files.length;
+ });
+
+ is(fileCount, 0, "Should not have selected any files");
+ }
+
+ // Cleanup
+ await ContentTask.spawn(browser, null, () => {
+ content.SpecialPowers.MockFilePicker.cleanup();
+ });
+ });
+}
+
+// Tests the confirmation prompt that shows after the user picked a folder.
+
+// Confirm the prompt
+add_task(async function test_confirm() {
+ await testUploadPrompt(true);
+});
+
+// Cancel the prompt
+add_task(async function test_cancel() {
+ await testUploadPrompt(false);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_contentOrigins.js b/browser/base/content/test/tabPrompts/browser_contentOrigins.js
new file mode 100644
index 0000000000..d39c8a3f67
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_contentOrigins.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+async function checkAlert(
+ pageToLoad,
+ expectedTitle,
+ expectedIcon = "chrome://global/skin/icons/defaultFavicon.svg"
+) {
+ function openFn(browser) {
+ return SpecialPowers.spawn(browser, [], () => {
+ if (content.document.nodePrincipal.isSystemPrincipal) {
+ // Can't eval in privileged contexts due to CSP, just call directly:
+ content.alert("Test");
+ } else {
+ // Eval everywhere else so it gets the principal of the loaded page.
+ content.eval("alert('Test')");
+ }
+ });
+ }
+ return checkDialog(pageToLoad, openFn, expectedTitle, expectedIcon);
+}
+
+async function checkBeforeunload(
+ pageToLoad,
+ expectedTitle,
+ expectedIcon = "chrome://global/skin/icons/defaultFavicon.svg"
+) {
+ async function openFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ {},
+ browser.browsingContext
+ );
+ return gBrowser.removeTab(tab); // trigger beforeunload.
+ }
+ return checkDialog(pageToLoad, openFn, expectedTitle, expectedIcon);
+}
+
+async function checkDialog(
+ pageToLoad,
+ openFn,
+ expectedTitle,
+ expectedIcon,
+ modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT
+) {
+ return BrowserTestUtils.withNewTab(pageToLoad, async browser => {
+ let promptPromise = PromptTestUtils.waitForPrompt(browser, {
+ modalType,
+ });
+ let spawnPromise = openFn(browser);
+ let dialog = await promptPromise;
+
+ let doc = dialog.ui.prompt.document;
+ let titleEl = doc.getElementById("titleText");
+ if (expectedTitle.value) {
+ is(titleEl.textContent, expectedTitle.value, "Title should match.");
+ } else {
+ is(
+ titleEl.dataset.l10nId,
+ expectedTitle.l10nId,
+ "Title l10n id should match."
+ );
+ }
+ ok(
+ !titleEl.parentNode.hasAttribute("overflown"),
+ "Title should fit without overflowing."
+ );
+
+ ok(BrowserTestUtils.is_visible(titleEl), "New title should be shown.");
+ ok(
+ BrowserTestUtils.is_hidden(doc.getElementById("infoTitle")),
+ "Old title should be hidden."
+ );
+ let iconCS = doc.ownerGlobal.getComputedStyle(
+ doc.querySelector(".titleIcon")
+ );
+ is(
+ iconCS.backgroundImage,
+ `url("${expectedIcon}")`,
+ "Icon is as expected."
+ );
+
+ // This is not particularly neat, but we want to also test overflow
+ // Our test systems don't have hosts that long, so just fake it:
+ if (browser.currentURI.asciiHost == "example.com") {
+ let longerDomain = "extravagantly.long.".repeat(10) + "example.com";
+ doc.documentElement.setAttribute(
+ "headertitle",
+ JSON.stringify({ raw: longerDomain, shouldUseMaskFade: true })
+ );
+ info("Wait for the prompt title to update.");
+ await BrowserTestUtils.waitForMutationCondition(
+ titleEl,
+ { characterData: true, attributes: true },
+ () =>
+ titleEl.textContent == longerDomain &&
+ titleEl.parentNode.hasAttribute("overflown")
+ );
+ is(titleEl.textContent, longerDomain, "The longer domain is reflected.");
+ ok(
+ titleEl.parentNode.hasAttribute("overflown"),
+ "The domain should overflow."
+ );
+ }
+
+ // Close the prompt again.
+ await PromptTestUtils.handlePrompt(dialog);
+ // The alert in the content process was sync, we need to make sure it gets
+ // cleaned up, but couldn't await it above because that'd hang the test!
+ await spawnPromise;
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["prompts.contentPromptSubDialog", true],
+ ["prompts.modalType.httpAuth", Ci.nsIPrompt.MODAL_TYPE_TAB],
+ ["prompts.tabChromePromptSubDialog", true],
+ ],
+ });
+});
+
+add_task(async function test_check_prompt_origin_display() {
+ await checkAlert("https://example.com/", { value: "example.com" });
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await checkAlert("http://example.com/", { value: "example.com" });
+ await checkAlert("data:text/html,<body>", {
+ l10nId: "common-dialog-title-null",
+ });
+
+ let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
+ let fileURI = Services.io.newFileURI(homeDir).spec;
+ await checkAlert(fileURI, { value: "file://" });
+
+ await checkAlert(
+ "about:config",
+ { l10nId: "common-dialog-title-system" },
+ "chrome://branding/content/icon32.png"
+ );
+
+ await checkBeforeunload(TEST_ROOT + "file_beforeunload_stop.html", {
+ value: "example.com",
+ });
+});
+
+add_task(async function test_check_auth() {
+ let server = new HttpServer();
+ registerCleanupFunction(() => {
+ return new Promise(resolve => {
+ server.stop(() => {
+ server = null;
+ resolve();
+ });
+ });
+ });
+
+ function forbiddenHandler(meta, res) {
+ res.setStatusLine(meta.httpVersion, 401, "Unauthorized");
+ res.setHeader("WWW-Authenticate", 'Basic realm="Realm"');
+ }
+ function pageHandler(meta, res) {
+ res.setStatusLine(meta.httpVersion, 200, "OK");
+ res.setHeader("Content-Type", "text/html");
+ let body = "<html><body></body></html>";
+ res.bodyOutputStream.write(body, body.length);
+ }
+ server.registerPathHandler("/forbidden", forbiddenHandler);
+ server.registerPathHandler("/page", pageHandler);
+ server.start(-1);
+
+ const HOST = `localhost:${server.identity.primaryPort}`;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const AUTH_URI = `http://${HOST}/forbidden`;
+
+ // Try a simple load:
+ await checkDialog(
+ "https://example.com/",
+ browser => BrowserTestUtils.loadURIString(browser, AUTH_URI),
+ HOST,
+ "chrome://global/skin/icons/defaultFavicon.svg",
+ Ci.nsIPrompt.MODAL_TYPE_TAB
+ );
+
+ let subframeLoad = function (browser) {
+ return SpecialPowers.spawn(browser, [AUTH_URI], uri => {
+ let f = content.document.createElement("iframe");
+ f.src = uri;
+ content.document.body.appendChild(f);
+ });
+ };
+
+ // Try x-origin subframe:
+ await checkDialog(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/1",
+ subframeLoad,
+ HOST,
+ /* Because this is x-origin, we expect a different icon: */
+ "chrome://global/skin/icons/security-broken.svg",
+ Ci.nsIPrompt.MODAL_TYPE_TAB
+ );
+});
diff --git a/browser/base/content/test/tabPrompts/browser_multiplePrompts.js b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
new file mode 100644
index 0000000000..597b7dfd6f
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
@@ -0,0 +1,171 @@
+"use strict";
+
+const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+/**
+ * Goes through a stacked series of dialogs opened with
+ * CONTENT_PROMPT_SUBDIALOG set to true, and ensures that
+ * the oldest one is front-most and has the right type. It
+ * then closes the oldest to newest dialog.
+ *
+ * @param {Element} tab The <tab> that has had content dialogs opened
+ * for it.
+ * @param {Number} promptCount How many dialogs we expected to have been
+ * opened.
+ *
+ * @return {Promise}
+ * @resolves {undefined} Once the dialogs have all been closed.
+ */
+async function closeDialogs(tab, dialogCount) {
+ let dialogElementsCount = dialogCount;
+ let dialogs =
+ tab.linkedBrowser.tabDialogBox.getContentDialogManager().dialogs;
+
+ is(
+ dialogs.length,
+ dialogElementsCount,
+ "There should be " + dialogElementsCount + " dialog(s)."
+ );
+
+ let i = dialogElementsCount - 1;
+ for (let dialog of dialogs) {
+ dialog.focus(true);
+ await dialog._dialogReady;
+
+ let dialogWindow = dialog.frameContentWindow;
+ let expectedType = ["alert", "prompt", "confirm"][i % 3];
+
+ is(
+ dialogWindow.Dialog.args.text,
+ expectedType + " countdown #" + i,
+ "The #" + i + " alert should be labelled as such."
+ );
+ i--;
+
+ dialogWindow.Dialog.ui.button0.click();
+
+ // The click is handled async; wait for an event loop turn for that to
+ // happen.
+ await new Promise(function (resolve) {
+ Services.tm.dispatchToMainThread(resolve);
+ });
+ }
+
+ dialogs = tab.linkedBrowser.tabDialogBox.getContentDialogManager().dialogs;
+ is(dialogs.length, 0, "Dialogs should all be dismissed.");
+}
+
+/**
+ * Goes through a stacked series of tabprompt modals opened with
+ * CONTENT_PROMPT_SUBDIALOG set to false, and ensures that
+ * the oldest one is front-most and has the right type. It also
+ * ensures that the other tabprompt modals are hidden. It
+ * then closes the oldest to newest dialog.
+ *
+ * @param {Element} tab The <tab> that has had tabprompt modals opened
+ * for it.
+ * @param {Number} promptCount How many modals we expected to have been
+ * opened.
+ *
+ * @return {Promise}
+ * @resolves {undefined} Once the modals have all been closed.
+ */
+async function closeTabModals(tab, promptCount) {
+ let promptElementsCount = promptCount;
+ while (promptElementsCount--) {
+ let promptElements =
+ tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+ is(
+ promptElements.length,
+ promptElementsCount + 1,
+ "There should be " + (promptElementsCount + 1) + " prompt(s)."
+ );
+ // The oldest should be the first.
+ let i = 0;
+
+ for (let promptElement of promptElements) {
+ let prompt = tab.linkedBrowser.tabModalPromptBox.getPrompt(promptElement);
+ let expectedType = ["alert", "prompt", "confirm"][i % 3];
+ is(
+ prompt.Dialog.args.text,
+ expectedType + " countdown #" + i,
+ "The #" + i + " alert should be labelled as such."
+ );
+ if (i !== promptElementsCount) {
+ is(prompt.element.hidden, true, "This prompt should be hidden.");
+ i++;
+ continue;
+ }
+
+ is(prompt.element.hidden, false, "The last prompt should not be hidden.");
+ prompt.onButtonClick(0);
+
+ // The click is handled async; wait for an event loop turn for that to
+ // happen.
+ await new Promise(function (resolve) {
+ Services.tm.dispatchToMainThread(resolve);
+ });
+ }
+ }
+
+ let promptElements =
+ tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+ is(promptElements.length, 0, "Prompts should all be dismissed.");
+}
+
+/*
+ * This test triggers multiple alerts on one single tab, because it"s possible
+ * for web content to do so. The behavior is described in bug 1266353.
+ *
+ * We assert the presentation of the multiple alerts, ensuring we show only
+ * the oldest one.
+ */
+add_task(async function () {
+ const PROMPTCOUNT = 9;
+
+ let unopenedPromptCount = PROMPTCOUNT;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true
+ );
+ info("Tab loaded");
+
+ let promptsOpenedPromise = BrowserTestUtils.waitForEvent(
+ tab.linkedBrowser,
+ "DOMWillOpenModalDialog",
+ false,
+ () => {
+ unopenedPromptCount--;
+ return unopenedPromptCount == 0;
+ }
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [PROMPTCOUNT], maxPrompts => {
+ var i = maxPrompts;
+ let fns = ["alert", "prompt", "confirm"];
+ function openDialog() {
+ i--;
+ if (i) {
+ SpecialPowers.Services.tm.dispatchToMainThread(openDialog);
+ }
+ content[fns[i % 3]](fns[i % 3] + " countdown #" + i);
+ }
+ SpecialPowers.Services.tm.dispatchToMainThread(openDialog);
+ });
+
+ await promptsOpenedPromise;
+
+ if (CONTENT_PROMPT_SUBDIALOG) {
+ await closeDialogs(tab, PROMPTCOUNT);
+ } else {
+ await closeTabModals(tab, PROMPTCOUNT);
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
new file mode 100644
index 0000000000..d95faa9665
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
@@ -0,0 +1,262 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+);
+let pageWithAlert = ROOT + "openPromptOffTimeout.html";
+
+registerCleanupFunction(function () {
+ Services.perms.removeAll();
+});
+
+/*
+ * This test opens a tab that alerts when it is hidden. We then switch away
+ * from the tab, and check that by default the tab is not automatically
+ * re-selected. We also check that a checkbox appears in the alert that allows
+ * the user to enable this automatically re-selecting. We then check that
+ * checking the checkbox does actually enable that behaviour.
+ */
+add_task(async function test_old_modal_ui() {
+ // We're intentionally testing the old modal mechanism, so disable the new one.
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ let firstTab = gBrowser.selectedTab;
+ // load page that opens prompt when page is hidden
+ let openedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ pageWithAlert,
+ true
+ );
+ let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute(
+ "attention",
+ openedTab
+ );
+ // switch away from that tab again - this triggers the alert.
+ await BrowserTestUtils.switchTab(gBrowser, firstTab);
+ // ... but that's async on e10s...
+ await openedTabGotAttentionPromise;
+ // check for attention attribute
+ is(
+ openedTab.hasAttribute("attention"),
+ true,
+ "Tab with alert should have 'attention' attribute."
+ );
+ ok(!openedTab.selected, "Tab with alert should not be selected");
+
+ // switch tab back, and check the checkbox is displayed:
+ await BrowserTestUtils.switchTab(gBrowser, openedTab);
+ // check the prompt is there, and the extra row is present
+ let promptElements =
+ openedTab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+ is(promptElements.length, 1, "There should be 1 prompt");
+ let ourPromptElement = promptElements[0];
+ let checkbox = ourPromptElement.querySelector(
+ "checkbox[label*='example.com']"
+ );
+ ok(checkbox, "The checkbox should be there");
+ ok(!checkbox.checked, "Checkbox shouldn't be checked");
+ // tick box and accept dialog
+ checkbox.checked = true;
+ let ourPrompt =
+ openedTab.linkedBrowser.tabModalPromptBox.getPrompt(ourPromptElement);
+ ourPrompt.onButtonClick(0);
+ // Wait for that click to actually be handled completely.
+ await new Promise(function (resolve) {
+ Services.tm.dispatchToMainThread(resolve);
+ });
+ // check permission is set
+ is(
+ Services.perms.ALLOW_ACTION,
+ PermissionTestUtils.testPermission(pageWithAlert, "focus-tab-by-prompt"),
+ "Tab switching should now be allowed"
+ );
+
+ // Check if the control center shows the correct permission.
+ let shown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gPermissionPanel._permissionPopup
+ );
+ gPermissionPanel._identityPermissionBox.click();
+ await shown;
+ let labelText = SitePermissions.getPermissionLabel("focus-tab-by-prompt");
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let label = permissionsList.querySelector(
+ ".permission-popup-permission-label"
+ );
+ is(label.textContent, labelText);
+ gPermissionPanel._permissionPopup.hidePopup();
+
+ // Check if the identity icon signals granted permission.
+ ok(
+ gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-box signals granted permissions"
+ );
+
+ let openedTabSelectedPromise = BrowserTestUtils.waitForAttribute(
+ "selected",
+ openedTab,
+ "true"
+ );
+ // switch to other tab again
+ await BrowserTestUtils.switchTab(gBrowser, firstTab);
+
+ // This is sync in non-e10s, but in e10s we need to wait for this, so yield anyway.
+ // Note that the switchTab promise doesn't actually guarantee anything about *which*
+ // tab ends up as selected when its event fires, so using that here wouldn't work.
+ await openedTabSelectedPromise;
+ // should be switched back
+ ok(openedTab.selected, "Ta-dah, the other tab should now be selected again!");
+
+ // In e10s, with the conformant promise scheduling, we have to wait for next tick
+ // to ensure that the prompt is open before removing the opened tab, because the
+ // promise callback of 'openedTabSelectedPromise' could be done at the middle of
+ // RemotePrompt.openTabPrompt() while 'DOMModalDialogClosed' event is fired.
+ await TestUtils.waitForTick();
+
+ BrowserTestUtils.removeTab(openedTab);
+});
+
+add_task(async function test_new_modal_ui() {
+ // We're intentionally testing the new modal mechanism, so make sure it's enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", true]],
+ });
+
+ // Make sure we clear the focus tab permission set in the previous test
+ PermissionTestUtils.remove(pageWithAlert, "focus-tab-by-prompt");
+
+ let firstTab = gBrowser.selectedTab;
+ // load page that opens prompt when page is hidden
+ let openedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ pageWithAlert,
+ true
+ );
+ let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute(
+ "attention",
+ openedTab
+ );
+ // switch away from that tab again - this triggers the alert.
+ await BrowserTestUtils.switchTab(gBrowser, firstTab);
+ // ... but that's async on e10s...
+ await openedTabGotAttentionPromise;
+ // check for attention attribute
+ is(
+ openedTab.hasAttribute("attention"),
+ true,
+ "Tab with alert should have 'attention' attribute."
+ );
+ ok(!openedTab.selected, "Tab with alert should not be selected");
+
+ // switch tab back, and check the checkbox is displayed:
+ await BrowserTestUtils.switchTab(gBrowser, openedTab);
+ // check the prompt is there
+ let promptElements = openedTab.linkedBrowser.parentNode.querySelectorAll(
+ ".content-prompt-dialog"
+ );
+
+ let dialogBox = gBrowser.getTabDialogBox(openedTab.linkedBrowser);
+ let contentPromptManager = dialogBox.getContentDialogManager();
+ is(promptElements.length, 1, "There should be 1 prompt");
+ is(
+ contentPromptManager._dialogs.length,
+ 1,
+ "Content prompt manager should have 1 dialog box."
+ );
+
+ // make sure the checkbox appears and that the permission for allowing tab switching
+ // is set when the checkbox is tickted and the dialog is accepted
+ let dialog = contentPromptManager._dialogs[0];
+
+ await dialog._dialogReady;
+
+ let dialogDoc = dialog._frame.contentWindow.document;
+ let checkbox = dialogDoc.querySelector("checkbox[label*='example.com']");
+ let button = dialogDoc.querySelector("#commonDialog").getButton("accept");
+
+ ok(checkbox, "The checkbox should be there");
+ ok(!checkbox.checked, "Checkbox shouldn't be checked");
+
+ // tick box and accept dialog
+ checkbox.checked = true;
+ button.click();
+ // Wait for that click to actually be handled completely.
+ await new Promise(function (resolve) {
+ Services.tm.dispatchToMainThread(resolve);
+ });
+
+ ok(!contentPromptManager._dialogs.length, "Dialog should be closed");
+
+ // check permission is set
+ is(
+ Services.perms.ALLOW_ACTION,
+ PermissionTestUtils.testPermission(pageWithAlert, "focus-tab-by-prompt"),
+ "Tab switching should now be allowed"
+ );
+
+ // Check if the control center shows the correct permission.
+ let shown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gPermissionPanel._permissionPopup
+ );
+ gPermissionPanel._identityPermissionBox.click();
+ await shown;
+ let labelText = SitePermissions.getPermissionLabel("focus-tab-by-prompt");
+ let permissionsList = document.getElementById(
+ "permission-popup-permission-list"
+ );
+ let label = permissionsList.querySelector(
+ ".permission-popup-permission-label"
+ );
+ is(label.textContent, labelText);
+ gPermissionPanel.hidePopup();
+
+ // Check if the identity icon signals granted permission.
+ ok(
+ gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
+ "identity-permission-box signals granted permissions"
+ );
+
+ let openedTabSelectedPromise = BrowserTestUtils.waitForAttribute(
+ "selected",
+ openedTab,
+ "true"
+ );
+
+ // switch to other tab again
+ await BrowserTestUtils.switchTab(gBrowser, firstTab);
+
+ // This is sync in non-e10s, but in e10s we need to wait for this, so yield anyway.
+ // Note that the switchTab promise doesn't actually guarantee anything about *which*
+ // tab ends up as selected when its event fires, so using that here wouldn't work.
+ await openedTabSelectedPromise;
+
+ ok(contentPromptManager._dialogs.length === 1, "Dialog opened.");
+ dialog = contentPromptManager._dialogs[0];
+ await dialog._dialogReady;
+
+ // should be switched back
+ ok(openedTab.selected, "Ta-dah, the other tab should now be selected again!");
+
+ // In e10s, with the conformant promise scheduling, we have to wait for next tick
+ // to ensure that the prompt is open before removing the opened tab, because the
+ // promise callback of 'openedTabSelectedPromise' could be done at the middle of
+ // RemotePrompt.openTabPrompt() while 'DOMModalDialogClosed' event is fired.
+ // await TestUtils.waitForTick();
+
+ await BrowserTestUtils.removeTab(openedTab);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_promptFocus.js b/browser/base/content/test/tabPrompts/browser_promptFocus.js
new file mode 100644
index 0000000000..89ca064c10
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_promptFocus.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+// MacOS has different default focus behavior for prompts.
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+/**
+ * Tests that prompts are focused when switching tabs.
+ */
+add_task(async function test_tabdialogbox_tab_switch_focus() {
+ // Open 3 tabs
+ let tabPromises = [];
+ for (let i = 0; i < 3; i += 1) {
+ tabPromises.push(
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true
+ )
+ );
+ }
+ // Wait for tabs to be ready
+ let tabs = await Promise.all(tabPromises);
+ let [tabA, tabB, tabC] = tabs;
+
+ // Spawn two prompts, which have different default focus as determined by
+ // CommonDialog#setDefaultFocus.
+ let openPromise = PromptTestUtils.waitForPrompt(tabA.linkedBrowser, {
+ modalType: Services.prompt.MODAL_TYPE_TAB,
+ promptType: "confirm",
+ });
+ Services.prompt.asyncConfirm(
+ tabA.linkedBrowser.browsingContext,
+ Services.prompt.MODAL_TYPE_TAB,
+ null,
+ "prompt A"
+ );
+ let promptA = await openPromise;
+
+ openPromise = PromptTestUtils.waitForPrompt(tabB.linkedBrowser, {
+ modalType: Services.prompt.MODAL_TYPE_TAB,
+ promptType: "promptPassword",
+ });
+ Services.prompt.asyncPromptPassword(
+ tabB.linkedBrowser.browsingContext,
+ Services.prompt.MODAL_TYPE_TAB,
+ null,
+ "prompt B",
+ "",
+ null,
+ false
+ );
+ let promptB = await openPromise;
+
+ // Switch tabs and check if the correct element was focused.
+
+ // Switch back to the third tab which doesn't have a prompt.
+ await BrowserTestUtils.switchTab(gBrowser, tabC);
+ is(
+ Services.focus.focusedElement,
+ tabC.linkedBrowser,
+ "Tab without prompt should have focus on browser."
+ );
+
+ // Switch to first tab which has prompt
+ await BrowserTestUtils.switchTab(gBrowser, tabA);
+
+ if (isMacOS) {
+ is(
+ Services.focus.focusedElement,
+ promptA.ui.infoBody,
+ "Tab with prompt should have focus on body."
+ );
+ } else {
+ is(
+ Services.focus.focusedElement,
+ promptA.ui.button0,
+ "Tab with prompt should have focus on default button."
+ );
+ }
+
+ await PromptTestUtils.handlePrompt(promptA);
+
+ // Switch to second tab which has prompt
+ await BrowserTestUtils.switchTab(gBrowser, tabB);
+ is(
+ Services.focus.focusedElement,
+ promptB.ui.password1Textbox,
+ "Tab with password prompt should have focus on password field."
+ );
+ await PromptTestUtils.handlePrompt(promptB);
+
+ // Cleanup
+ tabs.forEach(tab => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+/**
+ * Tests that an alert prompt has focus on the default element.
+ * @param {CommonDialog} prompt - Prompt to test focus for.
+ * @param {number} index - Index of the prompt to log.
+ */
+function testAlertPromptFocus(prompt, index) {
+ if (isMacOS) {
+ is(
+ Services.focus.focusedElement,
+ prompt.ui.infoBody,
+ `Prompt #${index} should have focus on body.`
+ );
+ } else {
+ is(
+ Services.focus.focusedElement,
+ prompt.ui.button0,
+ `Prompt #${index} should have focus on default button.`
+ );
+ }
+}
+
+/**
+ * Test that we set the correct focus when queuing multiple prompts.
+ */
+add_task(async function test_tabdialogbox_prompt_queue_focus() {
+ await BrowserTestUtils.withNewTab(gBrowser, async browser => {
+ const PROMPT_COUNT = 10;
+
+ let firstPromptPromise = PromptTestUtils.waitForPrompt(browser, {
+ modalType: Services.prompt.MODAL_TYPE_TAB,
+ promptType: "alert",
+ });
+
+ for (let i = 0; i < PROMPT_COUNT; i += 1) {
+ Services.prompt.asyncAlert(
+ browser.browsingContext,
+ Services.prompt.MODAL_TYPE_TAB,
+ null,
+ "prompt " + i
+ );
+ }
+
+ // Close prompts one by one and check focus.
+ let nextPromptPromise = firstPromptPromise;
+ for (let i = 0; i < PROMPT_COUNT; i += 1) {
+ let p = await nextPromptPromise;
+ testAlertPromptFocus(p, i);
+
+ if (i < PROMPT_COUNT - 1) {
+ nextPromptPromise = PromptTestUtils.waitForPrompt(browser, {
+ modalType: Services.prompt.MODAL_TYPE_TAB,
+ promptType: "alert",
+ });
+ }
+ await PromptTestUtils.handlePrompt(p);
+ }
+
+ // All prompts are closed, focus should be back on the browser.
+ is(
+ Services.focus.focusedElement,
+ browser,
+ "Tab without prompts should have focus on browser."
+ );
+ });
+});
diff --git a/browser/base/content/test/tabPrompts/browser_prompt_closed_window.js b/browser/base/content/test/tabPrompts/browser_prompt_closed_window.js
new file mode 100644
index 0000000000..4db3286691
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_prompt_closed_window.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that if we loop prompts from a closed tab, they don't
+ * start showing up as window prompts.
+ */
+add_task(async function test_closed_tab_doesnt_show_prompt() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Get a promise for the initial, in-tab prompt:
+ let promptPromise = BrowserTestUtils.promiseAlertDialogOpen();
+ await ContentTask.spawn(newWin.gBrowser.selectedBrowser, [], function () {
+ // Don't want to block, so use setTimeout with 0 timeout:
+ content.setTimeout(
+ () =>
+ content.eval(
+ 'let i = 0; while (!prompt("Prompts a lot!") && i++ < 10);'
+ ),
+ 0
+ );
+ });
+ // wait for the first prompt to have appeared:
+ await promptPromise;
+
+ // Now close the containing tab, and check for windowed prompts appearing.
+ let opened = false;
+ let obs = () => {
+ opened = true;
+ };
+ Services.obs.addObserver(obs, "domwindowopened");
+ registerCleanupFunction(() =>
+ Services.obs.removeObserver(obs, "domwindowopened")
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+
+ ok(!opened, "Should not have opened a prompt when closing the main window.");
+});
diff --git a/browser/base/content/test/tabPrompts/browser_switchTabPermissionPrompt.js b/browser/base/content/test/tabPrompts/browser_switchTabPermissionPrompt.js
new file mode 100644
index 0000000000..e803869b92
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_switchTabPermissionPrompt.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_check_file_prompt() {
+ let initialTab = gBrowser.selectedTab;
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await BrowserTestUtils.switchTab(gBrowser, initialTab);
+
+ let testHelper = async function (uri, expectedValue) {
+ BrowserTestUtils.loadURIString(browser, uri);
+ await BrowserTestUtils.browserLoaded(browser, false, uri);
+ let dialogFinishedShowing = TestUtils.topicObserved(
+ "common-dialog-loaded"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.setTimeout(() => {
+ content.alert("Hello");
+ }, 0);
+ });
+
+ let [dialogWin] = await dialogFinishedShowing;
+ let checkbox = dialogWin.document.getElementById("checkbox");
+ info("Got: " + checkbox.label);
+ ok(
+ checkbox.label.includes(expectedValue),
+ `Checkbox label should mention domain (${expectedValue}).`
+ );
+
+ dialogWin.document.querySelector("dialog").acceptDialog();
+ };
+
+ await testHelper("https://example.com/1", "example.com");
+ await testHelper("about:robots", "about:");
+ let file = Services.io.newFileURI(
+ Services.dirsvc.get("Desk", Ci.nsIFile)
+ ).spec;
+ await testHelper(file, "file://");
+ });
+});
diff --git a/browser/base/content/test/tabPrompts/browser_windowPrompt.js b/browser/base/content/test/tabPrompts/browser_windowPrompt.js
new file mode 100644
index 0000000000..535142f485
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_windowPrompt.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the in-window modal dialogs work correctly.
+ */
+add_task(async function test_check_window_modal_prompt_service() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.windowPromptSubDialog", true]],
+ });
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+ // Avoid blocking the test on the (sync) alert by sticking it in a timeout:
+ setTimeout(
+ () => Services.prompt.alert(window, "Some title", "some message"),
+ 0
+ );
+ let dialogWin = await dialogPromise;
+
+ // Check dialog content:
+ is(
+ dialogWin.document.getElementById("infoTitle").textContent,
+ "Some title",
+ "Title should be correct."
+ );
+ ok(
+ !dialogWin.document.getElementById("infoTitle").hidden,
+ "Title should be shown."
+ );
+ is(
+ dialogWin.document.getElementById("infoBody").textContent,
+ "some message",
+ "Body text should be correct."
+ );
+
+ // Check circumstances of opening.
+ ok(
+ dialogWin?.docShell?.chromeEventHandler,
+ "Should have embedded the dialog."
+ );
+ for (let menu of document.querySelectorAll("menubar > menu")) {
+ ok(menu.disabled, `Menu ${menu.id} should be disabled.`);
+ }
+
+ let container = dialogWin.docShell.chromeEventHandler.closest("dialog");
+ let closedPromise = BrowserTestUtils.waitForMutationCondition(
+ container,
+ { childList: true, attributes: true },
+ () => !container.hasChildNodes() && !container.open
+ );
+
+ EventUtils.sendKey("ESCAPE");
+ await closedPromise;
+
+ await BrowserTestUtils.waitForMutationCondition(
+ document.querySelector("menubar > menu"),
+ { attributes: true },
+ () => !document.querySelector("menubar > menu").disabled
+ );
+
+ // Check we cleaned up:
+ for (let menu of document.querySelectorAll("menubar > menu")) {
+ ok(!menu.disabled, `Menu ${menu.id} should not be disabled anymore.`);
+ }
+});
+
+/**
+ * Check that the dialog's own closing methods being invoked don't break things.
+ */
+add_task(async function test_check_window_modal_prompt_service() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.windowPromptSubDialog", true]],
+ });
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+ // Avoid blocking the test on the (sync) alert by sticking it in a timeout:
+ setTimeout(
+ () => Services.prompt.alert(window, "Some title", "some message"),
+ 0
+ );
+ let dialogWin = await dialogPromise;
+
+ // Check circumstances of opening.
+ ok(
+ dialogWin?.docShell?.chromeEventHandler,
+ "Should have embedded the dialog."
+ );
+
+ let container = dialogWin.docShell.chromeEventHandler.closest("dialog");
+ let closedPromise = BrowserTestUtils.waitForMutationCondition(
+ container,
+ { childList: true, attributes: true },
+ () => !container.hasChildNodes() && !container.open
+ );
+
+ // This can also be invoked by the user if the escape key is handled
+ // outside of our embedded dialog.
+ container.close();
+ await closedPromise;
+
+ // Check we cleaned up:
+ for (let menu of document.querySelectorAll("menubar > menu")) {
+ ok(!menu.disabled, `Menu ${menu.id} should not be disabled anymore.`);
+ }
+});
+
+add_task(async function test_check_multiple_prompts() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.windowPromptSubDialog", true]],
+ });
+ let container = document.getElementById("window-modal-dialog");
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+
+ let firstDialogClosedPromise = new Promise(resolve => {
+ // Avoid blocking the test on the (sync) alert by sticking it in a timeout:
+ setTimeout(() => {
+ Services.prompt.alert(window, "Some title", "some message");
+ resolve();
+ }, 0);
+ });
+ let dialogWin = await dialogPromise;
+
+ // Check circumstances of opening.
+ ok(
+ dialogWin?.docShell?.chromeEventHandler,
+ "Should have embedded the dialog."
+ );
+ is(container.childElementCount, 1, "Should only have 1 dialog in the DOM.");
+
+ let secondDialogClosedPromise = new Promise(resolve => {
+ // Avoid blocking the test on the (sync) alert by sticking it in a timeout:
+ setTimeout(() => {
+ Services.prompt.alert(window, "Another title", "another message");
+ resolve();
+ }, 0);
+ });
+
+ dialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+
+ info("Accepting dialog");
+ dialogWin.document.querySelector("dialog").acceptDialog();
+
+ let oldWin = dialogWin;
+
+ info("Second dialog should automatically come up.");
+ dialogWin = await dialogPromise;
+
+ isnot(oldWin, dialogWin, "Opened a new dialog.");
+ ok(container.open, "Dialog should be open.");
+
+ info("Now close the second dialog.");
+ dialogWin.document.querySelector("dialog").acceptDialog();
+
+ await firstDialogClosedPromise;
+ await secondDialogClosedPromise;
+
+ await BrowserTestUtils.waitForMutationCondition(
+ container,
+ { childList: true, attributes: true },
+ () => !container.hasChildNodes() && !container.open
+ );
+ // Check we cleaned up:
+ for (let menu of document.querySelectorAll("menubar > menu")) {
+ ok(!menu.disabled, `Menu ${menu.id} should not be disabled anymore.`);
+ }
+});
+
+/**
+ * Check that the in-window modal dialogs un-minimizes windows when necessary.
+ */
+add_task(async function test_check_minimize_response() {
+ // Window minimization doesn't necessarily work on Linux...
+ if (AppConstants.platform == "linux") {
+ return;
+ }
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.windowPromptSubDialog", true]],
+ });
+
+ let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ window.minimize();
+ await promiseSizeModeChange;
+ is(window.windowState, window.STATE_MINIMIZED, "Should be minimized.");
+
+ promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+ // Use an async alert to avoid blocking.
+ Services.prompt.asyncAlert(
+ window.browsingContext,
+ Ci.nsIPrompt.MODAL_TYPE_INTERNAL_WINDOW,
+ "Some title",
+ "some message"
+ );
+ let dialogWin = await dialogPromise;
+ await promiseSizeModeChange;
+
+ isnot(
+ window.windowState,
+ window.STATE_MINIMIZED,
+ "Should no longer be minimized."
+ );
+
+ // Check dialog content:
+ is(
+ dialogWin.document.getElementById("infoTitle").textContent,
+ "Some title",
+ "Title should be correct."
+ );
+
+ let container = dialogWin.docShell.chromeEventHandler.closest("dialog");
+ let closedPromise = BrowserTestUtils.waitForMutationCondition(
+ container,
+ { childList: true, attributes: true },
+ () => !container.hasChildNodes() && !container.open
+ );
+
+ EventUtils.sendKey("ESCAPE");
+ await closedPromise;
+
+ await BrowserTestUtils.waitForMutationCondition(
+ document.querySelector("menubar > menu"),
+ { attributes: true },
+ () => !document.querySelector("menubar > menu").disabled
+ );
+});
+
+/**
+ * Tests that we get a closed callback even when closing the prompt before the
+ * underlying SubDialog has fully opened.
+ */
+add_task(async function test_closed_callback() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.windowPromptSubDialog", true]],
+ });
+
+ let promptClosedPromise = Services.prompt.asyncAlert(
+ window.browsingContext,
+ Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
+ "Hello",
+ "Hello, World!"
+ );
+
+ let dialog = gDialogBox._dialog;
+ ok(dialog, "gDialogBox should have a dialog");
+
+ // Directly close the dialog without waiting for it to initialize.
+ dialog.close();
+
+ info("Waiting for prompt close");
+ await promptClosedPromise;
+
+ ok(!gDialogBox._dialog, "gDialogBox should no longer have a dialog");
+});
diff --git a/browser/base/content/test/tabPrompts/file_beforeunload_stop.html b/browser/base/content/test/tabPrompts/file_beforeunload_stop.html
new file mode 100644
index 0000000000..7273e60c65
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/file_beforeunload_stop.html
@@ -0,0 +1,8 @@
+<body>
+ <p>I will ask not to be closed.</p>
+ <script>
+ window.onbeforeunload = function() {
+ return "true";
+ };
+ </script>
+</body>
diff --git a/browser/base/content/test/tabPrompts/openPromptOffTimeout.html b/browser/base/content/test/tabPrompts/openPromptOffTimeout.html
new file mode 100644
index 0000000000..5dfd8cbeff
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/openPromptOffTimeout.html
@@ -0,0 +1,10 @@
+<body>
+This page opens an alert box when the page is hidden.
+<script>
+document.addEventListener("visibilitychange", () => {
+ if (document.hidden) {
+ alert("You hid my page!");
+ }
+});
+</script>
+</body>
diff --git a/browser/base/content/test/tabPrompts/redirect-crossDomain-tabTitle-update.html b/browser/base/content/test/tabPrompts/redirect-crossDomain-tabTitle-update.html
new file mode 100644
index 0000000000..773b3e47d9
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/redirect-crossDomain-tabTitle-update.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>example.com</title>
+ </head>
+ <body>
+ I am a friendly test page!
+ <script>
+ document.title="tab title update 1";
+ window.location.href="https://example.org:443/browser/browser/base/content/test/tabPrompts/auth-route.sjs";
+ document.title ="tab title update 2";
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabPrompts/redirect-crossDomain.html b/browser/base/content/test/tabPrompts/redirect-crossDomain.html
new file mode 100644
index 0000000000..ebae2c060a
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/redirect-crossDomain.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>example.com</title>
+ </head>
+ <body>
+ I am a friendly test page!
+ <script>
+ window.location.href="https://example.org:443/browser/browser/base/content/test/tabPrompts/auth-route.sjs";
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabPrompts/redirect-sameDomain.html b/browser/base/content/test/tabPrompts/redirect-sameDomain.html
new file mode 100644
index 0000000000..2e50689d1e
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/redirect-sameDomain.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>example.com</title>
+ </head>
+ <body>
+ I am a friendly test page!
+ <script>
+ window.location.href="https://test1.example.com:443/browser/browser/base/content/test/tabPrompts/auth-route.sjs";
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabcrashed/browser.ini b/browser/base/content/test/tabcrashed/browser.ini
new file mode 100644
index 0000000000..7aee7126b8
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+skip-if = !crashreporter
+support-files =
+ head.js
+ file_contains_emptyiframe.html
+ file_iframe.html
+
+[browser_autoSubmitRequest.js]
+[browser_launchFail.js]
+[browser_multipleCrashedTabs.js]
+https_first_disabled = true
+[browser_noPermanentKey.js]
+skip-if = true # Bug 1383315
+[browser_printpreview_crash.js]
+https_first_disabled = true
+[browser_showForm.js]
+[browser_shown.js]
+skip-if =
+ (verify && !debug && (os == 'win'))
+[browser_shownRestartRequired.js]
+[browser_withoutDump.js]
diff --git a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.ini b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.ini
new file mode 100644
index 0000000000..86c442469f
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+skip-if =
+ !debug || !crashreporter
+support-files =
+ head.js
+prefs =
+ dom.ipc.processCount=1
+ dom.ipc.processPrelaunch.fission.number=0
+
+[browser_aboutRestartRequired_basic.js]
+[browser_aboutRestartRequired_buildid_false-positive.js]
+skip-if =
+ win11_2009 && msix && debug # bug 1823581
+[browser_aboutRestartRequired_buildid_mismatch.js]
+skip-if =
+ win11_2009 && msix && debug # bug 1823581
+[browser_aboutRestartRequired_buildid_no-platform-ini.js]
+skip-if =
+ win11_2009 && msix && debug # bug 1823581
diff --git a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_basic.js b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_basic.js
new file mode 100644
index 0000000000..d62372cbba
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_basic.js
@@ -0,0 +1,31 @@
+"use strict";
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(5);
+
+SimpleTest.expectChildProcessCrash();
+
+add_task(async function test_browser_crashed_basic_event() {
+ info("Waiting for oop-browser-crashed event.");
+
+ Services.telemetry.clearScalars();
+ is(
+ getFalsePositiveTelemetry(),
+ undefined,
+ "Build ID mismatch false positive count should be undefined"
+ );
+
+ await forceCleanProcesses();
+ let eventPromise = getEventPromise("oop-browser-crashed", "basic");
+ let tab = await openNewTab(true);
+ await eventPromise;
+
+ is(
+ getFalsePositiveTelemetry(),
+ undefined,
+ "Build ID mismatch false positive count should be undefined"
+ );
+ await closeTab(tab);
+});
diff --git a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_false-positive.js b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_false-positive.js
new file mode 100644
index 0000000000..15e0b5ab31
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_false-positive.js
@@ -0,0 +1,35 @@
+"use strict";
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(2);
+
+SimpleTest.expectChildProcessCrash();
+
+add_task(async function test_browser_crashed_false_positive_event() {
+ info("Waiting for oop-browser-crashed event.");
+
+ Services.telemetry.clearScalars();
+ is(
+ getFalsePositiveTelemetry(),
+ undefined,
+ "Build ID mismatch false positive count should be undefined"
+ );
+
+ ok(await ensureBuildID(), "System has correct platform.ini");
+ setBuildidMatchDontSendEnv();
+ await forceCleanProcesses();
+ let eventPromise = getEventPromise("oop-browser-crashed", "false-positive");
+ let tab = await openNewTab(false);
+ await eventPromise;
+ unsetBuildidMatchDontSendEnv();
+
+ is(
+ getFalsePositiveTelemetry(),
+ 1,
+ "Build ID mismatch false positive count should be 1"
+ );
+
+ await closeTab(tab);
+});
diff --git a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_mismatch.js b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_mismatch.js
new file mode 100644
index 0000000000..80f35db159
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_mismatch.js
@@ -0,0 +1,56 @@
+"use strict";
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(2);
+
+SimpleTest.expectChildProcessCrash();
+
+add_task(async function test_browser_restartrequired_event() {
+ info("Waiting for oop-browser-buildid-mismatch event.");
+
+ Services.telemetry.clearScalars();
+ is(
+ getFalsePositiveTelemetry(),
+ undefined,
+ "Build ID mismatch false positive count should be undefined"
+ );
+
+ ok(await ensureBuildID(), "System has correct platform.ini");
+
+ let profD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let platformIniOrig = await IOUtils.readUTF8(
+ PathUtils.join(profD.path, "platform.ini")
+ );
+ let buildID = Services.appinfo.platformBuildID;
+ let platformIniNew = platformIniOrig.replace(buildID, "1234");
+
+ await IOUtils.writeUTF8(
+ PathUtils.join(profD.path, "platform.ini"),
+ platformIniNew,
+ { flush: true }
+ );
+
+ setBuildidMatchDontSendEnv();
+ await forceCleanProcesses();
+ let eventPromise = getEventPromise(
+ "oop-browser-buildid-mismatch",
+ "buildid-mismatch"
+ );
+ let tab = await openNewTab(false);
+ await eventPromise;
+ await IOUtils.writeUTF8(
+ PathUtils.join(profD.path, "platform.ini"),
+ platformIniOrig,
+ { flush: true }
+ );
+ unsetBuildidMatchDontSendEnv();
+
+ is(
+ getFalsePositiveTelemetry(),
+ undefined,
+ "Build ID mismatch false positive count should be undefined"
+ );
+ await closeTab(tab);
+});
diff --git a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_no-platform-ini.js b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_no-platform-ini.js
new file mode 100644
index 0000000000..232c79b02e
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_buildid_no-platform-ini.js
@@ -0,0 +1,50 @@
+"use strict";
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(2);
+
+SimpleTest.expectChildProcessCrash();
+
+add_task(async function test_browser_crashed_no_platform_ini_event() {
+ info("Waiting for oop-browser-buildid-mismatch event.");
+
+ Services.telemetry.clearScalars();
+ is(
+ getFalsePositiveTelemetry(),
+ undefined,
+ "Build ID mismatch false positive count should be undefined"
+ );
+
+ ok(await ensureBuildID(), "System has correct platform.ini");
+
+ let profD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let platformIniOrig = await IOUtils.readUTF8(
+ PathUtils.join(profD.path, "platform.ini")
+ );
+
+ await IOUtils.remove(PathUtils.join(profD.path, "platform.ini"));
+
+ setBuildidMatchDontSendEnv();
+ await forceCleanProcesses();
+ let eventPromise = getEventPromise(
+ "oop-browser-buildid-mismatch",
+ "no-platform-ini"
+ );
+ let tab = await openNewTab(false);
+ await eventPromise;
+ await IOUtils.writeUTF8(
+ PathUtils.join(profD.path, "platform.ini"),
+ platformIniOrig,
+ { flush: true }
+ );
+ unsetBuildidMatchDontSendEnv();
+
+ is(
+ getFalsePositiveTelemetry(),
+ undefined,
+ "Build ID mismatch false positive count should be undefined"
+ );
+ await closeTab(tab);
+});
diff --git a/browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js b/browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js
new file mode 100644
index 0000000000..b99e8a10b9
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js
@@ -0,0 +1,183 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const AUTOSUBMIT_PREF = "browser.crashReports.unsubmittedCheck.autoSubmit2";
+
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(2);
+
+/**
+ * Tests that if the user is not configured to autosubmit
+ * backlogged crash reports, that we offer to do that, and
+ * that the user can accept that offer.
+ */
+add_task(async function test_show_form() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[AUTOSUBMIT_PREF, false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ // Make sure we've flushed the browser messages so that
+ // we can restore it.
+ await TabStateFlusher.flush(browser);
+
+ // Now crash the browser.
+ await BrowserTestUtils.crashFrame(browser);
+
+ let doc = browser.contentDocument;
+
+ // Ensure the request is visible. We can safely reach into
+ // the content since about:tabcrashed is an in-process URL.
+ let requestAutoSubmit = doc.getElementById("requestAutoSubmit");
+ Assert.ok(
+ !requestAutoSubmit.hidden,
+ "Request for autosubmission is visible."
+ );
+
+ // Since the pref is set to false, the checkbox should be
+ // unchecked.
+ let autoSubmit = doc.getElementById("autoSubmit");
+ Assert.ok(
+ !autoSubmit.checked,
+ "Checkbox for autosubmission is not checked."
+ );
+
+ // Check the checkbox, and then restore the tab.
+ autoSubmit.checked = true;
+ let restoreButton = doc.getElementById("restoreTab");
+ restoreButton.click();
+
+ await BrowserTestUtils.browserLoaded(browser, false, PAGE);
+
+ // The autosubmission pref should now be set.
+ Assert.ok(
+ Services.prefs.getBoolPref(AUTOSUBMIT_PREF),
+ "Autosubmission pref should have been set."
+ );
+ }
+ );
+});
+
+/**
+ * Tests that if the user is autosubmitting backlogged crash reports
+ * that we don't make the offer again.
+ */
+add_task(async function test_show_form() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[AUTOSUBMIT_PREF, true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ await TabStateFlusher.flush(browser);
+ // Now crash the browser.
+ await BrowserTestUtils.crashFrame(browser);
+
+ let doc = browser.contentDocument;
+
+ // Ensure the request is NOT visible. We can safely reach into
+ // the content since about:tabcrashed is an in-process URL.
+ let requestAutoSubmit = doc.getElementById("requestAutoSubmit");
+ Assert.ok(
+ requestAutoSubmit.hidden,
+ "Request for autosubmission is not visible."
+ );
+
+ // Restore the tab.
+ let restoreButton = doc.getElementById("restoreTab");
+ restoreButton.click();
+
+ await BrowserTestUtils.browserLoaded(browser, false, PAGE);
+
+ // The autosubmission pref should still be set to true.
+ Assert.ok(
+ Services.prefs.getBoolPref(AUTOSUBMIT_PREF),
+ "Autosubmission pref should have been set."
+ );
+ }
+ );
+});
+
+/**
+ * Tests that we properly set the autoSubmit preference if the user is
+ * presented with a tabcrashed page without a crash report.
+ */
+add_task(async function test_no_offer() {
+ // We should default to sending the report.
+ Assert.ok(TabCrashHandler.prefs.getBoolPref("sendReport"));
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[AUTOSUBMIT_PREF, false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ await TabStateFlusher.flush(browser);
+
+ // Make it so that it seems like no dump is available for the next crash.
+ prepareNoDump();
+
+ // Now crash the browser.
+ await BrowserTestUtils.crashFrame(browser);
+
+ let doc = browser.contentDocument;
+
+ // Ensure the request to autosubmit is invisible, since there's no report.
+ let requestRect = doc
+ .getElementById("requestAutoSubmit")
+ .getBoundingClientRect();
+ Assert.equal(
+ 0,
+ requestRect.height,
+ "Request for autosubmission has no height"
+ );
+ Assert.equal(
+ 0,
+ requestRect.width,
+ "Request for autosubmission has no width"
+ );
+
+ // Since the pref is set to false, the checkbox should be
+ // unchecked.
+ let autoSubmit = doc.getElementById("autoSubmit");
+ Assert.ok(
+ !autoSubmit.checked,
+ "Checkbox for autosubmission is not checked."
+ );
+
+ let restoreButton = doc.getElementById("restoreTab");
+ restoreButton.click();
+
+ await BrowserTestUtils.browserLoaded(browser, false, PAGE);
+
+ // The autosubmission pref should now be set.
+ Assert.ok(
+ !Services.prefs.getBoolPref(AUTOSUBMIT_PREF),
+ "Autosubmission pref should not have changed."
+ );
+ }
+ );
+
+ // We should not have changed the default value for sending the report.
+ Assert.ok(TabCrashHandler.prefs.getBoolPref("sendReport"));
+});
diff --git a/browser/base/content/test/tabcrashed/browser_launchFail.js b/browser/base/content/test/tabcrashed/browser_launchFail.js
new file mode 100644
index 0000000000..e89038ac10
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_launchFail.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if the content process fails to launch in the
+ * foreground tab, that we show about:tabcrashed, but do not
+ * attempt to wait for a crash dump for it (which will never come).
+ */
+add_task(async function test_launchfail_foreground() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ let tabcrashed = BrowserTestUtils.waitForEvent(
+ browser,
+ "AboutTabCrashedReady",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.simulateProcessLaunchFail(browser);
+ Assert.equal(
+ 0,
+ TabCrashHandler.queuedCrashedBrowsers,
+ "No crashed browsers should be queued."
+ );
+ await tabcrashed;
+ });
+});
+
+/**
+ * Tests that if the content process fails to launch in a background
+ * tab, that upon choosing that tab, we show about:tabcrashed, but do
+ * not attempt to wait for a crash dump for it (which will never come).
+ */
+add_task(async function test_launchfail_background() {
+ let originalTab = gBrowser.selectedTab;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+
+ let tabcrashed = BrowserTestUtils.waitForEvent(
+ browser,
+ "AboutTabCrashedReady",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.simulateProcessLaunchFail(browser);
+ Assert.equal(
+ 0,
+ TabCrashHandler.queuedCrashedBrowsers,
+ "No crashed browsers should be queued."
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await tabcrashed;
+ });
+});
diff --git a/browser/base/content/test/tabcrashed/browser_multipleCrashedTabs.js b/browser/base/content/test/tabcrashed/browser_multipleCrashedTabs.js
new file mode 100644
index 0000000000..f29b88edb6
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_multipleCrashedTabs.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const PAGE_1 = "http://example.com";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const PAGE_2 = "http://example.org";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const PAGE_3 = "http://example.net";
+
+/**
+ * Checks that a particular about:tabcrashed page has the attribute set to
+ * use the "multiple about:tabcrashed" UI.
+ *
+ * @param browser (<xul:browser>)
+ * The browser to check.
+ * @param expected (Boolean)
+ * True if we expect the "multiple" state to be set.
+ * @returns Promise
+ * @resolves undefined
+ * When the check has completed.
+ */
+async function assertShowingMultipleUI(browser, expected) {
+ let showingMultiple = await SpecialPowers.spawn(browser, [], async () => {
+ return (
+ content.document.getElementById("main").getAttribute("multiple") == "true"
+ );
+ });
+ Assert.equal(showingMultiple, expected, "Got the expected 'multiple' state.");
+}
+
+/**
+ * Takes a Telemetry histogram snapshot and returns the sum of all counts.
+ *
+ * @param snapshot (Object)
+ * The Telemetry histogram snapshot to examine.
+ * @return (int)
+ * The sum of all counts in the snapshot.
+ */
+function snapshotCount(snapshot) {
+ return Object.values(snapshot.values).reduce((a, b) => a + b, 0);
+}
+
+/**
+ * Switches to a tab, crashes it, and waits for about:tabcrashed
+ * to load.
+ *
+ * @param tab (<xul:tab>)
+ * The tab to switch to and crash.
+ * @returns Promise
+ * @resolves undefined
+ * When about:tabcrashed is loaded.
+ */
+async function switchToAndCrashTab(tab) {
+ let browser = tab.linkedBrowser;
+
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ let tabcrashed = BrowserTestUtils.waitForEvent(
+ browser,
+ "AboutTabCrashedReady",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.crashFrame(browser);
+ await tabcrashed;
+}
+
+/**
+ * Tests that the appropriate pieces of UI in the about:tabcrashed pages
+ * are updated to reflect how many other about:tabcrashed pages there
+ * are.
+ */
+add_task(async function test_multiple_tabcrashed_pages() {
+ let histogram = Services.telemetry.getHistogramById(
+ "FX_CONTENT_CRASH_NOT_SUBMITTED"
+ );
+ histogram.clear();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_1);
+ let browser1 = tab1.linkedBrowser;
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_2);
+ let browser2 = tab2.linkedBrowser;
+
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_3);
+ let browser3 = tab3.linkedBrowser;
+
+ await switchToAndCrashTab(tab1);
+ Assert.ok(tab1.hasAttribute("crashed"), "tab1 has crashed");
+ Assert.ok(!tab2.hasAttribute("crashed"), "tab2 has not crashed");
+ Assert.ok(!tab3.hasAttribute("crashed"), "tab3 has not crashed");
+
+ // Should not be showing UI for multiple tabs in tab1.
+ await assertShowingMultipleUI(browser1, false);
+
+ await switchToAndCrashTab(tab2);
+ Assert.ok(tab1.hasAttribute("crashed"), "tab1 is still crashed");
+ Assert.ok(tab2.hasAttribute("crashed"), "tab2 has crashed");
+ Assert.ok(!tab3.hasAttribute("crashed"), "tab3 has not crashed");
+
+ // tab1 and tab2 should now be showing UI for multiple tab crashes.
+ await assertShowingMultipleUI(browser1, true);
+ await assertShowingMultipleUI(browser2, true);
+
+ await switchToAndCrashTab(tab3);
+ Assert.ok(tab1.hasAttribute("crashed"), "tab1 is still crashed");
+ Assert.ok(tab2.hasAttribute("crashed"), "tab2 is still crashed");
+ Assert.ok(tab3.hasAttribute("crashed"), "tab3 has crashed");
+
+ // tab1 and tab2 should now be showing UI for multiple tab crashes.
+ await assertShowingMultipleUI(browser1, true);
+ await assertShowingMultipleUI(browser2, true);
+ await assertShowingMultipleUI(browser3, true);
+
+ BrowserTestUtils.removeTab(tab1);
+ await assertShowingMultipleUI(browser2, true);
+ await assertShowingMultipleUI(browser3, true);
+
+ BrowserTestUtils.removeTab(tab2);
+ await assertShowingMultipleUI(browser3, false);
+
+ BrowserTestUtils.removeTab(tab3);
+
+ // We only record the FX_CONTENT_CRASH_NOT_SUBMITTED probe if there
+ // was a single about:tabcrashed page at unload time, so we expect
+ // only a single entry for the probe for when we removed the last
+ // crashed tab.
+ await BrowserTestUtils.waitForCondition(() => {
+ return snapshotCount(histogram.snapshot()) == 1;
+ }, `Collected value should become 1.`);
+
+ histogram.clear();
+});
diff --git a/browser/base/content/test/tabcrashed/browser_noPermanentKey.js b/browser/base/content/test/tabcrashed/browser_noPermanentKey.js
new file mode 100644
index 0000000000..ee1caa73c0
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_noPermanentKey.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+add_setup(async function () {
+ await setupLocalCrashReportServer();
+});
+
+/**
+ * Tests tab crash page when a browser that somehow doesn't have a permanentKey
+ * crashes.
+ */
+add_task(async function test_without_dump() {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ delete browser.permanentKey;
+
+ await BrowserTestUtils.crashFrame(browser);
+ let crashReport = promiseCrashReport();
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ Assert.ok(
+ doc.documentElement.classList.contains("crashDumpAvailable"),
+ "Should be offering to submit a crash report."
+ );
+ // With the permanentKey gone, restoring this tab is no longer
+ // possible. We'll just close it instead.
+ let closeTab = doc.getElementById("closeTab");
+ closeTab.click();
+ });
+
+ await crashReport;
+ }
+ );
+});
diff --git a/browser/base/content/test/tabcrashed/browser_printpreview_crash.js b/browser/base/content/test/tabcrashed/browser_printpreview_crash.js
new file mode 100644
index 0000000000..3ceb4fbe17
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_printpreview_crash.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/tabcrashed/file_contains_emptyiframe.html";
+const DOMAIN = "example.com";
+
+/**
+ * This is really a crashtest, but because we need PrintUtils this is written as a browser test.
+ * Test that when we don't crash when trying to print a document in the following scenario -
+ * A top level document has an iframe of different origin embedded (here example.com has test1.example.com iframe embedded)
+ * and they both set their document.domain to be "example.com".
+ */
+add_task(async function test() {
+ // 1. Open a new tab and wait for it to load the top level doc
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ let browser = newTab.linkedBrowser;
+
+ // 2. Navigate the iframe within the doc and wait for the load to complete
+ await SpecialPowers.spawn(browser, [], async function () {
+ const iframe = content.document.querySelector("iframe");
+ const loaded = new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ iframe.src =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://test1.example.com/browser/browser/base/content/test/tabcrashed/file_iframe.html";
+ await loaded;
+ });
+
+ // 3. Change the top level document's domain
+ await SpecialPowers.spawn(browser, [DOMAIN], async function (domain) {
+ content.document.domain = domain;
+ });
+
+ // 4. Get the reference to the iframe and change its domain
+ const iframe = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.querySelector("iframe").browsingContext;
+ });
+
+ await SpecialPowers.spawn(iframe, [DOMAIN], domain => {
+ content.document.domain = domain;
+ });
+
+ // 5. Try to print things
+ ok(
+ !document.querySelector(".printPreviewBrowser"),
+ "Should NOT be in print preview mode at the start of this test."
+ );
+
+ // Enter print preview
+ document.getElementById("cmd_print").doCommand();
+ await BrowserTestUtils.waitForCondition(() => {
+ let preview = document.querySelector(".printPreviewBrowser");
+ return preview && BrowserTestUtils.is_visible(preview);
+ });
+
+ let ppBrowser = document.querySelector(
+ ".printPreviewBrowser[previewtype=source]"
+ );
+ ok(ppBrowser, "Print preview browser was created");
+
+ ok(true, "We did not crash.");
+
+ // We haven't crashed! Exit the print preview.
+ gBrowser.getTabDialogBox(gBrowser.selectedBrowser).abortAllDialogs();
+ await BrowserTestUtils.waitForCondition(
+ () => !document.querySelector(".printPreviewBrowser")
+ );
+
+ info("We are not in print preview anymore.");
+
+ BrowserTestUtils.removeTab(newTab);
+});
diff --git a/browser/base/content/test/tabcrashed/browser_showForm.js b/browser/base/content/test/tabcrashed/browser_showForm.js
new file mode 100644
index 0000000000..9594f27f9e
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_showForm.js
@@ -0,0 +1,44 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(2);
+
+/**
+ * Tests that we show the about:tabcrashed additional details form
+ * if the "submit a crash report" checkbox was checked by default.
+ */
+add_task(async function test_show_form() {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ // Flip the pref so that the checkbox should be checked
+ // by default.
+ let pref = TabCrashHandler.prefs.root + "sendReport";
+ await SpecialPowers.pushPrefEnv({
+ set: [[pref, true]],
+ });
+
+ // Now crash the browser.
+ await BrowserTestUtils.crashFrame(browser);
+
+ let doc = browser.contentDocument;
+
+ // Ensure the checkbox is checked. We can safely reach into
+ // the content since about:tabcrashed is an in-process URL.
+ let checkbox = doc.getElementById("sendReport");
+ ok(checkbox.checked, "Send report checkbox is checked.");
+
+ // Ensure the options form is displayed.
+ let options = doc.getElementById("options");
+ ok(!options.hidden, "Showing the crash report options form.");
+ }
+ );
+});
diff --git a/browser/base/content/test/tabcrashed/browser_shown.js b/browser/base/content/test/tabcrashed/browser_shown.js
new file mode 100644
index 0000000000..b84c2c7061
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_shown.js
@@ -0,0 +1,150 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const COMMENTS = "Here's my test comment!";
+
+// Avoid timeouts, as in bug 1325530
+requestLongerTimeout(2);
+
+add_setup(async function () {
+ await setupLocalCrashReportServer();
+});
+
+/**
+ * This function returns a Promise that resolves once the following
+ * actions have taken place:
+ *
+ * 1) A new tab is opened up at PAGE
+ * 2) The tab is crashed
+ * 3) The about:tabcrashed page's fields are set in accordance with
+ * fieldValues
+ * 4) The tab is restored
+ * 5) A crash report is received from the testing server
+ * 6) Any tab crash prefs that were overwritten are reset
+ *
+ * @param fieldValues
+ * An Object describing how to set the about:tabcrashed
+ * fields. The following properties are accepted:
+ *
+ * comments (String)
+ * The comments to put in the comment textarea
+ * includeURL (bool)
+ * The checked value of the "Include URL" checkbox
+ *
+ * If any of these fields are missing, the defaults from
+ * the user preferences are used.
+ * @param expectedExtra
+ * An Object describing the expected values that the submitted
+ * crash report's extra data should contain.
+ * @returns Promise
+ */
+function crashTabTestHelper(fieldValues, expectedExtra) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ let prefs = TabCrashHandler.prefs;
+ let originalSendReport = prefs.getBoolPref("sendReport");
+ let originalIncludeURL = prefs.getBoolPref("includeURL");
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ await BrowserTestUtils.crashFrame(browser);
+ let doc = browser.contentDocument;
+
+ // Since about:tabcrashed will run in the parent process, we can safely
+ // manipulate its DOM nodes directly
+ let comments = doc.getElementById("comments");
+ let includeURL = doc.getElementById("includeURL");
+
+ if (fieldValues.hasOwnProperty("comments")) {
+ comments.value = fieldValues.comments;
+ }
+
+ if (fieldValues.hasOwnProperty("includeURL")) {
+ includeURL.checked = fieldValues.includeURL;
+ }
+
+ let crashReport = promiseCrashReport(expectedExtra);
+ let restoreTab = browser.contentDocument.getElementById("restoreTab");
+ restoreTab.click();
+ await BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
+ await crashReport;
+
+ // Submitting the crash report may have set some prefs regarding how to
+ // send tab crash reports. Let's reset them for the next test.
+ prefs.setBoolPref("sendReport", originalSendReport);
+ prefs.setBoolPref("includeURL", originalIncludeURL);
+ }
+ );
+}
+
+/**
+ * Tests what we send with the crash report by default. By default, we do not
+ * send any comments or the URL of the crashing page.
+ */
+add_task(async function test_default() {
+ await crashTabTestHelper(
+ {},
+ {
+ SubmittedFrom: "CrashedTab",
+ Throttleable: "1",
+ Comments: null,
+ URL: "",
+ }
+ );
+});
+
+/**
+ * Test just sending a comment.
+ */
+add_task(async function test_just_a_comment() {
+ await crashTabTestHelper(
+ {
+ SubmittedFrom: "CrashedTab",
+ Throttleable: "1",
+ comments: COMMENTS,
+ },
+ {
+ Comments: COMMENTS,
+ URL: "",
+ }
+ );
+});
+
+/**
+ * Test that we will send the URL of the page if includeURL is checked.
+ */
+add_task(async function test_send_URL() {
+ await crashTabTestHelper(
+ {
+ SubmittedFrom: "CrashedTab",
+ Throttleable: "1",
+ includeURL: true,
+ },
+ {
+ Comments: null,
+ URL: PAGE,
+ }
+ );
+});
+
+/**
+ * Test that we can send comments and the URL
+ */
+add_task(async function test_send_all() {
+ await crashTabTestHelper(
+ {
+ SubmittedFrom: "CrashedTab",
+ Throttleable: "1",
+ includeURL: true,
+ comments: COMMENTS,
+ },
+ {
+ Comments: COMMENTS,
+ URL: PAGE,
+ }
+ );
+});
diff --git a/browser/base/content/test/tabcrashed/browser_shownRestartRequired.js b/browser/base/content/test/tabcrashed/browser_shownRestartRequired.js
new file mode 100644
index 0000000000..9142b54a8a
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_shownRestartRequired.js
@@ -0,0 +1,121 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+async function assertIsAtRestartRequiredPage(browser) {
+ let doc = browser.contentDocument;
+
+ // Since about:restartRequired will run in the parent process, we can safely
+ // manipulate its DOM nodes directly
+ let title = doc.getElementById("title");
+ let description = doc.getElementById("errorLongContent");
+ let restartButton = doc.getElementById("restart");
+
+ Assert.ok(title, "Title element exists.");
+ Assert.ok(description, "Description element exists.");
+ Assert.ok(restartButton, "Restart button exists.");
+}
+
+/**
+ * This function returns a Promise that resolves once the following
+ * actions have taken place:
+ *
+ * 1) A new tab is opened up at PAGE
+ * 2) The tab is crashed
+ * 3) The about:restartrequired page is displayed
+ *
+ * @returns Promise
+ */
+function crashTabTestHelper() {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ // Simulate buildID mismatch.
+ TabCrashHandler.testBuildIDMismatch = true;
+
+ let restartRequiredLoaded = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutRestartRequiredLoad",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.crashFrame(browser, false);
+ await restartRequiredLoaded;
+ await assertIsAtRestartRequiredPage(browser);
+
+ // Reset
+ TabCrashHandler.testBuildIDMismatch = false;
+ }
+ );
+}
+
+/**
+ * Tests that the about:restartrequired page appears when buildID mismatches
+ * between parent and child processes are encountered.
+ */
+add_task(async function test_default() {
+ await crashTabTestHelper();
+});
+
+/**
+ * Tests that if the content process fails to launch in the
+ * foreground tab, that we show the restart required page, but do not
+ * attempt to wait for a crash dump for it (which will never come).
+ */
+add_task(async function test_restart_required_foreground() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ await BrowserTestUtils.simulateProcessLaunchFail(
+ browser,
+ true /* restart required */
+ );
+ Assert.equal(
+ 0,
+ TabCrashHandler.queuedCrashedBrowsers,
+ "No crashed browsers should be queued."
+ );
+ await loaded;
+ await assertIsAtRestartRequiredPage(browser);
+ });
+});
+
+/**
+ * Tests that if the content process fails to launch in a background
+ * tab because a restart is required, that upon choosing that tab, we
+ * show the restart required error page, but do not attempt to wait for
+ * a crash dump for it (which will never come).
+ */
+add_task(async function test_launchfail_background() {
+ let originalTab = gBrowser.selectedTab;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+ await BrowserTestUtils.simulateProcessLaunchFail(
+ browser,
+ true /* restart required */
+ );
+ Assert.equal(
+ 0,
+ TabCrashHandler.queuedCrashedBrowsers,
+ "No crashed browsers should be queued."
+ );
+ let loaded = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutRestartRequiredLoad",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await loaded;
+
+ await assertIsAtRestartRequiredPage(browser);
+ });
+});
diff --git a/browser/base/content/test/tabcrashed/browser_withoutDump.js b/browser/base/content/test/tabcrashed/browser_withoutDump.js
new file mode 100644
index 0000000000..4439f83078
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_withoutDump.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+add_setup(async function () {
+ prepareNoDump();
+});
+
+/**
+ * Tests tab crash page when a dump is not available.
+ */
+add_task(async function test_without_dump() {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await BrowserTestUtils.crashFrame(browser);
+
+ let tabClosingPromise = BrowserTestUtils.waitForTabClosing(tab);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ Assert.ok(
+ !doc.documentElement.classList.contains("crashDumpAvailable"),
+ "doesn't have crash dump"
+ );
+
+ let options = doc.getElementById("options");
+ Assert.ok(options, "has crash report options");
+ Assert.ok(options.hidden, "crash report options are hidden");
+
+ doc.getElementById("closeTab").click();
+ });
+
+ await tabClosingPromise;
+ }
+ );
+});
diff --git a/browser/base/content/test/tabcrashed/file_contains_emptyiframe.html b/browser/base/content/test/tabcrashed/file_contains_emptyiframe.html
new file mode 100644
index 0000000000..5c9a339e68
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/file_contains_emptyiframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<iframe></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/tabcrashed/file_iframe.html b/browser/base/content/test/tabcrashed/file_iframe.html
new file mode 100644
index 0000000000..13f0b53574
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/file_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+Iframe body
+</body>
+</html>
diff --git a/browser/base/content/test/tabcrashed/head.js b/browser/base/content/test/tabcrashed/head.js
new file mode 100644
index 0000000000..bc6185a283
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/head.js
@@ -0,0 +1,238 @@
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+/**
+ * Returns a Promise that resolves once a crash report has
+ * been submitted. This function will also test the crash
+ * reports extra data to see if it matches expectedExtra.
+ *
+ * @param expectedExtra (object)
+ * An Object whose key-value pairs will be compared
+ * against the key-value pairs in the extra data of the
+ * crash report. A test failure will occur if there is
+ * a mismatch.
+ *
+ * If the value of the key-value pair is "null", this will
+ * be interpreted as "this key should not be included in the
+ * extra data", and will cause a test failure if it is detected
+ * in the crash report.
+ *
+ * Note that this will ignore any keys that are not included
+ * in expectedExtra. It's possible that the crash report
+ * will contain other extra information that is not
+ * compared against.
+ * @returns Promise
+ */
+function promiseCrashReport(expectedExtra = {}) {
+ return (async function () {
+ info("Starting wait on crash-report-status");
+ let [subject] = await TestUtils.topicObserved(
+ "crash-report-status",
+ (unused, data) => {
+ return data == "success";
+ }
+ );
+ info("Topic observed!");
+
+ if (!(subject instanceof Ci.nsIPropertyBag2)) {
+ throw new Error("Subject was not a Ci.nsIPropertyBag2");
+ }
+
+ let remoteID = getPropertyBagValue(subject, "serverCrashID");
+ if (!remoteID) {
+ throw new Error("Report should have a server ID");
+ }
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(Services.crashmanager._submittedDumpsDir);
+ file.append(remoteID + ".txt");
+ if (!file.exists()) {
+ throw new Error("Report should have been received by the server");
+ }
+
+ file.remove(false);
+
+ let extra = getPropertyBagValue(subject, "extra");
+ if (!(extra instanceof Ci.nsIPropertyBag2)) {
+ throw new Error("extra was not a Ci.nsIPropertyBag2");
+ }
+
+ info("Iterating crash report extra keys");
+ for (let { name: key } of extra.enumerator) {
+ let value = extra.getPropertyAsAString(key);
+ if (key in expectedExtra) {
+ if (expectedExtra[key] == null) {
+ ok(false, `Got unexpected key ${key} with value ${value}`);
+ } else {
+ is(
+ value,
+ expectedExtra[key],
+ `Crash report had the right extra value for ${key}`
+ );
+ }
+ }
+ }
+ })();
+}
+
+/**
+ * For an nsIPropertyBag, returns the value for a given
+ * key.
+ *
+ * @param bag
+ * The nsIPropertyBag to retrieve the value from
+ * @param key
+ * The key that we want to get the value for from the
+ * bag
+ * @returns The value corresponding to the key from the bag,
+ * or null if the value could not be retrieved (for
+ * example, if no value is set at that key).
+ */
+function getPropertyBagValue(bag, key) {
+ try {
+ let val = bag.getProperty(key);
+ return val;
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Sets up the browser to send crash reports to the local crash report
+ * testing server.
+ */
+async function setupLocalCrashReportServer() {
+ const SERVER_URL =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
+
+ // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables crash
+ // reports. This test needs them enabled. The test also needs a mock
+ // report server, and fortunately one is already set up by toolkit/
+ // crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL,
+ // which CrashSubmit.jsm uses as a server override.
+ let noReport = Services.env.get("MOZ_CRASHREPORTER_NO_REPORT");
+ let serverUrl = Services.env.get("MOZ_CRASHREPORTER_URL");
+ Services.env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
+ Services.env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
+
+ registerCleanupFunction(function () {
+ Services.env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
+ Services.env.set("MOZ_CRASHREPORTER_URL", serverUrl);
+ });
+}
+
+/**
+ * Monkey patches TabCrashHandler.getDumpID to return null in order to test
+ * about:tabcrashed when a dump is not available.
+ */
+function prepareNoDump() {
+ let originalGetDumpID = TabCrashHandler.getDumpID;
+ TabCrashHandler.getDumpID = function (browser) {
+ return null;
+ };
+ registerCleanupFunction(() => {
+ TabCrashHandler.getDumpID = originalGetDumpID;
+ });
+}
+
+const kBuildidMatchEnv = "MOZ_BUILDID_MATCH_DONTSEND";
+
+function setBuildidMatchDontSendEnv() {
+ info("Setting " + kBuildidMatchEnv + "=1");
+ Services.env.set(kBuildidMatchEnv, "1");
+}
+
+function unsetBuildidMatchDontSendEnv() {
+ info("Setting " + kBuildidMatchEnv + "=0");
+ Services.env.set(kBuildidMatchEnv, "0");
+}
+
+function getEventPromise(eventName, eventKind) {
+ return new Promise(function (resolve, reject) {
+ info("Installing event listener (" + eventKind + ")");
+ window.addEventListener(
+ eventName,
+ event => {
+ ok(true, "Received " + eventName + " (" + eventKind + ") event");
+ info("Call resolve() for " + eventKind + " event");
+ resolve();
+ },
+ { once: true }
+ );
+ info("Installed event listener (" + eventKind + ")");
+ });
+}
+
+async function ensureBuildID() {
+ let profD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let platformIniOrig = await IOUtils.readUTF8(
+ PathUtils.join(profD.path, "platform.ini")
+ );
+ let buildID = Services.appinfo.platformBuildID;
+ return platformIniOrig.indexOf(buildID) > 0;
+}
+
+async function openNewTab(forceCrash) {
+ const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+ let options = {
+ gBrowser,
+ PAGE,
+ waitForLoad: false,
+ waitForStateStop: false,
+ forceNewProcess: true,
+ };
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(options);
+ if (forceCrash === true) {
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.crashFrame(
+ browser,
+ /* shouldShowTabCrashPage */ false,
+ /* shouldClearMinidumps */ true,
+ /* BrowsingContext */ null
+ );
+ }
+
+ return tab;
+}
+
+async function closeTab(tab) {
+ await TestUtils.waitForTick();
+ BrowserTestUtils.removeTab(tab);
+}
+
+function getFalsePositiveTelemetry() {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ return scalars["dom.contentprocess.buildID_mismatch_false_positive"];
+}
+
+// The logic bound to dom.ipc.processPrelaunch.enabled will react to value
+// changes: https://searchfox.org/mozilla-central/rev/ecd91b104714a8b2584a4c03175be50ccb3a7c67/dom/ipc/PreallocatedProcessManager.cpp#171-195
+// So we force flip to ensure we have no dangling process.
+async function forceCleanProcesses() {
+ const origPrefValue = SpecialPowers.getBoolPref(
+ "dom.ipc.processPrelaunch.enabled"
+ );
+ await SpecialPowers.setBoolPref(
+ "dom.ipc.processPrelaunch.enabled",
+ !origPrefValue
+ );
+ await SpecialPowers.setBoolPref(
+ "dom.ipc.processPrelaunch.enabled",
+ origPrefValue
+ );
+ const currPrefValue = SpecialPowers.getBoolPref(
+ "dom.ipc.processPrelaunch.enabled"
+ );
+ ok(currPrefValue === origPrefValue, "processPrelaunch properly re-enabled");
+}
diff --git a/browser/base/content/test/tabdialogs/browser.ini b/browser/base/content/test/tabdialogs/browser.ini
new file mode 100644
index 0000000000..2fbfa48b37
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files =
+ subdialog.xhtml
+
+[browser_multiple_dialog_navigation.js]
+[browser_subdialog_esc.js]
+support-files =
+ loadDelayedReply.sjs
+[browser_tabdialogbox_content_prompts.js]
+skip-if =
+ apple_silicon && !debug # Bug 1786514
+ apple_catalina && !debug # Bug 1786514
+ win10_2004 && !debug # Bug 1786514
+support-files =
+ test_page.html
+[browser_tabdialogbox_focus.js]
+https_first_disabled = true
+[browser_tabdialogbox_navigation.js]
+https_first_disabled = true
diff --git a/browser/base/content/test/tabdialogs/browser_multiple_dialog_navigation.js b/browser/base/content/test/tabdialogs/browser_multiple_dialog_navigation.js
new file mode 100644
index 0000000000..9d66ac1d7e
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_multiple_dialog_navigation.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_multiple_dialog_navigation() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/gone",
+ async browser => {
+ let firstDialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+ // We're gonna queue up some dialogs, and navigate. The tasks queueing the dialog
+ // are going to get aborted when the navigation happened, but that's OK because by
+ // that time they will have done their job. Detect and swallow that specific
+ // exception:
+ let navigationCatcher = e => {
+ if (e.name == "AbortError" && e.message.includes("destroyed before")) {
+ return;
+ }
+ throw e;
+ };
+ // Queue up 2 dialogs
+ let firstTask = SpecialPowers.spawn(browser, [], async function () {
+ content.eval(`alert('hi');`);
+ }).catch(navigationCatcher);
+ let secondTask = SpecialPowers.spawn(browser, [], async function () {
+ content.eval(`alert('hi again');`);
+ }).catch(navigationCatcher);
+ info("Waiting for first dialog.");
+ let dialogWin = await firstDialogPromise;
+
+ let secondDialogPromise = BrowserTestUtils.promiseAlertDialogOpen();
+ dialogWin.document
+ .getElementById("commonDialog")
+ .getButton("accept")
+ .click();
+ dialogWin = null;
+
+ info("Wait for second dialog to appear.");
+ let secondDialogWin = await secondDialogPromise;
+ let closedPromise = BrowserTestUtils.waitForEvent(
+ secondDialogWin,
+ "unload"
+ );
+ let loadedOtherPage = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ "https://example.org/gone"
+ );
+ BrowserTestUtils.loadURIString(browser, "https://example.org/gone");
+ info("Waiting for the next page to load.");
+ await loadedOtherPage;
+ info(
+ "Waiting for second dialog to close. If we time out here that's a bug!"
+ );
+ await closedPromise;
+ is(secondDialogWin.closed, true, "Should have closed second dialog.");
+ info("Ensure content tasks are done");
+ await secondTask;
+ await firstTask;
+ }
+ );
+});
diff --git a/browser/base/content/test/tabdialogs/browser_subdialog_esc.js b/browser/base/content/test/tabdialogs/browser_subdialog_esc.js
new file mode 100644
index 0000000000..63f2f276a3
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_subdialog_esc.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT_CHROME = getRootDirectory(gTestPath);
+const TEST_DIALOG_PATH = TEST_ROOT_CHROME + "subdialog.xhtml";
+
+const WEB_ROOT = TEST_ROOT_CHROME.replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+const TEST_LOAD_PAGE = WEB_ROOT + "loadDelayedReply.sjs";
+
+/**
+ * Tests that ESC on a SubDialog does not cancel ongoing loads in the parent.
+ */
+add_task(async function test_subdialog_esc_does_not_cancel_load() {
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ async function (browser) {
+ // Start loading a page
+ let loadStartedPromise = BrowserTestUtils.loadURIString(
+ browser,
+ TEST_LOAD_PAGE
+ );
+ let loadedPromise = BrowserTestUtils.browserLoaded(browser);
+ await loadStartedPromise;
+
+ // Open a dialog
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let dialogClose = dialogBox.open(TEST_DIALOG_PATH, {
+ keepOpenSameOriginNav: true,
+ }).closedPromise;
+
+ let dialogs = dialogBox.getTabDialogManager()._dialogs;
+
+ is(dialogs.length, 1, "Dialog manager has a dialog.");
+
+ info("Waiting for dialogs to open.");
+ await dialogs[0]._dialogReady;
+
+ // Close the dialog with esc key
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Waiting for dialog to close.");
+ await dialogClose;
+
+ info("Triggering load complete");
+ fetch(TEST_LOAD_PAGE, {
+ method: "POST",
+ });
+
+ // Load must complete
+ info("Waiting for load to complete");
+ await loadedPromise;
+ ok(true, "Load completed");
+ }
+ );
+});
+
+/**
+ * Tests that ESC on a SubDialog with an open dropdown doesn't close the dialog.
+ */
+add_task(async function test_subdialog_esc_on_dropdown_does_not_close_dialog() {
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ async function (browser) {
+ // Open the test dialog
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let dialogClose = dialogBox.open(TEST_DIALOG_PATH, {
+ keepOpenSameOriginNav: true,
+ }).closedPromise;
+
+ let dialogs = dialogBox.getTabDialogManager()._dialogs;
+
+ is(dialogs.length, 1, "Dialog manager has a dialog.");
+
+ let dialog = dialogs[0];
+
+ info("Waiting for dialog to open.");
+ await dialog._dialogReady;
+
+ // Open dropdown
+ let select = dialog._frame.contentDocument.getElementById("select");
+ let shownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+
+ info("Opening dropdown");
+ select.focus();
+ EventUtils.synthesizeKey("VK_SPACE", {}, dialog._frame.contentWindow);
+
+ let selectPopup = await shownPromise;
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphiding",
+ true
+ );
+
+ // Race dropdown closing vs SubDialog close
+ let race = Promise.race([
+ hiddenPromise.then(() => true),
+ dialogClose.then(() => false),
+ ]);
+
+ // Close the dropdown with esc key
+ info("Hitting escape key.");
+ await EventUtils.synthesizeKey("KEY_Escape");
+
+ let result = await race;
+ ok(result, "Select closed first");
+
+ await new Promise(resolve => executeSoon(resolve));
+
+ ok(!dialog._isClosing, "Dialog is not closing");
+ ok(dialog._openedURL, "Dialog is open");
+ }
+ );
+});
diff --git a/browser/base/content/test/tabdialogs/browser_tabdialogbox_content_prompts.js b/browser/base/content/test/tabdialogs/browser_tabdialogbox_content_prompts.js
new file mode 100644
index 0000000000..3067c53873
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_content_prompts.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CONTENT_PROMPT_PREF = "prompts.contentPromptSubDialog";
+const TEST_ROOT_CHROME = getRootDirectory(gTestPath);
+const TEST_DIALOG_PATH = TEST_ROOT_CHROME + "subdialog.xhtml";
+
+const TEST_DATA_URI = "data:text/html,<body onload='alert(1)'>";
+const TEST_EXTENSION_DATA = {
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("url", browser.runtime.getURL("alert.html"));
+ },
+ manifest: {
+ name: "Test Extension",
+ },
+ files: {
+ "alert.html": `<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>TabDialogBox Content Modal Test page</title>
+ <script src="./alert.js"></script>
+ </head>
+ <body>
+ <h1>TabDialogBox Content Modal</h1>
+ </body>
+</html>`,
+ "alert.js": `window.addEventListener("load", () => alert("Hi"));`,
+ },
+};
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_ORIGIN = "http://example.com";
+const TEST_PAGE =
+ TEST_ROOT_CHROME.replace("chrome://mochitests/content", TEST_ORIGIN) +
+ "test_page.html";
+
+var commonDialogsBundle = Services.strings.createBundle(
+ "chrome://global/locale/commonDialogs.properties"
+);
+
+// Setup.
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [[CONTENT_PROMPT_PREF, true]],
+ });
+});
+
+/**
+ * Test that a manager for content prompts is added to tab dialog box.
+ */
+add_task(async function test_tabdialog_content_prompts() {
+ await BrowserTestUtils.withNewTab(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ async function (browser) {
+ info("Open a tab prompt.");
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ dialogBox.open(TEST_DIALOG_PATH);
+
+ info("Check the content prompt dialog is only created when needed.");
+ let contentPromptDialog = document.querySelector(
+ ".content-prompt-dialog"
+ );
+ ok(!contentPromptDialog, "Content prompt dialog should not be created.");
+
+ info("Open a content prompt");
+ dialogBox.open(TEST_DIALOG_PATH, {
+ modalType: Ci.nsIPrompt.MODAL_TYPE_CONTENT,
+ });
+
+ contentPromptDialog = document.querySelector(".content-prompt-dialog");
+ ok(contentPromptDialog, "Content prompt dialog should be created.");
+ let contentPromptManager = dialogBox.getContentDialogManager();
+
+ is(
+ contentPromptManager._dialogs.length,
+ 1,
+ "Content prompt manager should have 1 dialog box."
+ );
+ }
+ );
+});
+
+/**
+ * Test origin text for a null principal.
+ */
+add_task(async function test_tabdialog_null_principal_title() {
+ let dialogShown = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "DOMWillOpenModalDialog"
+ );
+
+ await BrowserTestUtils.withNewTab(TEST_DATA_URI, async function (browser) {
+ info("Waiting for dialog to open.");
+ await dialogShown;
+ await checkOriginText(browser);
+ });
+});
+
+/**
+ * Test origin text for an extension page.
+ */
+add_task(async function test_tabdialog_extension_title() {
+ let extension = ExtensionTestUtils.loadExtension(TEST_EXTENSION_DATA);
+
+ await extension.startup();
+ let url = await extension.awaitMessage("url");
+ let dialogShown = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "DOMWillOpenModalDialog"
+ );
+
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ info("Waiting for dialog to open.");
+ await dialogShown;
+ await checkOriginText(browser, "Test Extension");
+ });
+
+ await extension.unload();
+});
+
+/**
+ * Test origin text for a regular page.
+ */
+add_task(async function test_tabdialog_page_title() {
+ let dialogShown = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "DOMWillOpenModalDialog"
+ );
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) {
+ info("Waiting for dialog to open.");
+ await dialogShown;
+ await checkOriginText(browser, TEST_ORIGIN);
+ });
+});
+
+/**
+ * Test helper for checking the origin header of a dialog.
+ *
+ * @param {Object} browser
+ * The browser the dialog was opened from.
+ * @param {String|null} origin
+ * The page origin that should be displayed in the header, if any.
+ */
+async function checkOriginText(browser, origin = null) {
+ info("Check the title is visible.");
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let contentPromptManager = dialogBox.getContentDialogManager();
+ let dialog = contentPromptManager._dialogs[0];
+
+ info("Waiting for dialog frame to be ready.");
+ await dialog._dialogReady;
+
+ let dialogDoc = dialog._frame.contentWindow.document;
+ let titleSelector = "#titleText";
+ let infoTitle = dialogDoc.querySelector(titleSelector);
+ ok(BrowserTestUtils.is_visible(infoTitle), "Title text is visible");
+
+ info("Check the displayed origin text is correct.");
+ if (origin) {
+ let host = origin;
+ try {
+ host = new URL(origin).host;
+ } catch (ex) {
+ /* will fail for the extension case. */
+ }
+ is(infoTitle.textContent, host, "Origin should be in header.");
+ } else {
+ is(
+ infoTitle.dataset.l10nId,
+ "common-dialog-title-null",
+ "Null principal string should be in header."
+ );
+ }
+}
diff --git a/browser/base/content/test/tabdialogs/browser_tabdialogbox_focus.js b/browser/base/content/test/tabdialogs/browser_tabdialogbox_focus.js
new file mode 100644
index 0000000000..08c0e8828d
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_focus.js
@@ -0,0 +1,212 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT_CHROME = getRootDirectory(gTestPath);
+const TEST_DIALOG_PATH = TEST_ROOT_CHROME + "subdialog.xhtml";
+
+/**
+ * Tests that tab dialogs are focused when switching tabs.
+ */
+add_task(async function test_tabdialogbox_tab_switch_focus() {
+ // Open 3 tabs
+ let tabPromises = [];
+ for (let i = 0; i < 3; i += 1) {
+ tabPromises.push(
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true
+ )
+ );
+ }
+
+ // Wait for tabs to be ready
+ let tabs = await Promise.all(tabPromises);
+
+ // Open subdialog in first two tabs
+ let dialogs = [];
+ for (let i = 0; i < 2; i += 1) {
+ let dialogBox = gBrowser.getTabDialogBox(tabs[i].linkedBrowser);
+ dialogBox.open(TEST_DIALOG_PATH);
+ dialogs.push(dialogBox.getTabDialogManager()._topDialog);
+ }
+
+ // Wait for dialogs to be ready
+ await Promise.all([dialogs[0]._dialogReady, dialogs[1]._dialogReady]);
+
+ // Switch to first tab which has dialog
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+
+ // The textbox in the dialogs content window should be focused
+ let dialogTextbox =
+ dialogs[0]._frame.contentDocument.querySelector("#textbox");
+ is(Services.focus.focusedElement, dialogTextbox, "Dialog textbox is focused");
+
+ // Switch to second tab which has dialog
+ await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+
+ // The textbox in the dialogs content window should be focused
+ let dialogTextbox2 =
+ dialogs[1]._frame.contentDocument.querySelector("#textbox");
+ is(
+ Services.focus.focusedElement,
+ dialogTextbox2,
+ "Dialog2 textbox is focused"
+ );
+
+ // Switch to third tab which does not have a dialog
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+
+ // Test that content is focused
+ is(
+ Services.focus.focusedElement,
+ tabs[2].linkedBrowser,
+ "Top level browser is focused"
+ );
+
+ // Cleanup
+ tabs.forEach(tab => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+/**
+ * Tests that if we're showing multiple tab dialogs they are focused in the
+ * correct order and custom focus handlers are called.
+ */
+add_task(async function test_tabdialogbox_multiple_focus() {
+ await BrowserTestUtils.withNewTab(gBrowser, async browser => {
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let dialogAClose = dialogBox.open(
+ TEST_DIALOG_PATH,
+ {},
+ {
+ testCustomFocusHandler: true,
+ }
+ ).closedPromise;
+ let dialogBClose = dialogBox.open(TEST_DIALOG_PATH).closedPromise;
+ let dialogCClose = dialogBox.open(
+ TEST_DIALOG_PATH,
+ {},
+ {
+ testCustomFocusHandler: true,
+ }
+ ).closedPromise;
+
+ let dialogs = dialogBox._tabDialogManager._dialogs;
+ let [dialogA, dialogB, dialogC] = dialogs;
+
+ // Wait until all dialogs are ready
+ await Promise.all(dialogs.map(dialog => dialog._dialogReady));
+
+ // Dialog A's custom focus target should be focused
+ let dialogElementA =
+ dialogA._frame.contentDocument.querySelector("#custom-focus-el");
+ is(
+ Services.focus.focusedElement,
+ dialogElementA,
+ "Dialog A custom focus target is focused"
+ );
+
+ // Close top dialog
+ dialogA.close();
+ await dialogAClose;
+
+ // Dialog B's first focus target should be focused
+ let dialogElementB =
+ dialogB._frame.contentDocument.querySelector("#textbox");
+ is(
+ Services.focus.focusedElement,
+ dialogElementB,
+ "Dialog B default focus target is focused"
+ );
+
+ // close top dialog
+ dialogB.close();
+ await dialogBClose;
+
+ // Dialog C's custom focus target should be focused
+ let dialogElementC =
+ dialogC._frame.contentDocument.querySelector("#custom-focus-el");
+ is(
+ Services.focus.focusedElement,
+ dialogElementC,
+ "Dialog C custom focus target is focused"
+ );
+
+ // Close last dialog
+ dialogC.close();
+ await dialogCClose;
+
+ is(
+ dialogBox._tabDialogManager._dialogs.length,
+ 0,
+ "All dialogs should be closed"
+ );
+ is(
+ Services.focus.focusedElement,
+ browser,
+ "Focus should be back on the browser"
+ );
+ });
+});
+
+/**
+ * Tests that other dialogs are still visible if one dialog is hidden.
+ */
+add_task(async function test_tabdialogbox_tab_switch_hidden() {
+ // Open 2 tabs
+ let tabPromises = [];
+ for (let i = 0; i < 2; i += 1) {
+ tabPromises.push(
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com",
+ true
+ )
+ );
+ }
+
+ // Wait for tabs to be ready
+ let tabs = await Promise.all(tabPromises);
+
+ // Open subdialog in tabs
+ let dialogs = [];
+ let dialogBox, dialogBoxManager, browser;
+ for (let i = 0; i < 2; i += 1) {
+ dialogBox = gBrowser.getTabDialogBox(tabs[i].linkedBrowser);
+ browser = tabs[i].linkedBrowser;
+ dialogBox.open(TEST_DIALOG_PATH);
+ dialogBoxManager = dialogBox.getTabDialogManager();
+ dialogs.push(dialogBoxManager._topDialog);
+ }
+
+ // Wait for dialogs to be ready
+ await Promise.all([dialogs[0]._dialogReady, dialogs[1]._dialogReady]);
+
+ // Hide the top dialog
+ dialogBoxManager.hideDialog(browser);
+
+ ok(
+ BrowserTestUtils.is_hidden(dialogBoxManager._dialogStack),
+ "Dialog stack is hidden"
+ );
+
+ // Switch to first tab
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+
+ // Check the dialog stack is showing in first tab
+ dialogBoxManager = gBrowser
+ .getTabDialogBox(tabs[0].linkedBrowser)
+ .getTabDialogManager();
+ is(dialogBoxManager._dialogStack.hidden, false, "Dialog stack is showing");
+
+ // Cleanup
+ tabs.forEach(tab => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
diff --git a/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js b/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js
new file mode 100644
index 0000000000..9e76f37f29
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT_CHROME = getRootDirectory(gTestPath);
+const TEST_DIALOG_PATH = TEST_ROOT_CHROME + "subdialog.xhtml";
+
+/**
+ * Tests that all tab dialogs are closed on navigation.
+ */
+add_task(async function test_tabdialogbox_multiple_close_on_nav() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ // Open two dialogs and wait for them to be ready.
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let closedPromises = [
+ dialogBox.open(TEST_DIALOG_PATH).closedPromise,
+ dialogBox.open(TEST_DIALOG_PATH).closedPromise,
+ ];
+
+ let dialogs = dialogBox.getTabDialogManager()._dialogs;
+
+ is(dialogs.length, 2, "Dialog manager has two dialogs.");
+
+ info("Waiting for dialogs to open.");
+ await Promise.all(dialogs.map(dialog => dialog._dialogReady));
+
+ // Navigate to a different page
+ BrowserTestUtils.loadURIString(browser, "https://example.org");
+
+ info("Waiting for dialogs to close.");
+ await closedPromises;
+
+ ok(true, "All open dialogs should close on navigation");
+ }
+ );
+});
+
+/**
+ * Tests dialog close on navigation triggered by web content.
+ */
+add_task(async function test_tabdialogbox_close_on_content_nav() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ // Open a dialog and wait for it to be ready
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let { closedPromise } = dialogBox.open(TEST_DIALOG_PATH);
+
+ let dialog = dialogBox.getTabDialogManager()._topDialog;
+
+ is(
+ dialogBox.getTabDialogManager()._dialogs.length,
+ 1,
+ "Dialog manager has one dialog."
+ );
+
+ info("Waiting for dialog to open.");
+ await dialog._dialogReady;
+
+ // Trigger a same origin navigation by the content
+ await ContentTask.spawn(browser, {}, () => {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ content.location = "http://example.com/1";
+ });
+
+ info("Waiting for dialog to close.");
+ await closedPromise;
+ ok(
+ true,
+ "Dialog should close for same origin navigation by the content."
+ );
+
+ // Open a new dialog
+ closedPromise = dialogBox.open(TEST_DIALOG_PATH, {
+ keepOpenSameOriginNav: true,
+ }).closedPromise;
+
+ info("Waiting for dialog to open.");
+ await dialog._dialogReady;
+
+ SimpleTest.requestFlakyTimeout("Waiting to ensure dialog does not close");
+ let race = Promise.race([
+ closedPromise,
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ new Promise(resolve => setTimeout(() => resolve("success"), 1000)),
+ ]);
+
+ // Trigger a same origin navigation by the content
+ await ContentTask.spawn(browser, {}, () => {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ content.location = "http://example.com/test";
+ });
+
+ is(
+ await race,
+ "success",
+ "Dialog should not close for same origin navigation by the content."
+ );
+
+ // Trigger a cross origin navigation by the content
+ await ContentTask.spawn(browser, {}, () => {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ content.location = "http://example.org/test2";
+ });
+
+ info("Waiting for dialog to close");
+ await closedPromise;
+
+ ok(
+ true,
+ "Dialog should close for cross origin navigation by the content."
+ );
+ }
+ );
+});
+
+/**
+ * Hides a dialog stack and tests that behavior doesn't change. Ensures
+ * navigation triggered by web content still closes all dialogs.
+ */
+add_task(async function test_tabdialogbox_hide() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ // Open a dialog and wait for it to be ready
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let dialogBoxManager = dialogBox.getTabDialogManager();
+ let closedPromises = [
+ dialogBox.open(TEST_DIALOG_PATH).closedPromise,
+ dialogBox.open(TEST_DIALOG_PATH).closedPromise,
+ ];
+
+ let dialogs = dialogBox.getTabDialogManager()._dialogs;
+
+ is(
+ dialogBox.getTabDialogManager()._dialogs.length,
+ 2,
+ "Dialog manager has two dialogs."
+ );
+
+ info("Waiting for dialogs to open.");
+ await Promise.all(dialogs.map(dialog => dialog._dialogReady));
+
+ ok(
+ !BrowserTestUtils.is_hidden(dialogBoxManager._dialogStack),
+ "Dialog stack is showing"
+ );
+
+ dialogBoxManager.hideDialog(browser);
+
+ is(
+ dialogBoxManager._dialogs.length,
+ 2,
+ "Dialog manager still has two dialogs."
+ );
+
+ ok(
+ BrowserTestUtils.is_hidden(dialogBoxManager._dialogStack),
+ "Dialog stack is hidden"
+ );
+
+ // Navigate to a different page
+ BrowserTestUtils.loadURIString(browser, "https://example.org");
+
+ info("Waiting for dialogs to close.");
+ await closedPromises;
+
+ ok(true, "All open dialogs should still close on navigation");
+ }
+ );
+});
diff --git a/browser/base/content/test/tabdialogs/loadDelayedReply.sjs b/browser/base/content/test/tabdialogs/loadDelayedReply.sjs
new file mode 100644
index 0000000000..cf046967bf
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/loadDelayedReply.sjs
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.processAsync();
+ if (request.method === "POST") {
+ getObjectState("wait", queryResponse => {
+ if (!queryResponse) {
+ throw new Error("Wrong call order");
+ }
+ queryResponse.finish();
+
+ response.setStatusLine(request.httpVersion, 200);
+ response.write("OK");
+ response.finish();
+ });
+ return;
+ }
+ response.setStatusLine(request.httpVersion, 200);
+ response.write("OK");
+ setObjectState("wait", response);
+}
diff --git a/browser/base/content/test/tabdialogs/subdialog.xhtml b/browser/base/content/test/tabdialogs/subdialog.xhtml
new file mode 100644
index 0000000000..03b2b76d49
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/subdialog.xhtml
@@ -0,0 +1,46 @@
+<?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">
+ <script>
+ document.addEventListener("dialogaccept", acceptSubdialog);
+ function acceptSubdialog() {
+ window.arguments[0].acceptCount++;
+ }
+ document.addEventListener("DOMContentLoaded", () => {
+ if (!window.arguments) {
+ return;
+ }
+ let [options] = window.arguments;
+ if (options?.testCustomFocusHandler) {
+ document.subDialogSetDefaultFocus = () => {
+ document.getElementById("custom-focus-el").focus();
+ }
+ }
+ }, {once: true})
+ </script>
+
+ <description id="desc">A sample sub-dialog for testing</description>
+
+ <html:input id="textbox" value="Default text" />
+
+ <html:select id="select">
+ <html:option>Foo</html:option>
+ <html:option>Bar</html:option>
+ </html:select>
+
+ <html:input id="custom-focus-el" value="Custom Focus Test" />
+
+ <separator class="thin"/>
+
+ <button oncommand="window.close();" label="Close" />
+
+</dialog>
+</window>
diff --git a/browser/base/content/test/tabdialogs/test_page.html b/browser/base/content/test/tabdialogs/test_page.html
new file mode 100644
index 0000000000..c5f17062cf
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/test_page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>TabDialogBox Content Modal Test page</title>
+</head>
+<body onload='alert("Hi");'>
+ <h1>TabDialogBox Content Modal</h1>
+</body>
+</html>
diff --git a/browser/base/content/test/tabs/204.sjs b/browser/base/content/test/tabs/204.sjs
new file mode 100644
index 0000000000..22b1d300e3
--- /dev/null
+++ b/browser/base/content/test/tabs/204.sjs
@@ -0,0 +1,3 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 204, "No Content");
+}
diff --git a/browser/base/content/test/tabs/blank.html b/browser/base/content/test/tabs/blank.html
new file mode 100644
index 0000000000..bcc2e389b8
--- /dev/null
+++ b/browser/base/content/test/tabs/blank.html
@@ -0,0 +1,2 @@
+<!doctype html>
+This page intentionally left blank.
diff --git a/browser/base/content/test/tabs/browser.ini b/browser/base/content/test/tabs/browser.ini
new file mode 100644
index 0000000000..68cfe44705
--- /dev/null
+++ b/browser/base/content/test/tabs/browser.ini
@@ -0,0 +1,211 @@
+[DEFAULT]
+support-files =
+ head.js
+ dummy_page.html
+ ../general/audio.ogg
+ file_mediaPlayback.html
+ test_process_flags_chrome.html
+ helper_origin_attrs_testing.js
+ file_about_srcdoc.html
+
+[browser_addAdjacentNewTab.js]
+[browser_addTab_index.js]
+[browser_adoptTab_failure.js]
+[browser_allow_process_switches_despite_related_browser.js]
+[browser_audioTabIcon.js]
+tags = audiochannel
+[browser_bfcache_exemption_about_pages.js]
+skip-if = !fission
+[browser_bug580956.js]
+[browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
+[browser_close_during_beforeunload.js]
+https_first_disabled = true
+[browser_close_tab_by_dblclick.js]
+[browser_contextmenu_openlink_after_tabnavigated.js]
+https_first_disabled = true
+skip-if =
+ verify && debug && os == "linux"
+support-files =
+ test_bug1358314.html
+[browser_dont_process_switch_204.js]
+support-files =
+ blank.html
+ 204.sjs
+[browser_e10s_about_page_triggeringprincipal.js]
+https_first_disabled = true
+skip-if = verify
+support-files =
+ file_about_child.html
+ file_about_parent.html
+[browser_e10s_about_process.js]
+[browser_e10s_chrome_process.js]
+skip-if = debug # Bug 1444565, Bug 1457887
+[browser_e10s_javascript.js]
+[browser_e10s_mozillaweb_process.js]
+[browser_e10s_switchbrowser.js]
+[browser_file_to_http_named_popup.js]
+[browser_file_to_http_script_closable.js]
+support-files = tab_that_closes.html
+[browser_hiddentab_contextmenu.js]
+[browser_lazy_tab_browser_events.js]
+[browser_link_in_tab_title_and_url_prefilled_blank_page.js]
+support-files =
+ common_link_in_tab_title_and_url_prefilled.js
+ link_in_tab_title_and_url_prefilled.html
+ request-timeout.sjs
+ wait-a-bit.sjs
+[browser_link_in_tab_title_and_url_prefilled_new_window.js]
+support-files =
+ common_link_in_tab_title_and_url_prefilled.js
+ link_in_tab_title_and_url_prefilled.html
+ request-timeout.sjs
+ wait-a-bit.sjs
+[browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js]
+support-files =
+ common_link_in_tab_title_and_url_prefilled.js
+ link_in_tab_title_and_url_prefilled.html
+ request-timeout.sjs
+ wait-a-bit.sjs
+[browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js]
+support-files =
+ common_link_in_tab_title_and_url_prefilled.js
+ link_in_tab_title_and_url_prefilled.html
+ request-timeout.sjs
+ wait-a-bit.sjs
+[browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js]
+support-files =
+ common_link_in_tab_title_and_url_prefilled.js
+ link_in_tab_title_and_url_prefilled.html
+ request-timeout.sjs
+ wait-a-bit.sjs
+[browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js]
+support-files =
+ common_link_in_tab_title_and_url_prefilled.js
+ link_in_tab_title_and_url_prefilled.html
+ request-timeout.sjs
+ wait-a-bit.sjs
+[browser_long_data_url_label_truncation.js]
+[browser_middle_click_new_tab_button_loads_clipboard.js]
+[browser_multiselect_tabs_active_tab_selected_by_default.js]
+[browser_multiselect_tabs_bookmark.js]
+[browser_multiselect_tabs_clear_selection_when_tab_switch.js]
+[browser_multiselect_tabs_close.js]
+[browser_multiselect_tabs_close_other_tabs.js]
+[browser_multiselect_tabs_close_tabs_to_the_left.js]
+[browser_multiselect_tabs_close_tabs_to_the_right.js]
+[browser_multiselect_tabs_close_using_shortcuts.js]
+[browser_multiselect_tabs_copy_through_drag_and_drop.js]
+[browser_multiselect_tabs_drag_to_bookmarks_toolbar.js]
+[browser_multiselect_tabs_duplicate.js]
+[browser_multiselect_tabs_event.js]
+[browser_multiselect_tabs_move.js]
+[browser_multiselect_tabs_move_to_another_window_drag.js]
+[browser_multiselect_tabs_move_to_new_window_contextmenu.js]
+https_first_disabled = true
+[browser_multiselect_tabs_mute_unmute.js]
+[browser_multiselect_tabs_open_related.js]
+[browser_multiselect_tabs_pin_unpin.js]
+[browser_multiselect_tabs_play.js]
+[browser_multiselect_tabs_reload.js]
+[browser_multiselect_tabs_reopen_in_container.js]
+[browser_multiselect_tabs_reorder.js]
+[browser_multiselect_tabs_using_Ctrl.js]
+[browser_multiselect_tabs_using_Shift.js]
+[browser_multiselect_tabs_using_Shift_and_Ctrl.js]
+[browser_multiselect_tabs_using_keyboard.js]
+skip-if =
+ os == "mac" # Skipped because macOS keyboard support requires changing system settings
+[browser_multiselect_tabs_using_selectedTabs.js]
+[browser_navigatePinnedTab.js]
+https_first_disabled = true
+[browser_navigate_home_focuses_addressbar.js]
+[browser_navigate_through_urls_origin_attributes.js]
+skip-if =
+ verify && os == "mac"
+[browser_new_file_whitelisted_http_tab.js]
+https_first_disabled = true
+[browser_new_tab_in_privilegedabout_process_pref.js]
+https_first_disabled = true
+skip-if =
+ os == "linux" && debug # Bug 1581500.
+[browser_new_tab_insert_position.js]
+https_first_disabled = true
+support-files = file_new_tab_page.html
+[browser_new_tab_url.js]
+support-files = file_new_tab_page.html
+[browser_newwindow_tabstrip_overflow.js]
+[browser_open_newtab_start_observer_notification.js]
+[browser_opened_file_tab_navigated_to_web.js]
+https_first_disabled = true
+[browser_origin_attrs_in_remote_type.js]
+[browser_origin_attrs_rel.js]
+skip-if =
+ verify && os == "mac"
+support-files = file_rel_opener_noopener.html
+[browser_originalURI.js]
+support-files =
+ page_with_iframe.html
+ redirect_via_header.html
+ redirect_via_header.html^headers^
+ redirect_via_meta_tag.html
+[browser_overflowScroll.js]
+skip-if =
+ win10_2004 # Bug 1775648
+ win11_2009 # Bug 1797751
+[browser_paste_event_at_middle_click_on_link.js]
+support-files = file_anchor_elements.html
+[browser_pinnedTabs.js]
+[browser_pinnedTabs_clickOpen.js]
+[browser_pinnedTabs_closeByKeyboard.js]
+[browser_positional_attributes.js]
+skip-if =
+ verify && os == "win"
+ verify && os == "mac"
+[browser_preloadedBrowser_zoom.js]
+[browser_privilegedmozilla_process_pref.js]
+https_first_disabled = true
+[browser_progress_keyword_search_handling.js]
+https_first_disabled = true
+[browser_relatedTabs_reset.js]
+[browser_reload_deleted_file.js]
+skip-if =
+ debug && os == "mac"
+ debug && os == "linux" #Bug 1421183, disabled on Linux/OSX for leaked windows
+[browser_removeTabsToTheEnd.js]
+[browser_removeTabsToTheStart.js]
+[browser_removeTabs_order.js]
+[browser_removeTabs_skipPermitUnload.js]
+[browser_replacewithwindow_commands.js]
+[browser_switch_by_scrolling.js]
+[browser_tabCloseProbes.js]
+[browser_tabCloseSpacer.js]
+skip-if =
+ os == "linux"
+ os == "win" # Bug 1616418
+ os == "mac" #Bug 1549985
+[browser_tabContextMenu_keyboard.js]
+[browser_tabReorder.js]
+[browser_tabReorder_overflow.js]
+[browser_tabSpinnerProbe.js]
+[browser_tabSuccessors.js]
+[browser_tab_a11y_description.js]
+[browser_tab_label_during_reload.js]
+[browser_tab_label_picture_in_picture.js]
+[browser_tab_manager_close.js]
+[browser_tab_manager_drag.js]
+[browser_tab_manager_keyboard_access.js]
+[browser_tab_manager_visibility.js]
+[browser_tab_move_to_new_window_reload.js]
+[browser_tab_play.js]
+[browser_tab_tooltips.js]
+[browser_tabswitch_contextmenu.js]
+[browser_tabswitch_select.js]
+support-files = open_window_in_new_tab.html
+[browser_tabswitch_updatecommands.js]
+[browser_tabswitch_window_focus.js]
+[browser_undo_close_tabs.js]
+skip-if = true #bug 1642084
+[browser_undo_close_tabs_at_start.js]
+[browser_viewsource_of_data_URI_in_file_process.js]
+[browser_visibleTabs_bookmarkAllTabs.js]
+[browser_visibleTabs_contextMenu.js]
diff --git a/browser/base/content/test/tabs/browser_addAdjacentNewTab.js b/browser/base/content/test/tabs/browser_addAdjacentNewTab.js
new file mode 100644
index 0000000000..c9b4b45ccc
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_addAdjacentNewTab.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // Ensure we can wait for about:newtab to load.
+ set: [["browser.newtab.preload", false]],
+ });
+
+ const tab1 = await addTab();
+ const tab2 = await addTab();
+ const tab3 = await addTab();
+
+ const menuItemOpenANewTab = document.getElementById("context_openANewTab");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+
+ is(tab1._tPos, 1, "First tab");
+ is(tab2._tPos, 2, "Second tab");
+ is(tab3._tPos, 3, "Third tab");
+
+ updateTabContextMenu(tab2);
+ is(menuItemOpenANewTab.hidden, false, "Open a new Tab is visible");
+
+ const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ // Open the tab context menu.
+ const contextMenu = document.getElementById("tabContextMenu");
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+
+ contextMenu.activateItem(menuItemOpenANewTab);
+
+ let newTab = await newTabPromise;
+
+ is(tab1._tPos, 1, "First tab");
+ is(tab2._tPos, 2, "Second tab");
+ is(newTab._tPos, 3, "Third tab");
+ is(tab3._tPos, 4, "Fourth tab");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(newTab);
+});
diff --git a/browser/base/content/test/tabs/browser_addTab_index.js b/browser/base/content/test/tabs/browser_addTab_index.js
new file mode 100644
index 0000000000..abfc0c213e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_addTab_index.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let tab = gBrowser.addTrustedTab("about:blank", { index: 10 });
+ is(tab._tPos, 1, "added tab index should be 1");
+ gBrowser.removeTab(tab);
+}
diff --git a/browser/base/content/test/tabs/browser_adoptTab_failure.js b/browser/base/content/test/tabs/browser_adoptTab_failure.js
new file mode 100644
index 0000000000..f20f4c0c56
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_adoptTab_failure.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// 'adoptTab' aborts when swapBrowsersAndCloseOther returns false.
+// That's usually a bug, but this function forces it to happen in order to check
+// that callers will behave as good as possible when it happens accidentally.
+function makeAdoptTabFailOnceFor(gBrowser, tab) {
+ const original = gBrowser.swapBrowsersAndCloseOther;
+ gBrowser.swapBrowsersAndCloseOther = function (aOurTab, aOtherTab) {
+ if (tab !== aOtherTab) {
+ return original.call(gBrowser, aOurTab, aOtherTab);
+ }
+ gBrowser.swapBrowsersAndCloseOther = original;
+ return false;
+ };
+}
+
+add_task(async function test_adoptTab() {
+ const tab = await addTab();
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const gBrowser2 = win2.gBrowser;
+
+ makeAdoptTabFailOnceFor(gBrowser2, tab);
+ is(gBrowser2.adoptTab(tab), null, "adoptTab returns null in case of failure");
+ ok(gBrowser2.adoptTab(tab), "adoptTab returns new tab in case of success");
+
+ await BrowserTestUtils.closeWindow(win2);
+});
+
+add_task(async function test_replaceTabsWithWindow() {
+ const nonAdoptableTab = await addTab("data:text/plain,nonAdoptableTab");
+ const auxiliaryTab = await addTab("data:text/plain,auxiliaryTab");
+ const selectedTab = await addTab("data:text/plain,selectedTab");
+ gBrowser.selectedTabs = [selectedTab, nonAdoptableTab, auxiliaryTab];
+
+ const windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ const win2 = gBrowser.replaceTabsWithWindow(selectedTab);
+ await BrowserTestUtils.waitForEvent(win2, "DOMContentLoaded");
+ const gBrowser2 = win2.gBrowser;
+ makeAdoptTabFailOnceFor(gBrowser2, nonAdoptableTab);
+ await windowOpenedPromise;
+
+ // nonAdoptableTab couldn't be adopted, but the new window should have adopted
+ // the other 2 tabs, and they should be in the proper order.
+ is(gBrowser2.tabs.length, 2);
+ is(gBrowser2.tabs[0].label, "data:text/plain,auxiliaryTab");
+ is(gBrowser2.tabs[1].label, "data:text/plain,selectedTab");
+
+ gBrowser.removeTab(nonAdoptableTab);
+ await BrowserTestUtils.closeWindow(win2);
+});
+
+add_task(async function test_on_drop() {
+ const nonAdoptableTab = await addTab("data:text/html,<title>nonAdoptableTab");
+ const auxiliaryTab = await addTab("data:text/html,<title>auxiliaryTab");
+ const selectedTab = await addTab("data:text/html,<title>selectedTab");
+ gBrowser.selectedTabs = [selectedTab, nonAdoptableTab, auxiliaryTab];
+
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const gBrowser2 = win2.gBrowser;
+ makeAdoptTabFailOnceFor(gBrowser2, nonAdoptableTab);
+ const initialTab = gBrowser2.tabs[0];
+ await dragAndDrop(selectedTab, initialTab, false, win2, false);
+
+ // nonAdoptableTab couldn't be adopted, but the new window should have adopted
+ // the other 2 tabs, and they should be in the right position.
+ is(gBrowser2.tabs.length, 3, "There are 3 tabs");
+ is(gBrowser2.tabs[0].label, "auxiliaryTab", "auxiliaryTab became tab 0");
+ is(gBrowser2.tabs[1].label, "selectedTab", "selectedTab became tab 1");
+ is(gBrowser2.tabs[2], initialTab, "initialTab became tab 2");
+ is(gBrowser2.selectedTab, gBrowser2.tabs[1], "Tab 1 is selected");
+ is(gBrowser2.multiSelectedTabsCount, 2, "Three multiselected tabs");
+ ok(gBrowser2.tabs[0].multiselected, "Tab 0 is multiselected");
+ ok(gBrowser2.tabs[1].multiselected, "Tab 1 is multiselected");
+ ok(!gBrowser2.tabs[2].multiselected, "Tab 2 is not multiselected");
+
+ gBrowser.removeTab(nonAdoptableTab);
+ await BrowserTestUtils.closeWindow(win2);
+});
+
+add_task(async function test_switchToTabHavingURI() {
+ const nonAdoptableTab = await addTab("data:text/plain,nonAdoptableTab");
+ const uri = nonAdoptableTab.linkedBrowser.currentURI;
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const gBrowser2 = win2.gBrowser;
+
+ is(nonAdoptableTab.closing, false);
+ is(nonAdoptableTab.selected, false);
+ is(gBrowser2.tabs.length, 1);
+
+ makeAdoptTabFailOnceFor(gBrowser2, nonAdoptableTab);
+ win2.switchToTabHavingURI(uri, false, { adoptIntoActiveWindow: true });
+
+ is(nonAdoptableTab.closing, false);
+ is(nonAdoptableTab.selected, true);
+ is(gBrowser2.tabs.length, 1);
+
+ win2.switchToTabHavingURI(uri, false, { adoptIntoActiveWindow: true });
+
+ is(nonAdoptableTab.closing, true);
+ is(nonAdoptableTab.selected, false);
+ is(gBrowser2.tabs.length, 2);
+
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js b/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js
new file mode 100644
index 0000000000..f1b4a98021
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const DUMMY_FILE = "dummy_page.html";
+const DATA_URI = "data:text/html,Hi";
+const DATA_URI_SOURCE = "view-source:" + DATA_URI;
+
+// Test for bug 1328829.
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DATA_URI);
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let promiseTab = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI_SOURCE);
+ BrowserViewSource(tab.linkedBrowser);
+ let viewSourceTab = await promiseTab;
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(viewSourceTab);
+ });
+
+ let dummyPage = getChromeDir(getResolvedURI(gTestPath));
+ dummyPage.append(DUMMY_FILE);
+ dummyPage.normalize();
+ const uriString = Services.io.newFileURI(dummyPage).spec;
+
+ let viewSourceBrowser = viewSourceTab.linkedBrowser;
+ let promiseLoad = BrowserTestUtils.browserLoaded(
+ viewSourceBrowser,
+ false,
+ uriString
+ );
+ BrowserTestUtils.loadURIString(viewSourceBrowser, uriString);
+ let href = await promiseLoad;
+ is(
+ href,
+ uriString,
+ "Check file:// URI loads in a browser that was previously for view-source"
+ );
+});
diff --git a/browser/base/content/test/tabs/browser_audioTabIcon.js b/browser/base/content/test/tabs/browser_audioTabIcon.js
new file mode 100644
index 0000000000..3e3db58f06
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_audioTabIcon.js
@@ -0,0 +1,676 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const PAGE =
+ "https://example.com/browser/browser/base/content/test/tabs/file_mediaPlayback.html";
+const TABATTR_REMOVAL_PREFNAME = "browser.tabs.delayHidingAudioPlayingIconMS";
+const INITIAL_TABATTR_REMOVAL_DELAY_MS = Services.prefs.getIntPref(
+ TABATTR_REMOVAL_PREFNAME
+);
+
+async function pause(tab, options) {
+ let extendedDelay = options && options.extendedDelay;
+ if (extendedDelay) {
+ // Use 10s to remove possibility of race condition with attr removal.
+ Services.prefs.setIntPref(TABATTR_REMOVAL_PREFNAME, 10000);
+ }
+
+ try {
+ let browser = tab.linkedBrowser;
+ let awaitDOMAudioPlaybackStopped;
+ if (!browser.audioMuted) {
+ awaitDOMAudioPlaybackStopped = BrowserTestUtils.waitForEvent(
+ browser,
+ "DOMAudioPlaybackStopped",
+ "DOMAudioPlaybackStopped event should get fired after pause"
+ );
+ }
+ await SpecialPowers.spawn(browser, [], async function () {
+ let audio = content.document.querySelector("audio");
+ audio.pause();
+ });
+
+ // If the tab has already be muted, it means the tab won't have soundplaying,
+ // so we don't need to check this attribute.
+ if (browser.audioMuted) {
+ return;
+ }
+
+ if (extendedDelay) {
+ ok(
+ tab.hasAttribute("soundplaying"),
+ "The tab should still have the soundplaying attribute immediately after pausing"
+ );
+
+ await awaitDOMAudioPlaybackStopped;
+ ok(
+ tab.hasAttribute("soundplaying"),
+ "The tab should still have the soundplaying attribute immediately after DOMAudioPlaybackStopped"
+ );
+ }
+
+ await wait_for_tab_playing_event(tab, false);
+ ok(
+ !tab.hasAttribute("soundplaying"),
+ "The tab should not have the soundplaying attribute after the timeout has resolved"
+ );
+ } finally {
+ // Make sure other tests don't timeout if an exception gets thrown above.
+ // Need to use setIntPref instead of clearUserPref because
+ // testing/profiles/common/user.js overrides the default value to help this and
+ // other tests run faster.
+ Services.prefs.setIntPref(
+ TABATTR_REMOVAL_PREFNAME,
+ INITIAL_TABATTR_REMOVAL_DELAY_MS
+ );
+ }
+}
+
+async function hide_tab(tab) {
+ let tabHidden = BrowserTestUtils.waitForEvent(tab, "TabHide");
+ gBrowser.hideTab(tab);
+ return tabHidden;
+}
+
+async function show_tab(tab) {
+ let tabShown = BrowserTestUtils.waitForEvent(tab, "TabShow");
+ gBrowser.showTab(tab);
+ return tabShown;
+}
+
+async function test_tooltip(icon, expectedTooltip, isActiveTab, tab) {
+ let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+
+ let tabContent = tab.querySelector(".tab-content");
+ await hover_icon(tabContent, tooltip);
+
+ await hover_icon(icon, tooltip);
+ if (isActiveTab) {
+ // The active tab should have the keybinding shortcut in the tooltip.
+ // We check this by ensuring that the strings are not equal but the expected
+ // message appears in the beginning.
+ isnot(
+ tooltip.getAttribute("label"),
+ expectedTooltip,
+ "Tooltips should not be equal"
+ );
+ is(
+ tooltip.getAttribute("label").indexOf(expectedTooltip),
+ 0,
+ "Correct tooltip expected"
+ );
+ } else {
+ is(
+ tooltip.getAttribute("label"),
+ expectedTooltip,
+ "Tooltips should not be equal"
+ );
+ }
+ leave_icon(icon);
+}
+
+function get_tab_state(tab) {
+ return JSON.parse(SessionStore.getTabState(tab));
+}
+
+async function test_muting_using_menu(tab, expectMuted) {
+ // Show the popup menu
+ let contextMenu = document.getElementById("tabContextMenu");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu", button: 2 });
+ await popupShownPromise;
+
+ // Check the menu
+ let expectedLabel = expectMuted ? "Unmute Tab" : "Mute Tab";
+ let expectedAccessKey = expectMuted ? "m" : "M";
+ let toggleMute = document.getElementById("context_toggleMuteTab");
+ is(toggleMute.label, expectedLabel, "Correct label expected");
+ is(toggleMute.accessKey, expectedAccessKey, "Correct accessKey expected");
+
+ is(
+ toggleMute.hasAttribute("muted"),
+ expectMuted,
+ "Should have the correct state for the muted attribute"
+ );
+ ok(
+ !toggleMute.hasAttribute("soundplaying"),
+ "Should not have the soundplaying attribute"
+ );
+
+ await play(tab);
+
+ is(
+ toggleMute.hasAttribute("muted"),
+ expectMuted,
+ "Should have the correct state for the muted attribute"
+ );
+ is(
+ !toggleMute.hasAttribute("soundplaying"),
+ expectMuted,
+ "The value of soundplaying attribute is incorrect"
+ );
+
+ await pause(tab);
+
+ is(
+ toggleMute.hasAttribute("muted"),
+ expectMuted,
+ "Should have the correct state for the muted attribute"
+ );
+ ok(
+ !toggleMute.hasAttribute("soundplaying"),
+ "Should not have the soundplaying attribute"
+ );
+
+ // Click on the menu and wait for the tab to be muted.
+ let mutedPromise = get_wait_for_mute_promise(tab, !expectMuted);
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.activateItem(toggleMute);
+ await popupHiddenPromise;
+ await mutedPromise;
+}
+
+async function test_playing_icon_on_tab(tab, browser, isPinned) {
+ let icon = isPinned ? tab.overlayIcon : tab.overlayIcon;
+ let isActiveTab = tab === gBrowser.selectedTab;
+
+ await play(tab);
+
+ await test_tooltip(icon, "Mute tab", isActiveTab, tab);
+
+ ok(
+ !("muted" in get_tab_state(tab)),
+ "No muted attribute should be persisted"
+ );
+ ok(
+ !("muteReason" in get_tab_state(tab)),
+ "No muteReason property should be persisted"
+ );
+
+ await test_mute_tab(tab, icon, true);
+
+ ok("muted" in get_tab_state(tab), "Muted attribute should be persisted");
+ ok(
+ "muteReason" in get_tab_state(tab),
+ "muteReason property should be persisted"
+ );
+
+ await test_tooltip(icon, "Unmute tab", isActiveTab, tab);
+
+ await test_mute_tab(tab, icon, false);
+
+ ok(
+ !("muted" in get_tab_state(tab)),
+ "No muted attribute should be persisted"
+ );
+ ok(
+ !("muteReason" in get_tab_state(tab)),
+ "No muteReason property should be persisted"
+ );
+
+ await test_tooltip(icon, "Mute tab", isActiveTab, tab);
+
+ await test_mute_tab(tab, icon, true);
+
+ await pause(tab);
+
+ ok(
+ tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"),
+ "Tab should still be muted but not playing"
+ );
+ ok(
+ tab.muted && !tab.soundPlaying,
+ "Tab should still be muted but not playing"
+ );
+
+ await test_tooltip(icon, "Unmute tab", isActiveTab, tab);
+
+ await test_mute_tab(tab, icon, false);
+
+ ok(
+ !tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"),
+ "Tab should not be be muted or playing"
+ );
+ ok(!tab.muted && !tab.soundPlaying, "Tab should not be be muted or playing");
+
+ // Make sure it's possible to mute using the context menu.
+ await test_muting_using_menu(tab, false);
+
+ // Make sure it's possible to unmute using the context menu.
+ await test_muting_using_menu(tab, true);
+}
+
+async function test_playing_icon_on_hidden_tab(tab) {
+ let oldSelectedTab = gBrowser.selectedTab;
+ let otherTabs = [
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
+ ];
+ let tabContainer = tab.container;
+ let alltabsButton = document.getElementById("alltabs-button");
+ let alltabsBadge = alltabsButton.badgeLabel;
+
+ function assertIconShowing() {
+ is(
+ getComputedStyle(alltabsBadge).backgroundImage,
+ 'url("chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg")',
+ "The audio playing icon is shown"
+ );
+ is(
+ tabContainer.getAttribute("hiddensoundplaying"),
+ "true",
+ "There are hidden audio tabs"
+ );
+ }
+
+ function assertIconHidden() {
+ is(
+ getComputedStyle(alltabsBadge).backgroundImage,
+ "none",
+ "The audio playing icon is hidden"
+ );
+ ok(
+ !tabContainer.hasAttribute("hiddensoundplaying"),
+ "There are no hidden audio tabs"
+ );
+ }
+
+ // Keep the passed in tab selected.
+ gBrowser.selectedTab = tab;
+
+ // Play sound in the other two (visible) tabs.
+ await play(otherTabs[0]);
+ await play(otherTabs[1]);
+ assertIconHidden();
+
+ // Hide one of the noisy tabs, we see the icon.
+ await hide_tab(otherTabs[0]);
+ assertIconShowing();
+
+ // Hiding the other tab keeps the icon.
+ await hide_tab(otherTabs[1]);
+ assertIconShowing();
+
+ // Pausing both tabs will hide the icon.
+ await pause(otherTabs[0]);
+ assertIconShowing();
+ await pause(otherTabs[1]);
+ assertIconHidden();
+
+ // The icon returns when audio starts again.
+ await play(otherTabs[0]);
+ await play(otherTabs[1]);
+ assertIconShowing();
+
+ // There is still an icon after hiding one tab.
+ await show_tab(otherTabs[0]);
+ assertIconShowing();
+
+ // The icon is hidden when both of the tabs are shown.
+ await show_tab(otherTabs[1]);
+ assertIconHidden();
+
+ await BrowserTestUtils.removeTab(otherTabs[0]);
+ await BrowserTestUtils.removeTab(otherTabs[1]);
+
+ // Make sure we didn't change the selected tab.
+ gBrowser.selectedTab = oldSelectedTab;
+}
+
+async function test_swapped_browser_while_playing(oldTab, newBrowser) {
+ // The tab was muted so it won't have soundplaying attribute even it's playing.
+ ok(
+ oldTab.hasAttribute("muted"),
+ "Expected the correct muted attribute on the old tab"
+ );
+ is(
+ oldTab.muteReason,
+ null,
+ "Expected the correct muteReason attribute on the old tab"
+ );
+ ok(
+ !oldTab.hasAttribute("soundplaying"),
+ "Expected the correct soundplaying attribute on the old tab"
+ );
+
+ let newTab = gBrowser.getTabForBrowser(newBrowser);
+ let AttrChangePromise = BrowserTestUtils.waitForEvent(
+ newTab,
+ "TabAttrModified",
+ false,
+ event => {
+ return event.detail.changed.includes("muted");
+ }
+ );
+
+ gBrowser.swapBrowsersAndCloseOther(newTab, oldTab);
+ await AttrChangePromise;
+
+ ok(
+ newTab.hasAttribute("muted"),
+ "Expected the correct muted attribute on the new tab"
+ );
+ is(
+ newTab.muteReason,
+ null,
+ "Expected the correct muteReason property on the new tab"
+ );
+ ok(
+ !newTab.hasAttribute("soundplaying"),
+ "Expected the correct soundplaying attribute on the new tab"
+ );
+
+ await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab);
+}
+
+async function test_swapped_browser_while_not_playing(oldTab, newBrowser) {
+ ok(
+ oldTab.hasAttribute("muted"),
+ "Expected the correct muted attribute on the old tab"
+ );
+ is(
+ oldTab.muteReason,
+ null,
+ "Expected the correct muteReason property on the old tab"
+ );
+ ok(
+ !oldTab.hasAttribute("soundplaying"),
+ "Expected the correct soundplaying attribute on the old tab"
+ );
+
+ let newTab = gBrowser.getTabForBrowser(newBrowser);
+ let AttrChangePromise = BrowserTestUtils.waitForEvent(
+ newTab,
+ "TabAttrModified",
+ false,
+ event => {
+ return event.detail.changed.includes("muted");
+ }
+ );
+
+ let AudioPlaybackPromise = new Promise(resolve => {
+ let observer = (subject, topic, data) => {
+ ok(false, "Should not see an audio-playback notification");
+ };
+ Services.obs.addObserver(observer, "audio-playback");
+ setTimeout(() => {
+ Services.obs.removeObserver(observer, "audio-playback");
+ resolve();
+ }, 100);
+ });
+
+ gBrowser.swapBrowsersAndCloseOther(newTab, oldTab);
+ await AttrChangePromise;
+
+ ok(
+ newTab.hasAttribute("muted"),
+ "Expected the correct muted attribute on the new tab"
+ );
+ is(
+ newTab.muteReason,
+ null,
+ "Expected the correct muteReason property on the new tab"
+ );
+ ok(
+ !newTab.hasAttribute("soundplaying"),
+ "Expected the correct soundplaying attribute on the new tab"
+ );
+
+ // Wait to see if an audio-playback event is dispatched.
+ await AudioPlaybackPromise;
+
+ ok(
+ newTab.hasAttribute("muted"),
+ "Expected the correct muted attribute on the new tab"
+ );
+ is(
+ newTab.muteReason,
+ null,
+ "Expected the correct muteReason property on the new tab"
+ );
+ ok(
+ !newTab.hasAttribute("soundplaying"),
+ "Expected the correct soundplaying attribute on the new tab"
+ );
+
+ await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab);
+}
+
+async function test_browser_swapping(tab, browser) {
+ // First, test swapping with a playing but muted tab.
+ await play(tab);
+
+ await test_mute_tab(tab, tab.overlayIcon, true);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function (newBrowser) {
+ await test_swapped_browser_while_playing(tab, newBrowser);
+
+ // Now, test swapping with a muted but not playing tab.
+ // Note that the tab remains muted, so we only need to pause playback.
+ tab = gBrowser.getTabForBrowser(newBrowser);
+ await pause(tab);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ secondAboutBlankBrowser =>
+ test_swapped_browser_while_not_playing(tab, secondAboutBlankBrowser)
+ );
+ }
+ );
+}
+
+async function test_click_on_pinned_tab_after_mute() {
+ async function taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ gBrowser.selectedTab = originallySelectedTab;
+ isnot(
+ tab,
+ gBrowser.selectedTab,
+ "Sanity check, the tab should not be selected!"
+ );
+
+ // Steps to reproduce the bug:
+ // Pin the tab.
+ gBrowser.pinTab(tab);
+
+ // Start playback and wait for it to finish.
+ await play(tab);
+
+ // Mute the tab.
+ let icon = tab.overlayIcon;
+ await test_mute_tab(tab, icon, true);
+
+ // Pause playback and wait for it to finish.
+ await pause(tab);
+
+ // Unmute tab.
+ await test_mute_tab(tab, icon, false);
+
+ // Now click on the tab.
+ EventUtils.synthesizeMouseAtCenter(tab.iconImage, { button: 0 });
+
+ is(tab, gBrowser.selectedTab, "Tab switch should be successful");
+
+ // Cleanup.
+ gBrowser.unpinTab(tab);
+ gBrowser.selectedTab = originallySelectedTab;
+ }
+
+ let originallySelectedTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ taskFn
+ );
+}
+
+// This test only does something useful in e10s!
+async function test_cross_process_load() {
+ async function taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Start playback and wait for it to finish.
+ await play(tab);
+
+ let soundPlayingStoppedPromise = BrowserTestUtils.waitForEvent(
+ tab,
+ "TabAttrModified",
+ false,
+ event => event.detail.changed.includes("soundplaying")
+ );
+
+ // Go to a different process.
+ BrowserTestUtils.loadURIString(browser, "about:mozilla");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await soundPlayingStoppedPromise;
+
+ ok(
+ !tab.hasAttribute("soundplaying"),
+ "Tab should not be playing sound any more"
+ );
+ ok(!tab.soundPlaying, "Tab should not be playing sound any more");
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ taskFn
+ );
+}
+
+async function test_mute_keybinding() {
+ async function test_muting_using_keyboard(tab) {
+ let mutedPromise = get_wait_for_mute_promise(tab, true);
+ EventUtils.synthesizeKey("m", { ctrlKey: true });
+ await mutedPromise;
+ mutedPromise = get_wait_for_mute_promise(tab, false);
+ EventUtils.synthesizeKey("m", { ctrlKey: true });
+ await mutedPromise;
+ }
+ async function taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Make sure it's possible to mute before the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ // Start playback and wait for it to finish.
+ await play(tab);
+
+ // Make sure it's possible to mute after the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ // Pause playback and wait for it to finish.
+ await pause(tab);
+
+ // Make sure things work if the tab is pinned.
+ gBrowser.pinTab(tab);
+
+ // Make sure it's possible to mute before the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ // Start playback and wait for it to finish.
+ await play(tab);
+
+ // Make sure it's possible to mute after the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ gBrowser.unpinTab(tab);
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ taskFn
+ );
+}
+
+async function test_on_browser(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Test the icon in a normal tab.
+ await test_playing_icon_on_tab(tab, browser, false);
+
+ gBrowser.pinTab(tab);
+
+ // Test the icon in a pinned tab.
+ await test_playing_icon_on_tab(tab, browser, true);
+
+ gBrowser.unpinTab(tab);
+
+ // Test the sound playing icon for hidden tabs.
+ await test_playing_icon_on_hidden_tab(tab);
+
+ // Retest with another browser in the foreground tab
+ if (gBrowser.selectedBrowser.currentURI.spec == PAGE) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html,test",
+ },
+ () => test_on_browser(browser)
+ );
+ } else {
+ await test_browser_swapping(tab, browser);
+ }
+}
+
+async function test_delayed_tabattr_removal() {
+ async function taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await play(tab);
+
+ // Extend the delay to guarantee the soundplaying attribute
+ // is not removed from the tab when audio is stopped. Without
+ // the extended delay the attribute could be removed in the
+ // same tick and the test wouldn't catch that this broke.
+ await pause(tab, { extendedDelay: true });
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ taskFn
+ );
+}
+
+requestLongerTimeout(2);
+add_task(async function test_page() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ test_on_browser
+ );
+});
+
+add_task(test_click_on_pinned_tab_after_mute);
+
+add_task(test_cross_process_load);
+
+add_task(test_mute_keybinding);
+
+add_task(test_delayed_tabattr_removal);
diff --git a/browser/base/content/test/tabs/browser_bfcache_exemption_about_pages.js b/browser/base/content/test/tabs/browser_bfcache_exemption_about_pages.js
new file mode 100644
index 0000000000..bcb872604c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_bfcache_exemption_about_pages.js
@@ -0,0 +1,176 @@
+requestLongerTimeout(2);
+
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+async function navigateTo(browser, urls, expectedPersist) {
+ // Navigate to a bunch of urls
+ for (let url of urls) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.loadURIString(browser, url);
+ await loaded;
+ }
+ // When we track pageshow event, save the evt.persisted on a doc element,
+ // so it can be checked from the test directly.
+ let pageShowCheck = evt => {
+ evt.target.ownerGlobal.document.documentElement.setAttribute(
+ "persisted",
+ evt.persisted
+ );
+ return true;
+ };
+ is(
+ browser.canGoBack,
+ true,
+ `After navigating to urls=${urls}, we can go back from uri=${browser.currentURI.spec}`
+ );
+ if (expectedPersist) {
+ // If we expect the page to persist, then the uri we are testing is about:blank.
+ // Currently we are only testing cases when we go forward to about:blank page,
+ // because it gets removed from history if it is sandwiched between two
+ // regular history entries. This means we can't test a scenario such as:
+ // page X, about:blank, page Y, go back -- about:blank page will be removed, and
+ // going back from page Y will take us to page X.
+
+ // Go back from about:blank (it will be the last uri in 'urls')
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ info(`Navigating back from uri=${browser.currentURI.spec}`);
+ browser.goBack();
+ await pageShowPromise;
+ info(`Got pageshow event`);
+ // Now go forward
+ let forwardPageShow = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ false,
+ pageShowCheck
+ );
+ info(`Navigating forward from uri=${browser.currentURI.spec}`);
+ browser.goForward();
+ await forwardPageShow;
+ // Check that the page got persisted
+ let persisted = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.documentElement.getAttribute("persisted");
+ });
+ is(
+ persisted,
+ expectedPersist.toString(),
+ `uri ${browser.currentURI.spec} should have persisted`
+ );
+ } else {
+ // Go back
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ false,
+ pageShowCheck
+ );
+ info(`Navigating back from uri=${browser.currentURI.spec}`);
+ browser.goBack();
+ await pageShowPromise;
+ info(`Got pageshow event`);
+ // Check that the page did not get persisted
+ let persisted = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.documentElement.getAttribute("persisted");
+ });
+ is(
+ persisted,
+ expectedPersist.toString(),
+ `uri ${browser.currentURI.spec} shouldn't have persisted`
+ );
+ }
+}
+
+add_task(async function testAboutPagesExemptFromBfcache() {
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({ set: [["fission.bfcacheInParent", true]] });
+
+ // Navigate to a bunch of urls, then go back once, check that the penultimate page did not go into BFbache
+ var browser;
+ // First page is about:privatebrowsing
+ const private_test_cases = [
+ ["about:blank"],
+ ["about:blank", "about:privatebrowsing", "about:blank"],
+ ];
+ for (const urls of private_test_cases) {
+ info(`Private tab - navigate to ${urls} urls`);
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ waitForTabURL: "about:privatebrowsing",
+ });
+ browser = win.gBrowser.selectedTab.linkedBrowser;
+ await navigateTo(browser, urls, false);
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ // First page is about:blank
+ const regular_test_cases = [
+ ["about:home"],
+ ["about:home", "about:blank"],
+ ["about:blank", "about:newtab"],
+ ];
+ for (const urls of regular_test_cases) {
+ info(`Regular tab - navigate to ${urls} urls`);
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ opening: "about:newtab",
+ });
+ browser = tab.linkedBrowser;
+ await navigateTo(browser, urls, false);
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
+
+// Test that about:blank or pages that have about:* subframes get bfcached.
+// TODO bug 1705789: add about:reader tests when we make them bfcache compatible.
+add_task(async function testAboutPagesBfcacheAllowed() {
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({ set: [["fission.bfcacheInParent", true]] });
+
+ var browser;
+ // First page is about:privatebrowsing
+ // about:privatebrowsing -> about:blank, go back, go forward, - about:blank is bfcached
+ // about:privatebrowsing -> about:home -> about:blank, go back, go forward, - about:blank is bfcached
+ // about:privatebrowsing -> file_about_srcdoc.html, go back, go forward - file_about_srcdoc.html is bfcached
+ const private_test_cases = [
+ ["about:blank"],
+ ["about:home", "about:blank"],
+ [BASE + "file_about_srcdoc.html"],
+ ];
+ for (const urls of private_test_cases) {
+ info(`Private tab - navigate to ${urls} urls`);
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ waitForTabURL: "about:privatebrowsing",
+ });
+ browser = win.gBrowser.selectedTab.linkedBrowser;
+ await navigateTo(browser, urls, true);
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ // First page is about:blank
+ // about:blank -> about:home -> about:blank, go back, go forward, - about:blank is bfcached
+ // about:blank -> about:home -> file_about_srcdoc.html, go back, go forward - file_about_srcdoc.html is bfcached
+ const regular_test_cases = [
+ ["about:home", "about:blank"],
+ ["about:home", BASE + "file_about_srcdoc.html"],
+ ];
+ for (const urls of regular_test_cases) {
+ info(`Regular tab - navigate to ${urls} urls`);
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ });
+ browser = tab.linkedBrowser;
+ await navigateTo(browser, urls, true);
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_bug580956.js b/browser/base/content/test/tabs/browser_bug580956.js
new file mode 100644
index 0000000000..1aa6aae129
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_bug580956.js
@@ -0,0 +1,25 @@
+function numClosedTabs() {
+ return SessionStore.getClosedTabCountForWindow(window);
+}
+
+function isUndoCloseEnabled() {
+ updateTabContextMenu();
+ return !document.getElementById("context_undoCloseTab").disabled;
+}
+
+add_task(async function test() {
+ Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", 0);
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ is(numClosedTabs(), 0, "There should be 0 closed tabs.");
+ ok(!isUndoCloseEnabled(), "Undo Close Tab should be disabled.");
+
+ var tab = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/");
+ var browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ ok(isUndoCloseEnabled(), "Undo Close Tab should be enabled.");
+});
diff --git a/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js b/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js
new file mode 100644
index 0000000000..a3436fcefb
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TabState } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabState.sys.mjs"
+);
+
+/**
+ * Simulate a restart of a tab by removing it, then add a lazy tab
+ * which is restored with the tabData of the removed tab.
+ *
+ * @param tab
+ * The tab to restart.
+ * @return {Object} the restored lazy tab
+ */
+const restartTab = async function (tab) {
+ let tabData = TabState.clone(tab);
+ BrowserTestUtils.removeTab(tab);
+
+ let restoredLazyTab = BrowserTestUtils.addTab(gBrowser, "", {
+ createLazyBrowser: true,
+ });
+ SessionStore.setTabState(restoredLazyTab, JSON.stringify(tabData));
+ return restoredLazyTab;
+};
+
+function get_tab_state(tab) {
+ return JSON.parse(SessionStore.getTabState(tab));
+}
+
+add_task(async function () {
+ const tab = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/");
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Let's make sure the tab is not in a muted state at the beginning
+ ok(!("muted" in get_tab_state(tab)), "Tab should not be in a muted state");
+
+ info("toggling Muted audio...");
+ tab.toggleMuteAudio();
+
+ ok("muted" in get_tab_state(tab), "Tab should be in a muted state");
+
+ info("Restarting tab...");
+ let restartedTab = await restartTab(tab);
+
+ ok(
+ "muted" in get_tab_state(restartedTab),
+ "Restored tab should still be in a muted state after restart"
+ );
+ ok(!restartedTab.linkedPanel, "Restored tab should not be inserted");
+
+ BrowserTestUtils.removeTab(restartedTab);
+});
diff --git a/browser/base/content/test/tabs/browser_close_during_beforeunload.js b/browser/base/content/test/tabs/browser_close_during_beforeunload.js
new file mode 100644
index 0000000000..2a93e29c00
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_close_during_beforeunload.js
@@ -0,0 +1,46 @@
+"use strict";
+
+// Tests that a second attempt to close a window while blocked on a
+// beforeunload confirmation ignores the beforeunload listener and
+// unblocks the original close call.
+
+const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+const DIALOG_TOPIC = CONTENT_PROMPT_SUBDIALOG
+ ? "common-dialog-loaded"
+ : "tabmodal-dialog-loaded";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let browser = win.gBrowser.selectedBrowser;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(browser, "http://example.com/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ content.addEventListener("beforeunload", event => {
+ event.preventDefault();
+ });
+ });
+
+ let confirmationShown = false;
+
+ BrowserUtils.promiseObserved(DIALOG_TOPIC).then(() => {
+ confirmationShown = true;
+ win.close();
+ });
+
+ win.close();
+ ok(confirmationShown, "Before unload confirmation should have been shown");
+ ok(win.closed, "Window should have been closed after second close() call");
+});
diff --git a/browser/base/content/test/tabs/browser_close_tab_by_dblclick.js b/browser/base/content/test/tabs/browser_close_tab_by_dblclick.js
new file mode 100644
index 0000000000..9d251f1ea6
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_close_tab_by_dblclick.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 PREF_CLOSE_TAB_BY_DBLCLICK = "browser.tabs.closeTabByDblclick";
+
+function triggerDblclickOn(target) {
+ let promise = BrowserTestUtils.waitForEvent(target, "dblclick");
+ EventUtils.synthesizeMouseAtCenter(target, { clickCount: 1 });
+ EventUtils.synthesizeMouseAtCenter(target, { clickCount: 2 });
+ return promise;
+}
+
+add_task(async function dblclick() {
+ let tab = gBrowser.selectedTab;
+ await triggerDblclickOn(tab);
+ ok(!tab.closing, "Double click the selected tab won't close it");
+});
+
+add_task(async function dblclickWithPrefSet() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_CLOSE_TAB_BY_DBLCLICK, true]],
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla", {
+ skipAnimation: true,
+ });
+ isnot(tab, gBrowser.selectedTab, "The new tab is in the background");
+
+ await triggerDblclickOn(tab);
+ is(tab, gBrowser.selectedTab, "Double click a background tab will select it");
+
+ await triggerDblclickOn(tab);
+ ok(tab.closing, "Double click the selected tab will close it");
+});
diff --git a/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js b/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js
new file mode 100644
index 0000000000..3ce653cafc
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js
@@ -0,0 +1,60 @@
+"use strict";
+
+const example_base =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/browser/base/content/test/tabs/";
+
+add_task(async function test_contextmenu_openlink_after_tabnavigated() {
+ let url = example_base + "test_bug1358314.html";
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "a",
+ 0,
+ 0,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+ info("Popup Shown");
+
+ info("Navigate the tab with the opened context menu");
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let awaitNewTabOpen = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/",
+ true
+ );
+
+ info("Click the 'open link in new tab' menu item");
+ let openLinkMenuItem = contextMenu.querySelector("#context-openlinkintab");
+ contextMenu.activateItem(openLinkMenuItem);
+
+ info("Wait for the new tab to be opened");
+ const newTab = await awaitNewTabOpen;
+
+ // Close the contextMenu popup if it has not been closed yet.
+ contextMenu.hidePopup();
+
+ is(
+ newTab.linkedBrowser.currentURI.spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/",
+ "Got the expected URL loaded in the new tab"
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_dont_process_switch_204.js b/browser/base/content/test/tabs/browser_dont_process_switch_204.js
new file mode 100644
index 0000000000..009ef54340
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_dont_process_switch_204.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_URL = TEST_ROOT + "204.sjs";
+const BLANK_URL = TEST_ROOT + "blank.html";
+
+// Test for bug 1626362.
+add_task(async function () {
+ await BrowserTestUtils.withNewTab("about:robots", async function (aBrowser) {
+ // Get the current pid for browser for comparison later, we expect this
+ // to be the parent process for about:robots.
+ let browserPid = await SpecialPowers.spawn(aBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ is(
+ Services.appinfo.processID,
+ browserPid,
+ "about:robots should have loaded in the parent"
+ );
+
+ // Attempt to load a uri that returns a 204 response, and then check that
+ // we didn't process switch for it.
+ let stopped = BrowserTestUtils.browserStopped(aBrowser, TEST_URL, true);
+ BrowserTestUtils.loadURIString(aBrowser, TEST_URL);
+ await stopped;
+
+ let newPid = await SpecialPowers.spawn(aBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ is(
+ browserPid,
+ newPid,
+ "Shouldn't change process when we get a 204 response"
+ );
+
+ // Load a valid http page and confirm that we did change process
+ // to confirm that we weren't in a web process to begin with.
+ let loaded = BrowserTestUtils.browserLoaded(aBrowser, false, BLANK_URL);
+ BrowserTestUtils.loadURIString(aBrowser, BLANK_URL);
+ await loaded;
+
+ newPid = await SpecialPowers.spawn(aBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ isnot(browserPid, newPid, "Should change process for a valid response");
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js
new file mode 100644
index 0000000000..08bd2278ef
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js
@@ -0,0 +1,208 @@
+"use strict";
+
+const kChildPage = getRootDirectory(gTestPath) + "file_about_child.html";
+const kParentPage = getRootDirectory(gTestPath) + "file_about_parent.html";
+
+const kAboutPagesRegistered = Promise.all([
+ BrowserTestUtils.registerAboutPage(
+ registerCleanupFunction,
+ "test-about-principal-child",
+ kChildPage,
+ Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | Ci.nsIAboutModule.ALLOW_SCRIPT
+ ),
+ BrowserTestUtils.registerAboutPage(
+ registerCleanupFunction,
+ "test-about-principal-parent",
+ kParentPage,
+ Ci.nsIAboutModule.ALLOW_SCRIPT
+ ),
+]);
+
+add_task(async function test_principal_click() {
+ await kAboutPagesRegistered;
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.skip_about_page_has_csp_assert", true]],
+ });
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-principal-parent",
+ async function (browser) {
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "about:test-about-principal-child"
+ );
+ let myLink = browser.contentDocument.getElementById(
+ "aboutchildprincipal"
+ );
+ myLink.click();
+ await loadPromise;
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.originalURI.asciiSpec,
+ "about:test-about-principal-child",
+ "sanity check - make sure we test the principal for the correct URI"
+ );
+
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "loading about: from privileged page must have a triggering of System"
+ );
+
+ let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+ is(
+ contentPolicyType,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "sanity check - loading a top level document"
+ );
+
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(
+ loadingPrincipal,
+ null,
+ "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal"
+ );
+ }
+ );
+ }
+ );
+});
+
+add_task(async function test_principal_ctrl_click() {
+ await kAboutPagesRegistered;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.sandbox.content.level", 1],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-principal-parent",
+ async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:test-about-principal-child",
+ true
+ );
+ // simulate ctrl+click
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#aboutchildprincipal",
+ { ctrlKey: true, metaKey: true },
+ gBrowser.selectedBrowser
+ );
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.originalURI.asciiSpec,
+ "about:test-about-principal-child",
+ "sanity check - make sure we test the principal for the correct URI"
+ );
+
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "loading about: from privileged page must have a triggering of System"
+ );
+
+ let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+ is(
+ contentPolicyType,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "sanity check - loading a top level document"
+ );
+
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(
+ loadingPrincipal,
+ null,
+ "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal"
+ );
+ }
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_principal_right_click_open_link_in_new_tab() {
+ await kAboutPagesRegistered;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.sandbox.content.level", 1],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-principal-parent",
+ async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:test-about-principal-child",
+ true
+ );
+
+ // simulate right-click open link in tab
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-openlinkintab").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#aboutchildprincipal",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.originalURI.asciiSpec,
+ "about:test-about-principal-child",
+ "sanity check - make sure we test the principal for the correct URI"
+ );
+
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "loading about: from privileged page must have a triggering of System"
+ );
+
+ let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+ is(
+ contentPolicyType,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "sanity check - loading a top level document"
+ );
+
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(
+ loadingPrincipal,
+ null,
+ "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal"
+ );
+ }
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
diff --git a/browser/base/content/test/tabs/browser_e10s_about_process.js b/browser/base/content/test/tabs/browser_e10s_about_process.js
new file mode 100644
index 0000000000..f73e8e659c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_about_process.js
@@ -0,0 +1,174 @@
+const CHROME = {
+ id: "cb34538a-d9da-40f3-b61a-069f0b2cb9fb",
+ path: "test-chrome",
+ flags: 0,
+};
+const CANREMOTE = {
+ id: "2480d3e1-9ce4-4b84-8ae3-910b9a95cbb3",
+ path: "test-allowremote",
+ flags: Ci.nsIAboutModule.URI_CAN_LOAD_IN_CHILD,
+};
+const MUSTREMOTE = {
+ id: "f849cee5-e13e-44d2-981d-0fb3884aaead",
+ path: "test-mustremote",
+ flags: Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD,
+};
+const CANPRIVILEGEDREMOTE = {
+ id: "a04ffafe-6c63-4266-acae-0f4b093165aa",
+ path: "test-canprivilegedremote",
+ flags:
+ Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
+ Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS,
+};
+const MUSTEXTENSION = {
+ id: "f7a1798f-965b-49e9-be83-ec6ee4d7d675",
+ path: "test-mustextension",
+ flags: Ci.nsIAboutModule.URI_MUST_LOAD_IN_EXTENSION_PROCESS,
+};
+
+const TEST_MODULES = [
+ CHROME,
+ CANREMOTE,
+ MUSTREMOTE,
+ CANPRIVILEGEDREMOTE,
+ MUSTEXTENSION,
+];
+
+function AboutModule() {}
+
+AboutModule.prototype = {
+ newChannel(aURI, aLoadInfo) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ getURIFlags(aURI) {
+ for (let module of TEST_MODULES) {
+ if (aURI.pathQueryRef.startsWith(module.path)) {
+ return module.flags;
+ }
+ }
+
+ ok(false, "Called getURIFlags for an unknown page " + aURI.spec);
+ return 0;
+ },
+
+ getIndexedDBOriginPostfix(aURI) {
+ return null;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]),
+};
+
+var AboutModuleFactory = {
+ createInstance(aIID) {
+ return new AboutModule().QueryInterface(aIID);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+};
+
+add_setup(async function () {
+ SpecialPowers.setBoolPref(
+ "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
+ true
+ );
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ for (let module of TEST_MODULES) {
+ registrar.registerFactory(
+ Components.ID(module.id),
+ "",
+ "@mozilla.org/network/protocol/about;1?what=" + module.path,
+ AboutModuleFactory
+ );
+ }
+});
+
+registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref(
+ "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess"
+ );
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ for (let module of TEST_MODULES) {
+ registrar.unregisterFactory(Components.ID(module.id), AboutModuleFactory);
+ }
+});
+
+add_task(async function test_chrome() {
+ test_url_for_process_types({
+ url: "about:" + CHROME.path,
+ chromeResult: true,
+ webContentResult: false,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
+
+add_task(async function test_any() {
+ test_url_for_process_types({
+ url: "about:" + CANREMOTE.path,
+ chromeResult: true,
+ webContentResult: true,
+ privilegedAboutContentResult: true,
+ privilegedMozillaContentResult: true,
+ extensionProcessResult: true,
+ });
+});
+
+add_task(async function test_remote() {
+ test_url_for_process_types({
+ url: "about:" + MUSTREMOTE.path,
+ chromeResult: false,
+ webContentResult: true,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
+
+add_task(async function test_privileged_remote_true() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.remote.separatePrivilegedContentProcess", true]],
+ });
+
+ // This shouldn't be taken literally. We will always use the privleged about
+ // content type if the URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS flag is enabled and
+ // the pref is turned on.
+ test_url_for_process_types({
+ url: "about:" + CANPRIVILEGEDREMOTE.path,
+ chromeResult: false,
+ webContentResult: false,
+ privilegedAboutContentResult: true,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
+
+add_task(async function test_privileged_remote_false() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.remote.separatePrivilegedContentProcess", false]],
+ });
+
+ // This shouldn't be taken literally. We will always use the privleged about
+ // content type if the URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS flag is enabled and
+ // the pref is turned on.
+ test_url_for_process_types({
+ url: "about:" + CANPRIVILEGEDREMOTE.path,
+ chromeResult: false,
+ webContentResult: true,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
+
+add_task(async function test_extension() {
+ test_url_for_process_types({
+ url: "about:" + MUSTEXTENSION.path,
+ chromeResult: false,
+ webContentResult: false,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: true,
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_e10s_chrome_process.js b/browser/base/content/test/tabs/browser_e10s_chrome_process.js
new file mode 100644
index 0000000000..aa6a893372
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_chrome_process.js
@@ -0,0 +1,136 @@
+// Returns a function suitable for add_task which loads startURL, runs
+// transitionTask and waits for endURL to load, checking that the URLs were
+// loaded in the correct process.
+function makeTest(
+ name,
+ startURL,
+ startProcessIsRemote,
+ endURL,
+ endProcessIsRemote,
+ transitionTask
+) {
+ return async function () {
+ info("Running test " + name + ", " + transitionTask.name);
+ let browser = gBrowser.selectedBrowser;
+
+ // In non-e10s nothing should be remote
+ if (!gMultiProcessBrowser) {
+ startProcessIsRemote = false;
+ endProcessIsRemote = false;
+ }
+
+ // Load the initial URL and make sure we are in the right initial process
+ info("Loading initial URL");
+ BrowserTestUtils.loadURIString(browser, startURL);
+ await BrowserTestUtils.browserLoaded(browser, false, startURL);
+
+ is(browser.currentURI.spec, startURL, "Shouldn't have been redirected");
+ is(
+ browser.isRemoteBrowser,
+ startProcessIsRemote,
+ "Should be displayed in the right process"
+ );
+
+ let docLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ endURL
+ );
+ await transitionTask(browser, endURL);
+ await docLoadedPromise;
+
+ is(browser.currentURI.spec, endURL, "Should have made it to the final URL");
+ is(
+ browser.isRemoteBrowser,
+ endProcessIsRemote,
+ "Should be displayed in the right process"
+ );
+ };
+}
+
+const PATH = (
+ getRootDirectory(gTestPath) + "test_process_flags_chrome.html"
+).replace("chrome://mochitests", "");
+
+const CHROME = "chrome://mochitests" + PATH;
+const CANREMOTE = "chrome://mochitests-any" + PATH;
+const MUSTREMOTE = "chrome://mochitests-content" + PATH;
+
+add_setup(async function () {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ forceNotRemote: true,
+ });
+});
+
+registerCleanupFunction(() => {
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_chrome() {
+ test_url_for_process_types({
+ url: CHROME,
+ chromeResult: true,
+ webContentResult: false,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
+
+add_task(async function test_any() {
+ test_url_for_process_types({
+ url: CANREMOTE,
+ chromeResult: true,
+ webContentResult: true,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
+
+add_task(async function test_remote() {
+ test_url_for_process_types({
+ url: MUSTREMOTE,
+ chromeResult: false,
+ webContentResult: true,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
+
+// The set of page transitions
+var TESTS = [
+ ["chrome -> chrome", CHROME, false, CHROME, false],
+ ["chrome -> canremote", CHROME, false, CANREMOTE, false],
+ ["chrome -> mustremote", CHROME, false, MUSTREMOTE, true],
+ ["remote -> chrome", MUSTREMOTE, true, CHROME, false],
+ ["remote -> canremote", MUSTREMOTE, true, CANREMOTE, true],
+ ["remote -> mustremote", MUSTREMOTE, true, MUSTREMOTE, true],
+];
+
+// The different ways to transition from one page to another
+var TRANSITIONS = [
+ // Loads the new page by calling browser.loadURI directly
+ async function loadURI(browser, uri) {
+ info("Calling browser.loadURI");
+ BrowserTestUtils.loadURIString(browser, uri);
+ },
+
+ // Loads the new page by finding a link with the right href in the document and
+ // clicking it
+ function clickLink(browser, uri) {
+ info("Clicking link");
+ SpecialPowers.spawn(browser, [uri], function frame_script(frameUri) {
+ let link = content.document.querySelector("a[href='" + frameUri + "']");
+ link.click();
+ });
+ },
+];
+
+// Creates a set of test tasks, one for each combination of TESTS and TRANSITIONS.
+for (let test of TESTS) {
+ for (let transition of TRANSITIONS) {
+ add_task(makeTest(...test, transition));
+ }
+}
diff --git a/browser/base/content/test/tabs/browser_e10s_javascript.js b/browser/base/content/test/tabs/browser_e10s_javascript.js
new file mode 100644
index 0000000000..ffb03b4d79
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_javascript.js
@@ -0,0 +1,19 @@
+const CHROME_PROCESS = E10SUtils.NOT_REMOTE;
+const WEB_CONTENT_PROCESS = E10SUtils.WEB_REMOTE_TYPE;
+
+add_task(async function () {
+ let url = "javascript:dosomething()";
+
+ ok(
+ E10SUtils.canLoadURIInRemoteType(url, /* fission */ false, CHROME_PROCESS),
+ "Check URL in chrome process."
+ );
+ ok(
+ E10SUtils.canLoadURIInRemoteType(
+ url,
+ /* fission */ false,
+ WEB_CONTENT_PROCESS
+ ),
+ "Check URL in web content process."
+ );
+});
diff --git a/browser/base/content/test/tabs/browser_e10s_mozillaweb_process.js b/browser/base/content/test/tabs/browser_e10s_mozillaweb_process.js
new file mode 100644
index 0000000000..88542a0b16
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_mozillaweb_process.js
@@ -0,0 +1,52 @@
+add_task(async function test_privileged_remote_true() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.remote.separatePrivilegedContentProcess", true],
+ ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
+ ["browser.tabs.remote.separatedMozillaDomains", "example.org"],
+ ],
+ });
+
+ test_url_for_process_types({
+ url: "https://example.com",
+ chromeResult: false,
+ webContentResult: true,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+ test_url_for_process_types({
+ url: "https://example.org",
+ chromeResult: false,
+ webContentResult: false,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: true,
+ extensionProcessResult: false,
+ });
+});
+
+add_task(async function test_privileged_remote_false() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.remote.separatePrivilegedContentProcess", true],
+ ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", false],
+ ],
+ });
+
+ test_url_for_process_types({
+ url: "https://example.com",
+ chromeResult: false,
+ webContentResult: true,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+ test_url_for_process_types({
+ url: "https://example.org",
+ chromeResult: false,
+ webContentResult: true,
+ privilegedAboutContentResult: false,
+ privilegedMozillaContentResult: false,
+ extensionProcessResult: false,
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_e10s_switchbrowser.js b/browser/base/content/test/tabs/browser_e10s_switchbrowser.js
new file mode 100644
index 0000000000..0104f3c60c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_switchbrowser.js
@@ -0,0 +1,490 @@
+requestLongerTimeout(2);
+
+const DUMMY_PATH = "browser/browser/base/content/test/general/dummy_page.html";
+
+const gExpectedHistory = {
+ index: -1,
+ entries: [],
+};
+
+async function get_remote_history(browser) {
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let sessionHistory = browser.browsingContext?.sessionHistory;
+ if (!sessionHistory) {
+ return null;
+ }
+
+ let result = {
+ index: sessionHistory.index,
+ entries: [],
+ };
+
+ for (let i = 0; i < sessionHistory.count; i++) {
+ let entry = sessionHistory.getEntryAtIndex(i);
+ result.entries.push({
+ uri: entry.URI.spec,
+ title: entry.title,
+ });
+ }
+ return result;
+ }
+
+ return SpecialPowers.spawn(browser, [], () => {
+ let webNav = content.docShell.QueryInterface(Ci.nsIWebNavigation);
+ let sessionHistory = webNav.sessionHistory;
+ let result = {
+ index: sessionHistory.index,
+ entries: [],
+ };
+
+ for (let i = 0; i < sessionHistory.count; i++) {
+ let entry = sessionHistory.legacySHistory.getEntryAtIndex(i);
+ result.entries.push({
+ uri: entry.URI.spec,
+ title: entry.title,
+ });
+ }
+
+ return result;
+ });
+}
+
+var check_history = async function () {
+ let sessionHistory = await get_remote_history(gBrowser.selectedBrowser);
+
+ let count = sessionHistory.entries.length;
+ is(
+ count,
+ gExpectedHistory.entries.length,
+ "Should have the right number of history entries"
+ );
+ is(
+ sessionHistory.index,
+ gExpectedHistory.index,
+ "Should have the right history index"
+ );
+
+ for (let i = 0; i < count; i++) {
+ let entry = sessionHistory.entries[i];
+ info("Checking History Entry: " + entry.uri);
+ is(entry.uri, gExpectedHistory.entries[i].uri, "Should have the right URI");
+ is(
+ entry.title,
+ gExpectedHistory.entries[i].title,
+ "Should have the right title"
+ );
+ }
+};
+
+function clear_history() {
+ gExpectedHistory.index = -1;
+ gExpectedHistory.entries = [];
+}
+
+// Waits for a load and updates the known history
+var waitForLoad = async function (uriString) {
+ info("Loading " + uriString);
+ // Longwinded but this ensures we don't just shortcut to LoadInNewProcess
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ gBrowser.selectedBrowser.webNavigation.loadURI(
+ Services.io.newURI(uriString),
+ loadURIOptions
+ );
+
+ await BrowserTestUtils.browserStopped(gBrowser, uriString);
+
+ // Some of the documents we're using in this test use Fluent,
+ // and they may finish localization later.
+ // To prevent this test from being intermittent, we'll
+ // wait for the `document.l10n.ready` promise to resolve.
+ if (
+ gBrowser.selectedBrowser.contentWindow &&
+ gBrowser.selectedBrowser.contentWindow.document.l10n
+ ) {
+ await gBrowser.selectedBrowser.contentWindow.document.l10n.ready;
+ }
+ gExpectedHistory.index++;
+ gExpectedHistory.entries.push({
+ uri: gBrowser.currentURI.spec,
+ title: gBrowser.contentTitle,
+ });
+};
+
+// Waits for a load and updates the known history
+var waitForLoadWithFlags = async function (
+ uriString,
+ flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE
+) {
+ info("Loading " + uriString + " flags = " + flags);
+ gBrowser.selectedBrowser.loadURI(Services.io.newURI(uriString), {
+ flags,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await BrowserTestUtils.browserStopped(gBrowser, uriString);
+ if (!(flags & Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY)) {
+ if (flags & Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY) {
+ gExpectedHistory.entries.pop();
+ } else {
+ gExpectedHistory.index++;
+ }
+
+ gExpectedHistory.entries.push({
+ uri: gBrowser.currentURI.spec,
+ title: gBrowser.contentTitle,
+ });
+ }
+};
+
+var back = async function () {
+ info("Going back");
+ gBrowser.goBack();
+ await BrowserTestUtils.browserStopped(gBrowser);
+ gExpectedHistory.index--;
+};
+
+var forward = async function () {
+ info("Going forward");
+ gBrowser.goForward();
+ await BrowserTestUtils.browserStopped(gBrowser);
+ gExpectedHistory.index++;
+};
+
+// Tests that navigating from a page that should be in the remote process and
+// a page that should be in the main process works and retains history
+add_task(async function test_navigation() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+
+ let expectedRemote = gMultiProcessBrowser;
+
+ info("1");
+ // Create a tab and load a remote page in it
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ let { permanentKey } = gBrowser.selectedBrowser;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await waitForLoad("http://example.org/" + DUMMY_PATH);
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+
+ info("2");
+ // Load another page
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await waitForLoad("http://example.com/" + DUMMY_PATH);
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await check_history();
+
+ info("3");
+ // Load a non-remote page
+ await waitForLoad("about:robots");
+ await TestUtils.waitForCondition(
+ () => !!gBrowser.selectedBrowser.contentTitle.length,
+ "Waiting for about:robots title to update"
+ );
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ false,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await check_history();
+
+ info("4");
+ // Load a remote page
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await waitForLoad("http://example.org/" + DUMMY_PATH);
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await check_history();
+
+ info("5");
+ await back();
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ false,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await TestUtils.waitForCondition(
+ () => !!gBrowser.selectedBrowser.contentTitle.length,
+ "Waiting for about:robots title to update"
+ );
+ await check_history();
+
+ info("6");
+ await back();
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await check_history();
+
+ info("7");
+ await forward();
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ false,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await TestUtils.waitForCondition(
+ () => !!gBrowser.selectedBrowser.contentTitle.length,
+ "Waiting for about:robots title to update"
+ );
+ await check_history();
+
+ info("8");
+ await forward();
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await check_history();
+
+ info("9");
+ await back();
+ await TestUtils.waitForCondition(
+ () => !!gBrowser.selectedBrowser.contentTitle.length,
+ "Waiting for about:robots title to update"
+ );
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ false,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await check_history();
+
+ info("10");
+ // Load a new remote page, this should replace the last history entry
+ gExpectedHistory.entries.splice(gExpectedHistory.entries.length - 1, 1);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await waitForLoad("http://example.com/" + DUMMY_PATH);
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+ await check_history();
+
+ info("11");
+ gBrowser.removeCurrentTab();
+ clear_history();
+});
+
+// Tests that calling gBrowser.loadURI or browser.loadURI to load a page in a
+// different process updates the browser synchronously
+add_task(async function test_synchronous() {
+ let expectedRemote = gMultiProcessBrowser;
+
+ info("1");
+ // Create a tab and load a remote page in it
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ let { permanentKey } = gBrowser.selectedBrowser;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await waitForLoad("http://example.org/" + DUMMY_PATH);
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+
+ info("2");
+ // Load another page
+ info("Loading about:robots");
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserStopped(gBrowser);
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ false,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+
+ info("3");
+ // Load the remote page again
+ info("Loading http://example.org/" + DUMMY_PATH);
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/" + DUMMY_PATH
+ );
+ await BrowserTestUtils.browserStopped(gBrowser);
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ permanentKey,
+ "browser.permanentKey is still the same"
+ );
+
+ info("4");
+ gBrowser.removeCurrentTab();
+ clear_history();
+});
+
+// Tests that load flags are correctly passed through to the child process with
+// normal loads
+add_task(async function test_loadflags() {
+ let expectedRemote = gMultiProcessBrowser;
+
+ info("1");
+ // Create a tab and load a remote page in it
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ await waitForLoadWithFlags("about:robots");
+ await TestUtils.waitForCondition(
+ () => gBrowser.selectedBrowser.contentTitle != "about:robots",
+ "Waiting for about:robots title to update"
+ );
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ false,
+ "Remote attribute should be correct"
+ );
+ await check_history();
+
+ info("2");
+ // Load a page in the remote process with some custom flags
+ await waitForLoadWithFlags(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/" + DUMMY_PATH,
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY
+ );
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ await check_history();
+
+ info("3");
+ // Load a non-remote page
+ await waitForLoadWithFlags("about:robots");
+ await TestUtils.waitForCondition(
+ () => !!gBrowser.selectedBrowser.contentTitle.length,
+ "Waiting for about:robots title to update"
+ );
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ false,
+ "Remote attribute should be correct"
+ );
+ await check_history();
+
+ info("4");
+ // Load another remote page
+ await waitForLoadWithFlags(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/" + DUMMY_PATH,
+ Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY
+ );
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ await check_history();
+
+ info("5");
+ // Load another remote page from a different origin
+ await waitForLoadWithFlags(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/" + DUMMY_PATH,
+ Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY
+ );
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ expectedRemote,
+ "Remote attribute should be correct"
+ );
+ await check_history();
+
+ is(
+ gExpectedHistory.entries.length,
+ 2,
+ "Should end with the right number of history entries"
+ );
+
+ info("6");
+ gBrowser.removeCurrentTab();
+ clear_history();
+});
diff --git a/browser/base/content/test/tabs/browser_file_to_http_named_popup.js b/browser/base/content/test/tabs/browser_file_to_http_named_popup.js
new file mode 100644
index 0000000000..57e5ec7ad3
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_file_to_http_named_popup.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const TEST_FILE = fileURL("dummy_page.html");
+const TEST_HTTP = httpURL("dummy_page.html");
+
+// Test for Bug 1634252
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(TEST_FILE, async function (fileBrowser) {
+ info("Tab ready");
+
+ async function summonPopup(firstRun) {
+ var winPromise;
+ if (firstRun) {
+ winPromise = BrowserTestUtils.waitForNewWindow({
+ url: TEST_HTTP,
+ });
+ }
+
+ await SpecialPowers.spawn(
+ fileBrowser,
+ [TEST_HTTP, firstRun],
+ (target, firstRun_) => {
+ var win = content.open(target, "named", "width=400,height=400");
+ win.focus();
+ ok(win, "window.open was successful");
+ if (firstRun_) {
+ content.document.firstWindow = win;
+ } else {
+ content.document.otherWindow = win;
+ }
+ }
+ );
+
+ if (firstRun) {
+ // We should only wait for the window the first time, because only the
+ // later times no new window should be created.
+ info("Waiting for new window");
+ var win = await winPromise;
+ ok(win, "Got a window");
+ }
+ }
+
+ info("Opening window");
+ await summonPopup(true);
+ info("Opening window again");
+ await summonPopup(false);
+
+ await SpecialPowers.spawn(fileBrowser, [], () => {
+ ok(content.document.firstWindow, "Window is non-null");
+ is(
+ content.document.otherWindow,
+ content.document.firstWindow,
+ "Windows are the same"
+ );
+
+ content.document.firstWindow.close();
+ });
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_file_to_http_script_closable.js b/browser/base/content/test/tabs/browser_file_to_http_script_closable.js
new file mode 100644
index 0000000000..00ef3d7322
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_file_to_http_script_closable.js
@@ -0,0 +1,43 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const TEST_FILE = fileURL("dummy_page.html");
+const TEST_HTTP = httpURL("tab_that_closes.html");
+
+// Test for Bug 1632441
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.allow_scripts_to_close_windows", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(TEST_FILE, async function (fileBrowser) {
+ info("Tab ready");
+
+ // The request will open a new tab, capture the new tab and the load in it.
+ info("Creating promise");
+ var newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => {
+ return url.endsWith("tab_that_closes.html");
+ },
+ true,
+ false
+ );
+
+ // Click the link, which will post to target="_blank"
+ info("Creating and clicking link");
+ await SpecialPowers.spawn(fileBrowser, [TEST_HTTP], target => {
+ content.open(target);
+ });
+
+ // The new tab will load.
+ info("Waiting for load");
+ var newTab = await newTabPromise;
+ ok(newTab, "Tab is loaded");
+ info("waiting for it to close");
+ await BrowserTestUtils.waitForTabClosing(newTab);
+ ok(true, "The test completes without a timeout");
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/tabs/browser_hiddentab_contextmenu.js b/browser/base/content/test/tabs/browser_hiddentab_contextmenu.js
new file mode 100644
index 0000000000..5c54896efb
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_hiddentab_contextmenu.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/. */
+
+// Tests the context menu of hidden tabs which users can open via the All Tabs
+// menu's Hidden Tabs view.
+
+add_task(async function test() {
+ is(gBrowser.visibleTabs.length, 1, "there is initially one visible tab");
+
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.hideTab(tab);
+ ok(tab.hidden, "new tab is hidden");
+ is(gBrowser.visibleTabs.length, 1, "there is still only one visible tabs");
+
+ updateTabContextMenu(tab);
+ ok(
+ document.getElementById("context_moveTabOptions").disabled,
+ "Move Tab menu is disabled"
+ );
+ ok(
+ document.getElementById("context_closeTabsToTheStart").disabled,
+ "Close Tabs to Left is disabled"
+ );
+ ok(
+ document.getElementById("context_closeTabsToTheEnd").disabled,
+ "Close Tabs to Right is disabled"
+ );
+ ok(
+ document.getElementById("context_reopenInContainer").disabled,
+ "Open in New Container Tab menu is disabled"
+ );
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js b/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js
new file mode 100644
index 0000000000..665bdb7f69
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js
@@ -0,0 +1,157 @@
+"use strict";
+
+// Helper that watches events that may be triggered when tab browsers are
+// swapped during the test.
+//
+// The primary purpose of this helper is to access tab browser properties
+// during tab events, to verify that there are no undesired side effects, as a
+// regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1695346
+class TabEventTracker {
+ constructor(tab) {
+ this.tab = tab;
+
+ tab.addEventListener("TabAttrModified", this);
+ tab.addEventListener("TabShow", this);
+ tab.addEventListener("TabHide", this);
+ }
+
+ handleEvent(event) {
+ let description = `${this._expectations.description} at ${event.type}`;
+ if (event.type === "TabAttrModified") {
+ description += `, changed=${event.detail.changed}`;
+ }
+
+ const browser = this.tab.linkedBrowser;
+ is(
+ browser.currentURI.spec,
+ this._expectations.tabUrl,
+ `${description} - expected currentURI`
+ );
+ ok(browser._cachedCurrentURI, `${description} - currentURI was cached`);
+
+ if (event.type === "TabAttrModified") {
+ if (event.detail.changed.includes("muted")) {
+ if (browser.audioMuted) {
+ this._counts.muted++;
+ } else {
+ this._counts.unmuted++;
+ }
+ }
+ } else if (event.type === "TabShow") {
+ this._counts.shown++;
+ } else if (event.type === "TabHide") {
+ this._counts.hidden++;
+ } else {
+ ok(false, `Unexpected event: ${event.type}`);
+ }
+ }
+
+ setExpectations(expectations) {
+ this._expectations = expectations;
+
+ this._counts = {
+ muted: 0,
+ unmuted: 0,
+ shown: 0,
+ hidden: 0,
+ };
+ }
+
+ checkExpectations() {
+ const { description, counters, tabUrl } = this._expectations;
+ Assert.deepEqual(
+ this._counts,
+ counters,
+ `${description} - events observed while swapping tab`
+ );
+ let browser = this.tab.linkedBrowser;
+ is(browser.currentURI.spec, tabUrl, `${description} - tab's currentURI`);
+
+ // Tabs without titles default to URLs without scheme, according to the
+ // logic of tabbrowser.js's setTabTitle/_setTabLabel.
+ // TODO bug 1695512: lazy tabs deviate from that expectation, so the title
+ // is the full URL instead of the URL with the scheme stripped.
+ let tabTitle = tabUrl;
+ is(browser.contentTitle, tabTitle, `${description} - tab's contentTitle`);
+ }
+}
+
+add_task(async function test_hidden_muted_lazy_tabs_and_swapping() {
+ const params = { createLazyBrowser: true };
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const URL_HIDDEN = "http://example.com/hide";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const URL_MUTED = "http://example.com/mute";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const URL_NORMAL = "http://example.com/back";
+
+ const lazyTab = BrowserTestUtils.addTab(gBrowser, "", params);
+ const mutedTab = BrowserTestUtils.addTab(gBrowser, URL_MUTED, params);
+ const hiddenTab = BrowserTestUtils.addTab(gBrowser, URL_HIDDEN, params);
+ const normalTab = BrowserTestUtils.addTab(gBrowser, URL_NORMAL, params);
+
+ mutedTab.toggleMuteAudio();
+ gBrowser.hideTab(hiddenTab);
+
+ is(lazyTab.linkedPanel, "", "lazyTab is lazy");
+ is(hiddenTab.linkedPanel, "", "hiddenTab is lazy");
+ is(mutedTab.linkedPanel, "", "mutedTab is lazy");
+ is(normalTab.linkedPanel, "", "normalTab is lazy");
+
+ ok(mutedTab.linkedBrowser.audioMuted, "mutedTab is muted");
+ ok(hiddenTab.hidden, "hiddenTab is hidden");
+ ok(!lazyTab.linkedBrowser.audioMuted, "lazyTab was not muted");
+ ok(!lazyTab.hidden, "lazyTab was not hidden");
+
+ const tabEventTracker = new TabEventTracker(lazyTab);
+
+ tabEventTracker.setExpectations({
+ description: "mutedTab replaced lazyTab (initial)",
+ counters: {
+ muted: 1,
+ unmuted: 0,
+ shown: 0,
+ hidden: 0,
+ },
+ tabUrl: URL_MUTED,
+ });
+ gBrowser.swapBrowsersAndCloseOther(lazyTab, mutedTab);
+ tabEventTracker.checkExpectations();
+ is(lazyTab.linkedPanel, "", "muted lazyTab is still lazy");
+ ok(lazyTab.linkedBrowser.audioMuted, "muted lazyTab is now muted");
+ ok(!lazyTab.hidden, "muted lazyTab is not hidden");
+
+ tabEventTracker.setExpectations({
+ description: "hiddenTab replaced lazyTab/mutedTab",
+ counters: {
+ muted: 0,
+ unmuted: 1,
+ shown: 0,
+ hidden: 1,
+ },
+ tabUrl: URL_HIDDEN,
+ });
+ gBrowser.swapBrowsersAndCloseOther(lazyTab, hiddenTab);
+ tabEventTracker.checkExpectations();
+ is(lazyTab.linkedPanel, "", "hidden lazyTab is still lazy");
+ ok(!lazyTab.linkedBrowser.audioMuted, "hidden lazyTab is not muted any more");
+ ok(lazyTab.hidden, "hidden lazyTab has been hidden");
+
+ tabEventTracker.setExpectations({
+ description: "normalTab replaced lazyTab/hiddenTab",
+ counters: {
+ muted: 0,
+ unmuted: 0,
+ shown: 1,
+ hidden: 0,
+ },
+ tabUrl: URL_NORMAL,
+ });
+ gBrowser.swapBrowsersAndCloseOther(lazyTab, normalTab);
+ tabEventTracker.checkExpectations();
+ is(lazyTab.linkedPanel, "", "normal lazyTab is still lazy");
+ ok(!lazyTab.linkedBrowser.audioMuted, "normal lazyTab is not muted any more");
+ ok(!lazyTab.hidden, "normal lazyTab is not hidden any more");
+
+ BrowserTestUtils.removeTab(lazyTab);
+});
diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.js
new file mode 100644
index 0000000000..b573113481
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.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/. */
+
+// Test the behavior of the tab and the urlbar when opening about:blank by clicking link.
+
+/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
+ this
+);
+
+add_task(async function blank_target__foreground() {
+ await doTestInSameWindow({
+ link: "blank-page--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: "",
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: "",
+ history: [BLANK_URL],
+ },
+ });
+});
+
+add_task(async function blank_target__background() {
+ await doTestInSameWindow({
+ link: "blank-page--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: HOME_URL,
+ history: [BLANK_URL],
+ },
+ });
+});
+
+add_task(async function other_target__foreground() {
+ await doTestInSameWindow({
+ link: "blank-page--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ history: [BLANK_URL],
+ },
+ });
+});
+
+add_task(async function other_target__background() {
+ await doTestInSameWindow({
+ link: "blank-page--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: HOME_URL,
+ history: [BLANK_URL],
+ },
+ });
+});
+
+add_task(async function by_script() {
+ await doTestInSameWindow({
+ link: "blank-page--by-script",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ history: [BLANK_URL],
+ },
+ });
+});
+
+add_task(async function no_target() {
+ await doTestInSameWindow({
+ link: "blank-page--no-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ // Inherit the title and URL until finishing loading a new link when the
+ // link is opened in same tab.
+ tab: HOME_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ history: [HOME_URL, BLANK_URL],
+ },
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_new_window.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_new_window.js
new file mode 100644
index 0000000000..6d18887941
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_new_window.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 the behavior of the tab and the urlbar on new window opened by clicking
+// link.
+
+/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
+ this
+);
+
+add_task(async function normal_page__blank_target() {
+ await doTestWithNewWindow({
+ link: "wait-a-bit--blank-target",
+ expectedSetURICalled: true,
+ });
+});
+
+add_task(async function normal_page__other_target() {
+ await doTestWithNewWindow({
+ link: "wait-a-bit--other-target",
+ expectedSetURICalled: false,
+ });
+});
+
+add_task(async function normal_page__by_script() {
+ await doTestWithNewWindow({
+ link: "wait-a-bit--by-script",
+ expectedSetURICalled: false,
+ });
+});
+
+add_task(async function blank_page__blank_target() {
+ await doTestWithNewWindow({
+ link: "blank-page--blank-target",
+ expectedSetURICalled: false,
+ });
+});
+
+add_task(async function blank_page__other_target() {
+ await doTestWithNewWindow({
+ link: "blank-page--other-target",
+ expectedSetURICalled: false,
+ });
+});
+
+add_task(async function blank_page__by_script() {
+ await doTestWithNewWindow({
+ link: "blank-page--by-script",
+ expectedSetURICalled: false,
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js
new file mode 100644
index 0000000000..fa7bd3fa7e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.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/. */
+
+// Test the behavior of the tab and the urlbar when opening normal web page by
+// clicking link that the target is "_blank".
+
+/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
+ this
+);
+
+add_task(async function normal_page__foreground__click() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__foreground__contextmenu() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CONTEXT_MENU,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__foreground__abort() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__foreground__timeout() {
+ await doTestInSameWindow({
+ link: "request-timeout--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: REQUEST_TIMEOUT_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: REQUEST_TIMEOUT_URL,
+ history: [REQUEST_TIMEOUT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__foreground__session_restore() {
+ await doSessionRestoreTest({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ expectedSessionHistory: [WAIT_A_BIT_URL],
+ expectedSessionRestored: true,
+ });
+});
+
+add_task(async function normal_page__background__click() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: HOME_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__background__contextmenu() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CONTEXT_MENU,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: HOME_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__background__abort() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ history: [],
+ },
+ });
+});
+
+add_task(async function normal_page__background__timeout() {
+ await doTestInSameWindow({
+ link: "request-timeout--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ history: [REQUEST_TIMEOUT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__background__session_restore() {
+ await doSessionRestoreTest({
+ link: "wait-a-bit--blank-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ expectedSessionRestored: false,
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js
new file mode 100644
index 0000000000..1284ba675f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 the behavior of the tab and the urlbar when opening normal web page by
+// clicking link that opens by script.
+
+/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
+ this
+);
+
+add_task(async function normal_page__by_script() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--by-script",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__by_script__abort() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--by-script",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ history: [],
+ },
+ });
+});
+
+add_task(async function normal_page__by_script__timeout() {
+ await doTestInSameWindow({
+ link: "request-timeout--by-script",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: REQUEST_TIMEOUT_URL,
+ history: [REQUEST_TIMEOUT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__by_script__session_restore() {
+ await doSessionRestoreTest({
+ link: "wait-a-bit--by-script",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ expectedSessionRestored: false,
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js
new file mode 100644
index 0000000000..87544589d7
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 the behavior of the tab and the urlbar when opening normal web page by
+// clicking link that has no target.
+
+/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
+ this
+);
+
+add_task(async function normal_page__no_target() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--no-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ // Inherit the title and URL until finishing loading a new link when the
+ // link is opened in same tab.
+ tab: HOME_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ history: [HOME_URL, WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__no_target__abort() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--no-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: HOME_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: HOME_TITLE,
+ urlbar: HOME_URL,
+ history: [HOME_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__no_target__timeout() {
+ await doTestInSameWindow({
+ link: "request-timeout--no-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: HOME_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: REQUEST_TIMEOUT_URL,
+ history: [HOME_URL, REQUEST_TIMEOUT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__no_target__session_restore() {
+ await doSessionRestoreTest({
+ link: "wait-a-bit--no-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ expectedSessionRestored: false,
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js
new file mode 100644
index 0000000000..afc647415e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js
@@ -0,0 +1,156 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 the behavior of the tab and the urlbar when opening normal web page by
+// clicking link that the target is "other".
+
+/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
+ this
+);
+
+add_task(async function normal_page__other_target__foreground() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: WAIT_A_BIT_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__other_target__foreground__abort() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ history: [],
+ },
+ });
+});
+
+add_task(async function normal_page__other_target__foreground__timeout() {
+ await doTestInSameWindow({
+ link: "request-timeout--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ loadingState: {
+ tab: BLANK_TITLE,
+ urlbar: BLANK_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: REQUEST_TIMEOUT_URL,
+ history: [REQUEST_TIMEOUT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__foreground__session_restore() {
+ await doSessionRestoreTest({
+ link: "wait-a-bit--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.FOREGROUND,
+ expectedSessionRestored: false,
+ });
+});
+
+add_task(async function normal_page__other_target__background() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: HOME_URL,
+ history: [WAIT_A_BIT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__other_target__background__abort() {
+ await doTestInSameWindow({
+ link: "wait-a-bit--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ history: [],
+ },
+ });
+});
+
+add_task(async function normal_page__other_target__background__timeout() {
+ await doTestInSameWindow({
+ link: "request-timeout--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ loadingState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: HOME_URL,
+ history: [REQUEST_TIMEOUT_URL],
+ },
+ });
+});
+
+add_task(async function normal_page__foreground__session_restore() {
+ await doSessionRestoreTest({
+ link: "wait-a-bit--other-target",
+ openBy: OPEN_BY.CLICK,
+ openAs: OPEN_AS.BACKGROUND,
+ expectedSessionRestored: false,
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js b/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js
new file mode 100644
index 0000000000..db0571a2c0
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that tab labels for base64 data: URLs are always truncated
+ * to ensure that we don't hang trying to paint really long overflown
+ * text runs.
+ * This becomes a performance issue with 1mb or so long data URIs;
+ * this test uses a much shorter one for simplicity's sake.
+ */
+add_task(async function test_ensure_truncation() {
+ const MOBY = `
+ <!DOCTYPE html>
+ <meta charset="utf-8">
+ Call me Ishmael. Some years ago—never mind how
+ long precisely—having little or no money in my purse, and nothing particular
+ to interest me on shore, I thought I would sail about a little and see the
+ watery part of the world. It is a way I have of driving off the spleen and
+ regulating the circulation. Whenever I find myself growing grim about the
+ mouth; whenever it is a damp, drizzly November in my soul; whenever I find
+ myself involuntarily pausing before coffin warehouses, and bringing up the
+ rear of every funeral I meet; and especially whenever my hypos get such an
+ upper hand of me, that it requires a strong moral principle to prevent me
+ from deliberately stepping into the street, and methodically knocking
+ people's hats off—then, I account it high time to get to sea as soon as I
+ can. This is my substitute for pistol and ball. With a philosophical
+ flourish Cato throws himself upon his sword; I quietly take to the ship.
+ There is nothing surprising in this. If they but knew it, almost all men in
+ their degree, some time or other, cherish very nearly the same feelings
+ towards the ocean with me.`;
+
+ let fileReader = new FileReader();
+ const DATA_URL = await new Promise(resolve => {
+ fileReader.addEventListener("load", e => resolve(fileReader.result));
+ fileReader.readAsDataURL(new Blob([MOBY], { type: "text/html" }));
+ });
+ // Substring the full URL to avoid log clutter because Assert will print
+ // the entire thing.
+ Assert.stringContains(
+ DATA_URL.substring(0, 30),
+ "base64",
+ "data URL needs to be base64"
+ );
+
+ let newTab;
+ function tabLabelChecker() {
+ Assert.lessOrEqual(
+ newTab.label.length,
+ 501,
+ "Tab label should not exceed 500 chars + ellipsis."
+ );
+ }
+ let mutationObserver = new MutationObserver(tabLabelChecker);
+ registerCleanupFunction(() => mutationObserver.disconnect());
+
+ gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ event => {
+ newTab = event.target;
+ tabLabelChecker();
+ mutationObserver.observe(newTab, {
+ attributeFilter: ["label"],
+ });
+ },
+ { once: true }
+ );
+
+ await BrowserTestUtils.withNewTab(DATA_URL, async () => {
+ // Wait another longer-than-tick to ensure more mutation observer things have
+ // come in.
+ await new Promise(executeSoon);
+
+ // Check one last time for good measure, for the final label:
+ tabLabelChecker();
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_middle_click_new_tab_button_loads_clipboard.js b/browser/base/content/test/tabs/browser_middle_click_new_tab_button_loads_clipboard.js
new file mode 100644
index 0000000000..07d0be0232
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_middle_click_new_tab_button_loads_clipboard.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const searchclipboardforPref = "browser.tabs.searchclipboardfor.middleclick";
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [searchclipboardforPref, true],
+ // set preloading to false so we can await the new tab being opened.
+ ["browser.newtab.preload", false],
+ ],
+ });
+ NewTabPagePreloading.removePreloadedBrowser(window);
+ // Create an engine to use for the test.
+ SearchTestUtils.init(this);
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://example.org/",
+ search_url_get_params: "q={searchTerms}",
+ },
+ { setAsDefaultPrivate: true }
+ );
+ // We overflow tabs, close all the extra ones.
+ registerCleanupFunction(() => {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ });
+});
+
+add_task(async function middleclick_tabs_newtab_button_with_url_in_clipboard() {
+ var previousTabsLength = gBrowser.tabs.length;
+ info("Previous tabs count is " + previousTabsLength);
+
+ let url = "javascript:https://www.example.com/";
+ let safeUrl = "https://www.example.com/";
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ url,
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(url);
+ },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+
+ info("Middle clicking 'new tab' button");
+ let promiseTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ safeUrl,
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("tabs-newtab-button"),
+ { button: 1 }
+ );
+
+ await promiseTabLoaded;
+ is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab");
+ is(
+ gBrowser.currentURI.spec,
+ safeUrl,
+ "New Tab URL is the safe content of the clipboard"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(
+ async function middleclick_tabs_newtab_button_with_word_in_clipboard() {
+ var previousTabsLength = gBrowser.tabs.length;
+ info("Previous tabs count is " + previousTabsLength);
+
+ let word = "word";
+ let searchUrl = "https://example.org/?q=word";
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ word,
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(word);
+ },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+
+ info("Middle clicking 'new tab' button");
+ let promiseTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ searchUrl,
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("tabs-newtab-button"),
+ { button: 1 }
+ );
+
+ await promiseTabLoaded;
+ is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab");
+ is(
+ gBrowser.currentURI.spec,
+ searchUrl,
+ "New Tab URL is the search engine with the content of the clipboard"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+);
+
+add_task(async function middleclick_new_tab_button_with_url_in_clipboard() {
+ await BrowserTestUtils.overflowTabs(registerCleanupFunction, window);
+ await BrowserTestUtils.waitForCondition(() => {
+ return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
+ });
+
+ var previousTabsLength = gBrowser.tabs.length;
+ info("Previous tabs count is " + previousTabsLength);
+
+ let url = "javascript:https://www.example.com/";
+ let safeUrl = "https://www.example.com/";
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ url,
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(url);
+ },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+
+ info("Middle clicking 'new tab' button");
+ let promiseTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ safeUrl,
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("new-tab-button"),
+ { button: 1 }
+ );
+
+ await promiseTabLoaded;
+ is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab");
+ is(
+ gBrowser.currentURI.spec,
+ safeUrl,
+ "New Tab URL is the safe content of the clipboard"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function middleclick_new_tab_button_with_word_in_clipboard() {
+ var previousTabsLength = gBrowser.tabs.length;
+ info("Previous tabs count is " + previousTabsLength);
+
+ let word = "word";
+ let searchUrl = "https://example.org/?q=word";
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ word,
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(word);
+ },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+
+ info("Middle clicking 'new tab' button");
+ let promiseTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ searchUrl,
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("new-tab-button"),
+ { button: 1 }
+ );
+
+ await promiseTabLoaded;
+ is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab");
+ is(
+ gBrowser.currentURI.spec,
+ searchUrl,
+ "New Tab URL is the search engine with the content of the clipboard"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function middleclick_new_tab_button_with_spaces_in_clipboard() {
+ let spaces = " \n ";
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ spaces,
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(spaces);
+ },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+
+ info("Middle clicking 'new tab' button");
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser);
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("new-tab-button"),
+ { button: 1 }
+ );
+
+ await promiseTabOpened;
+ is(
+ gBrowser.currentURI.spec,
+ "about:newtab",
+ "New Tab URL is the regular new tab page."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_active_tab_selected_by_default.js b/browser/base/content/test/tabs/browser_multiselect_tabs_active_tab_selected_by_default.js
new file mode 100644
index 0000000000..8edf56d3d4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_active_tab_selected_by_default.js
@@ -0,0 +1,52 @@
+add_task(async function multiselectActiveTabByDefault() {
+ const tab1 = await addTab();
+ const tab2 = await addTab();
+ const tab3 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ info("Try multiselecting Tab1 (active) with click+CtrlKey");
+ await triggerClickOn(tab1, { ctrlKey: true });
+
+ is(gBrowser.selectedTab, tab1, "Tab1 is active");
+ ok(
+ !tab1.multiselected,
+ "Tab1 is not multi-selected because we are not in multi-select context yet"
+ );
+ ok(!tab2.multiselected, "Tab2 is not multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero tabs multi-selected");
+
+ info("We multi-select tab1 and tab2 with ctrl key down");
+ await triggerClickOn(tab2, { ctrlKey: true });
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ is(gBrowser.selectedTab, tab1, "Tab1 is active");
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three tabs multi-selected");
+ is(
+ gBrowser.lastMultiSelectedTab,
+ tab3,
+ "Tab3 is the last multi-selected tab"
+ );
+
+ info("Unselect tab1 from multi-selection using ctrlKey");
+
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ triggerClickOn(tab1, { ctrlKey: true })
+ );
+
+ isnot(gBrowser.selectedTab, tab1, "Tab1 is not active anymore");
+ is(gBrowser.selectedTab, tab3, "Tab3 is active");
+ ok(!tab1.multiselected, "Tab1 is not multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two tabs multi-selected");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js b/browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js
new file mode 100644
index 0000000000..a24e72c0bb
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.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/. */
+
+async function addTab_example_com() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/", {
+ skipAnimation: true,
+ });
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return tab;
+}
+
+add_task(async function test() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let menuItemBookmarkTab = document.getElementById("context_bookmarkTab");
+ let menuItemBookmarkSelectedTabs = document.getElementById(
+ "context_bookmarkSelectedTabs"
+ );
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+
+ // Check the context menu with a non-multiselected tab
+ updateTabContextMenu(tab3);
+ is(menuItemBookmarkTab.hidden, false, "Bookmark Tab is visible");
+ is(
+ menuItemBookmarkSelectedTabs.hidden,
+ true,
+ "Bookmark Selected Tabs is hidden"
+ );
+
+ // Check the context menu with a multiselected tab and one unique page in the selection.
+ updateTabContextMenu(tab2);
+ is(menuItemBookmarkTab.hidden, true, "Bookmark Tab is visible");
+ is(
+ menuItemBookmarkSelectedTabs.hidden,
+ false,
+ "Bookmark Selected Tabs is hidden"
+ );
+ is(
+ PlacesCommandHook.uniqueSelectedPages.length,
+ 1,
+ "No more than one unique selected page"
+ );
+
+ info("Add a different page to selection");
+ let tab4 = await addTab_example_com();
+ await triggerClickOn(tab4, { ctrlKey: true });
+
+ ok(tab4.multiselected, "Tab4 is multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ // Check the context menu with a multiselected tab and two unique pages in the selection.
+ updateTabContextMenu(tab2);
+ is(menuItemBookmarkTab.hidden, true, "Bookmark Tab is visible");
+ is(
+ menuItemBookmarkSelectedTabs.hidden,
+ false,
+ "Bookmark Selected Tabs is hidden"
+ );
+ is(
+ PlacesCommandHook.uniqueSelectedPages.length,
+ 2,
+ "More than one unique selected page"
+ );
+
+ for (let tab of [tab1, tab2, tab3, tab4]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_clear_selection_when_tab_switch.js b/browser/base/content/test/tabs/browser_multiselect_tabs_clear_selection_when_tab_switch.js
new file mode 100644
index 0000000000..6e75e29c9a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_clear_selection_when_tab_switch.js
@@ -0,0 +1,33 @@
+add_task(async function test() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ for (let tab of [tab1, tab2, tab3]) {
+ await triggerClickOn(tab, { ctrlKey: true });
+ }
+
+ is(gBrowser.multiSelectedTabsCount, 4, "Four multiselected tabs");
+ is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab");
+
+ info("Un-select the active tab");
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ triggerClickOn(initialTab, { ctrlKey: true })
+ );
+
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+ is(gBrowser.selectedTab, tab3, "Tab3 is the active tab");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Selection cleared after tab-switch");
+ is(gBrowser.selectedTab, tab1, "Tab1 is the active tab");
+
+ for (let tab of [tab1, tab2, tab3]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close.js
new file mode 100644
index 0000000000..2d2295c14a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close.js
@@ -0,0 +1,192 @@
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+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;
+}
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_WARN_ON_CLOSE, false]],
+ });
+});
+
+add_task(async function usingTabCloseButton() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+ is(gBrowser.selectedTab, tab1, "Tab1 is active");
+
+ await triggerClickOn(tab3, { ctrlKey: true });
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+ gBrowser.hideTab(tab3);
+ is(
+ gBrowser.multiSelectedTabsCount,
+ 2,
+ "Two multiselected tabs after hiding one tab"
+ );
+ gBrowser.showTab(tab3);
+ is(
+ gBrowser.multiSelectedTabsCount,
+ 3,
+ "Three multiselected tabs after re-showing hidden tab"
+ );
+ await triggerClickOn(tab3, { ctrlKey: true });
+ is(
+ gBrowser.multiSelectedTabsCount,
+ 2,
+ "Two multiselected tabs after ctrl-clicking multiselected tab"
+ );
+
+ // Closing a tab which is not multiselected
+ let tab4CloseBtn = tab4.closeButton;
+ let tab4Closing = BrowserTestUtils.waitForTabClosing(tab4);
+
+ tab4.mOverCloseButton = true;
+ ok(tab4.mOverCloseButton, "Mouse over tab4 close button");
+ tab4CloseBtn.click();
+ await tab4Closing;
+
+ is(gBrowser.selectedTab, tab1, "Tab1 is active");
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab2.closing, "Tab2 is not closing");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab3.closing, "Tab3 is not closing");
+ ok(tab4.closing, "Tab4 is closing");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ // Closing a selected tab
+ let tab2CloseBtn = tab2.closeButton;
+ tab2.mOverCloseButton = true;
+ let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1);
+ let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
+ tab2CloseBtn.click();
+ await tab1Closing;
+ await tab2Closing;
+
+ ok(tab1.closing, "Tab1 is closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(!tab3.closing, "Tab3 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ BrowserTestUtils.removeTab(tab3);
+});
+
+add_task(async function usingTabContextMenu() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+
+ let menuItemCloseTab = document.getElementById("context_closeTab");
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ // Check the context menu with a non-multiselected tab
+ updateTabContextMenu(tab4);
+ let { args } = document.l10n.getAttributes(menuItemCloseTab);
+ is(args.tabCount, 1, "Close Tab item lists a single tab");
+
+ // Check the context menu with a multiselected tab. We have to actually open
+ // it (not just call `updateTabContextMenu`) in order for
+ // `TabContextMenu.contextTab` to stay non-null when we click an item.
+ let menu = await openTabMenuFor(tab2);
+ ({ args } = document.l10n.getAttributes(menuItemCloseTab));
+ is(args.tabCount, 2, "Close Tab item lists more than one tab");
+
+ let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1);
+ let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
+ menu.activateItem(menuItemCloseTab);
+ await tab1Closing;
+ await tab2Closing;
+
+ ok(tab1.closing, "Tab1 is closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(!tab3.closing, "Tab3 is not closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+});
+
+add_task(async function closeAllMultiselectedMiddleClick() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+ let tab6 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ // Close currently selected tab1
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1);
+ await triggerMiddleClickOn(tab1);
+ await tab1Closing;
+
+ // Close a not currently selected tab2
+ await BrowserTestUtils.switchTab(gBrowser, tab3);
+ let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
+ await triggerMiddleClickOn(tab2);
+ await tab2Closing;
+
+ // Close the not multiselected middle clicked tab6
+ await triggerClickOn(tab4, { ctrlKey: true });
+ await triggerClickOn(tab5, { ctrlKey: true });
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(tab4.multiselected, "Tab4 is multiselected");
+ ok(tab5.multiselected, "Tab5 is multiselected");
+ ok(!tab6.multiselected, "Tab6 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ let tab6Closing = BrowserTestUtils.waitForTabClosing(tab6);
+ await triggerMiddleClickOn(tab6);
+ await tab6Closing;
+
+ // Close multiselected tabs(3, 4, 5)
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(tab4.multiselected, "Tab4 is multiselected");
+ ok(tab5.multiselected, "Tab5 is multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ let tab3Closing = BrowserTestUtils.waitForTabClosing(tab3);
+ let tab4Closing = BrowserTestUtils.waitForTabClosing(tab4);
+ let tab5Closing = BrowserTestUtils.waitForTabClosing(tab5);
+ await triggerMiddleClickOn(tab5);
+ await tab3Closing;
+ await tab4Closing;
+ await tab5Closing;
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js
new file mode 100644
index 0000000000..9214fe00a4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js
@@ -0,0 +1,122 @@
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_WARN_ON_CLOSE, false]],
+ });
+});
+
+add_task(async function withAMultiSelectedTab() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await triggerClickOn(tab1, { ctrlKey: true });
+
+ let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
+ gBrowser.pinTab(tab4);
+ await tab4Pinned;
+
+ ok(initialTab.multiselected, "InitialTab is multiselected");
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(!tab2.multiselected, "Tab2 is not multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(tab4.pinned, "Tab4 is pinned");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+ is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab");
+
+ let closingTabs = [tab2, tab3];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeAllTabsBut(tab1);
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(!initialTab.closing, "InitialTab is not closing");
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(tab3.closing, "Tab3 is closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+ is(gBrowser.selectedTab, initialTab, "InitialTab is still the active tab");
+
+ gBrowser.clearMultiSelectedTabs();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab4);
+});
+
+add_task(async function withNotAMultiSelectedTab() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+ await triggerClickOn(tab5, { ctrlKey: true });
+
+ let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
+ gBrowser.pinTab(tab4);
+ await tab4Pinned;
+
+ let tab5Pinned = BrowserTestUtils.waitForEvent(tab5, "TabPinned");
+ gBrowser.pinTab(tab5);
+ await tab5Pinned;
+
+ ok(!initialTab.multiselected, "InitialTab is not multiselected");
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(tab4.pinned, "Tab4 is pinned");
+ ok(tab5.multiselected, "Tab5 is multiselected");
+ ok(tab5.pinned, "Tab5 is pinned");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+ is(gBrowser.selectedTab, tab1, "Tab1 is the active tab");
+
+ let closingTabs = [tab1, tab2, tab3];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.removeAllTabsBut(initialTab)
+ );
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(!initialTab.closing, "InitialTab is not closing");
+ ok(tab1.closing, "Tab1 is closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(tab3.closing, "Tab3 is closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ ok(!tab5.closing, "Tab5 is not closing");
+ is(
+ gBrowser.multiSelectedTabsCount,
+ 0,
+ "Zero multiselected tabs, selection is cleared"
+ );
+ is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab now");
+
+ for (let tab of [tab4, tab5]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_left.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_left.js
new file mode 100644
index 0000000000..874c161bca
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_left.js
@@ -0,0 +1,131 @@
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_WARN_ON_CLOSE, false]],
+ });
+});
+
+add_task(async function withAMultiSelectedTab() {
+ // don't mess with the original tab
+ let originalTab = gBrowser.selectedTab;
+ gBrowser.pinTab(originalTab);
+
+ let tab0 = await addTab();
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await triggerClickOn(tab4, { ctrlKey: true });
+
+ ok(!tab0.multiselected, "Tab0 is not multiselected");
+ ok(!tab1.multiselected, "Tab1 is not multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(tab4.multiselected, "Tab4 is multiselected");
+ ok(!tab5.multiselected, "Tab5 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ // Tab3 will be closed because tab4 is the contextTab.
+ let closingTabs = [tab0, tab1, tab3];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeTabsToTheStartFrom(tab4);
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(tab0.closing, "Tab0 is closing");
+ ok(tab1.closing, "Tab1 is closing");
+ ok(!tab2.closing, "Tab2 is not closing");
+ ok(tab3.closing, "Tab3 is closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ ok(!tab5.closing, "Tab5 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ // cleanup
+ gBrowser.unpinTab(originalTab);
+ for (let tab of [tab2, tab4, tab5]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function withNotAMultiSelectedTab() {
+ // don't mess with the original tab
+ let originalTab = gBrowser.selectedTab;
+ gBrowser.pinTab(originalTab);
+
+ let tab0 = await addTab();
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab3, { ctrlKey: true });
+ await triggerClickOn(tab5, { ctrlKey: true });
+
+ ok(!tab0.multiselected, "Tab0 is not multiselected");
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(!tab2.multiselected, "Tab2 is not multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(tab5.multiselected, "Tab5 is multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ let closingTabs = [tab0, tab1];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeTabsToTheStartFrom(tab2);
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(tab0.closing, "Tab0 is closing");
+ ok(tab1.closing, "Tab1 is closing");
+ ok(!tab2.closing, "Tab2 is not closing");
+ ok(!tab3.closing, "Tab3 is not closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ ok(!tab5.closing, "Tab5 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 2, "Selection is not cleared");
+
+ closingTabs = [tab2, tab3];
+ tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeTabsToTheStartFrom(tab4);
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(tab2.closing, "Tab2 is closing");
+ ok(tab3.closing, "Tab3 is closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ ok(!tab5.closing, "Tab5 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 0, "Selection is cleared");
+
+ // cleanup
+ gBrowser.unpinTab(originalTab);
+ for (let tab of [tab4, tab5]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js
new file mode 100644
index 0000000000..f145930364
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js
@@ -0,0 +1,113 @@
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_WARN_ON_CLOSE, false]],
+ });
+});
+
+add_task(async function withAMultiSelectedTab() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(!tab2.multiselected, "Tab2 is not multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(!tab5.multiselected, "Tab5 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ // Tab2 will be closed because tab1 is the contextTab.
+ let closingTabs = [tab2, tab4, tab5];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeTabsToTheEndFrom(tab1);
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(!tab3.closing, "Tab3 is not closing");
+ ok(tab4.closing, "Tab4 is closing");
+ ok(tab5.closing, "Tab5 is closing");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ for (let tab of [tab1, tab2, tab3]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function withNotAMultiSelectedTab() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab3, { ctrlKey: true });
+ await triggerClickOn(tab5, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(!tab2.multiselected, "Tab2 is not multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(tab5.multiselected, "Tab5 is multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ let closingTabs = [tab5];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeTabsToTheEndFrom(tab4);
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(!tab2.closing, "Tab2 is not closing");
+ ok(!tab3.closing, "Tab3 is not closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ ok(tab5.closing, "Tab5 is closing");
+ is(gBrowser.multiSelectedTabsCount, 2, "Selection is not cleared");
+
+ closingTabs = [tab3, tab4];
+ tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeTabsToTheEndFrom(tab2);
+
+ for (let promise of tabClosingPromises) {
+ await promise;
+ }
+
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(!tab2.closing, "Tab2 is not closing");
+ ok(tab3.closing, "Tab3 is closing");
+ ok(tab4.closing, "Tab4 is closing");
+ is(gBrowser.multiSelectedTabsCount, 0, "Selection is cleared");
+
+ for (let tab of [tab1, tab2]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_using_shortcuts.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_using_shortcuts.js
new file mode 100644
index 0000000000..da367f6645
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_using_shortcuts.js
@@ -0,0 +1,64 @@
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_WARN_ON_CLOSE, false]],
+ });
+});
+
+add_task(async function using_Ctrl_W() {
+ for (let key of ["w", "VK_F4"]) {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, triggerClickOn(tab1, {}));
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await triggerClickOn(tab2, { ctrlKey: true });
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1);
+ let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
+ let tab3Closing = BrowserTestUtils.waitForTabClosing(tab3);
+
+ EventUtils.synthesizeKey(key, { accelKey: true });
+
+ // On OSX, Cmd+F4 should not close tabs.
+ const shouldBeClosing = key == "w" || AppConstants.platform != "macosx";
+
+ if (shouldBeClosing) {
+ await tab1Closing;
+ await tab2Closing;
+ await tab3Closing;
+ }
+
+ ok(!tab4.closing, "Tab4 is not closing");
+
+ if (shouldBeClosing) {
+ ok(tab1.closing, "Tab1 is closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(tab3.closing, "Tab3 is closing");
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+ } else {
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(!tab2.closing, "Tab2 is not closing");
+ ok(!tab3.closing, "Tab3 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 3, "Still Three multiselected tabs");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ }
+
+ BrowserTestUtils.removeTab(tab4);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js b/browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js
new file mode 100644
index 0000000000..029708560a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js
@@ -0,0 +1,51 @@
+add_task(async function test() {
+ let tab0 = gBrowser.selectedTab;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab1 = await addTab("http://example.com/1");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab2 = await addTab("http://example.com/2");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab3 = await addTab("http://example.com/3");
+ let tabs = [tab0, tab1, tab2, tab3];
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ is(gBrowser.selectedTab, tab1, "Tab1 is active");
+ is(gBrowser.selectedTabs.length, 2, "Two selected tabs");
+ is(gBrowser.visibleTabs.length, 4, "Four tabs in window before copy");
+
+ for (let i of [1, 2]) {
+ ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
+ }
+ for (let i of [0, 3]) {
+ ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected");
+ }
+
+ await dragAndDrop(tab1, tab3, true);
+
+ is(gBrowser.selectedTab, tab1, "tab1 is still active");
+ is(gBrowser.selectedTabs.length, 2, "Two selected tabs");
+ is(gBrowser.visibleTabs.length, 6, "Six tabs in window after copy");
+
+ let tab4 = gBrowser.visibleTabs[4];
+ let tab5 = gBrowser.visibleTabs[5];
+ tabs.push(tab4);
+ tabs.push(tab5);
+
+ for (let i of [1, 2]) {
+ ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
+ }
+ for (let i of [0, 3, 4, 5]) {
+ ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected");
+ }
+
+ await BrowserTestUtils.waitForCondition(() => getUrl(tab4) == getUrl(tab1));
+ await BrowserTestUtils.waitForCondition(() => getUrl(tab5) == getUrl(tab2));
+
+ ok(true, "Tab1 and tab2 are duplicated succesfully");
+
+ for (let tab of tabs.filter(t => t != tab0)) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js b/browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js
new file mode 100644
index 0000000000..42342c889c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js
@@ -0,0 +1,74 @@
+add_task(async function test() {
+ // Disable tab animations
+ gReduceMotionOverride = true;
+
+ // Open Bookmarks Toolbar
+ let bookmarksToolbar = document.getElementById("PersonalToolbar");
+ setToolbarVisibility(bookmarksToolbar, true);
+ ok(!bookmarksToolbar.collapsed, "bookmarksToolbar should be visible now");
+
+ let tab1 = await addTab("http://mochi.test:8888/1");
+ let tab2 = await addTab("http://mochi.test:8888/2");
+ let tab3 = await addTab("http://mochi.test:8888/3");
+ let tab4 = await addTab("http://mochi.test:8888/4");
+ let tab5 = await addTab("http://mochi.test:8888/5");
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await triggerClickOn(tab1, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(!tab5.multiselected, "Tab5 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ // Use getElementsByClassName so the list is live and will update as items change.
+ let currentBookmarks =
+ bookmarksToolbar.getElementsByClassName("bookmark-item");
+ let startBookmarksLength = currentBookmarks.length;
+
+ // The destination element should be a non-folder bookmark
+ let destBookmarkItem = () =>
+ bookmarksToolbar.querySelector(
+ "#PlacesToolbarItems .bookmark-item:not([container])"
+ );
+
+ await EventUtils.synthesizePlainDragAndDrop({
+ srcElement: tab1,
+ destElement: destBookmarkItem(),
+ });
+ await TestUtils.waitForCondition(
+ () => currentBookmarks.length == startBookmarksLength + 2,
+ "waiting for 2 bookmarks"
+ );
+ is(
+ currentBookmarks.length,
+ startBookmarksLength + 2,
+ "Bookmark count should have increased by 2"
+ );
+
+ // Drag non-selection to the bookmarks toolbar
+ startBookmarksLength = currentBookmarks.length;
+ await EventUtils.synthesizePlainDragAndDrop({
+ srcElement: tab3,
+ destElement: destBookmarkItem(),
+ });
+ await TestUtils.waitForCondition(
+ () => currentBookmarks.length == startBookmarksLength + 1,
+ "waiting for 1 bookmark"
+ );
+ is(
+ currentBookmarks.length,
+ startBookmarksLength + 1,
+ "Bookmark count should have increased by 1"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+ BrowserTestUtils.removeTab(tab5);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js b/browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js
new file mode 100644
index 0000000000..d9f5e58669
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js
@@ -0,0 +1,136 @@
+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;
+}
+
+add_task(async function test() {
+ let originalTab = gBrowser.selectedTab;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab1 = await addTab("http://example.com/1");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab2 = await addTab("http://example.com/2");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab3 = await addTab("http://example.com/3");
+
+ let menuItemDuplicateTab = document.getElementById("context_duplicateTab");
+ let menuItemDuplicateTabs = document.getElementById("context_duplicateTabs");
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+
+ // Check the context menu with a multiselected tabs
+ updateTabContextMenu(tab2);
+ is(menuItemDuplicateTab.hidden, true, "Duplicate Tab is hidden");
+ is(menuItemDuplicateTabs.hidden, false, "Duplicate Tabs is visible");
+
+ // Check the context menu with a non-multiselected tab
+ updateTabContextMenu(tab3);
+ is(menuItemDuplicateTab.hidden, false, "Duplicate Tab is visible");
+ is(menuItemDuplicateTabs.hidden, true, "Duplicate Tabs is hidden");
+
+ let newTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/3",
+ true
+ );
+ {
+ let menu = await openTabMenuFor(tab3);
+ menu.activateItem(menuItemDuplicateTab);
+ }
+ let tab4 = await newTabOpened;
+
+ is(
+ getUrl(tab4),
+ getUrl(tab3),
+ "tab4 should have same URL as tab3, where it was duplicated from"
+ );
+
+ // Selection should be cleared after duplication
+ ok(!tab1.multiselected, "Tab1 is not multiselected");
+ ok(!tab2.multiselected, "Tab2 is not multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+
+ is(gBrowser.selectedTab._tPos, tab4._tPos, "Tab4 should be selected");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(!tab2.multiselected, "Tab2 is not multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+
+ // Check the context menu with a non-multiselected tab
+ updateTabContextMenu(tab3);
+ is(menuItemDuplicateTab.hidden, true, "Duplicate Tab is hidden");
+ is(menuItemDuplicateTabs.hidden, false, "Duplicate Tabs is visible");
+
+ // 7 tabs because there was already one open when the test starts.
+ // Can't use BrowserTestUtils.waitForNewTab because waitForNewTab only works
+ // with one tab at a time.
+ let newTabsOpened = TestUtils.waitForCondition(
+ () => gBrowser.visibleTabs.length == 7,
+ "Wait for two tabs to get created"
+ );
+ {
+ let menu = await openTabMenuFor(tab3);
+ menu.activateItem(menuItemDuplicateTabs);
+ }
+ await newTabsOpened;
+ info("Two tabs opened");
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ getUrl(gBrowser.visibleTabs[4]) == "http://example.com/1" &&
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ getUrl(gBrowser.visibleTabs[5]) == "http://example.com/3"
+ );
+ });
+
+ is(
+ originalTab,
+ gBrowser.visibleTabs[0],
+ "Original tab should still be first"
+ );
+ is(tab1, gBrowser.visibleTabs[1], "tab1 should still be second");
+ is(tab2, gBrowser.visibleTabs[2], "tab2 should still be third");
+ is(tab3, gBrowser.visibleTabs[3], "tab3 should still be fourth");
+ is(
+ getUrl(gBrowser.visibleTabs[4]),
+ getUrl(tab1),
+ "the first duplicated tab should be placed next to tab3 and have URL of tab1"
+ );
+ is(
+ getUrl(gBrowser.visibleTabs[5]),
+ getUrl(tab3),
+ "the second duplicated tab should have URL of tab3 and maintain same order"
+ );
+ is(
+ tab4,
+ gBrowser.visibleTabs[6],
+ "tab4 should now be the still be the seventh tab"
+ );
+
+ let tabsToClose = gBrowser.visibleTabs.filter(t => t != originalTab);
+ for (let tab of tabsToClose) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_event.js b/browser/base/content/test/tabs/browser_multiselect_tabs_event.js
new file mode 100644
index 0000000000..992cf75e5e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_event.js
@@ -0,0 +1,220 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+add_task(async function clickWithPrefSet() {
+ let detectUnexpected = true;
+ window.addEventListener("TabMultiSelect", () => {
+ if (detectUnexpected) {
+ ok(false, "Shouldn't get unexpected event");
+ }
+ });
+ async function expectEvent(callback, expectedTabs) {
+ let event = new Promise(resolve => {
+ detectUnexpected = false;
+ window.addEventListener(
+ "TabMultiSelect",
+ () => {
+ detectUnexpected = true;
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ await callback();
+ await event;
+ ok(true, "Got TabMultiSelect event");
+ expectSelected(expectedTabs);
+ // Await some time to ensure no additional event is triggered
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ async function expectNoEvent(callback, expectedTabs) {
+ await callback();
+ expectSelected(expectedTabs);
+ // Await some time to ensure no event is triggered
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ function expectSelected(expected) {
+ let { selectedTabs } = gBrowser;
+ is(selectedTabs.length, expected.length, "Check number of selected tabs");
+ for (
+ let i = 0, n = Math.min(expected.length, selectedTabs.length);
+ i < n;
+ ++i
+ ) {
+ is(selectedTabs[i], expected[i], `Check the selected tab #${i + 1}`);
+ }
+ }
+
+ let initialTab = gBrowser.selectedTab;
+ let tab1, tab2, tab3;
+
+ info("Expect no event when opening tabs");
+ await expectNoEvent(async () => {
+ tab1 = await addTab();
+ tab2 = await addTab();
+ tab3 = await addTab();
+ }, [initialTab]);
+
+ info("Switching tab should trigger event");
+ await expectEvent(async () => {
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ }, [tab1]);
+
+ info("Multiselecting tab with Ctrl+click should trigger event");
+ await expectEvent(async () => {
+ await triggerClickOn(tab2, { ctrlKey: true });
+ }, [tab1, tab2]);
+
+ info("Unselecting tab with Ctrl+click should trigger event");
+ await expectEvent(async () => {
+ await triggerClickOn(tab2, { ctrlKey: true });
+ }, [tab1]);
+
+ info("Multiselecting tabs with Shift+click should trigger event");
+ await expectEvent(async () => {
+ await triggerClickOn(tab3, { shiftKey: true });
+ }, [tab1, tab2, tab3]);
+
+ info("Expect no event if multiselection doesn't change with Shift+click");
+ await expectNoEvent(async () => {
+ await triggerClickOn(tab3, { shiftKey: true });
+ }, [tab1, tab2, tab3]);
+
+ info(
+ "Expect no event if multiselection doesn't change with Ctrl+Shift+click"
+ );
+ await expectNoEvent(async () => {
+ await triggerClickOn(tab2, { ctrlKey: true, shiftKey: true });
+ }, [tab1, tab2, tab3]);
+
+ info(
+ "Expect no event if selected tab doesn't change with gBrowser.selectedTab"
+ );
+ await expectNoEvent(async () => {
+ gBrowser.selectedTab = tab1;
+ }, [tab1, tab2, tab3]);
+
+ info(
+ "Clearing multiselection by switching tab with gBrowser.selectedTab should trigger event"
+ );
+ await expectEvent(async () => {
+ await BrowserTestUtils.switchTab(gBrowser, () => {
+ gBrowser.selectedTab = tab3;
+ });
+ }, [tab3]);
+
+ info(
+ "Click on the active and the only mutliselected tab should not trigger event"
+ );
+ await expectNoEvent(async () => {
+ await triggerClickOn(tab3, {});
+ }, [tab3]);
+
+ info(
+ "Expect no event if selected tab doesn't change with gBrowser.selectedTabs"
+ );
+ gBrowser.selectedTabs = [tab3];
+ expectSelected([tab3]);
+
+ info("Multiselecting tabs with gBrowser.selectedTabs should trigger event");
+ await expectEvent(async () => {
+ gBrowser.selectedTabs = [tab3, tab2, tab1];
+ }, [tab1, tab2, tab3]);
+
+ info(
+ "Expect no event if multiselection doesn't change with gBrowser.selectedTabs"
+ );
+ await expectNoEvent(async () => {
+ gBrowser.selectedTabs = [tab3, tab1, tab2];
+ }, [tab1, tab2, tab3]);
+
+ info("Switching tab with gBrowser.selectedTabs should trigger event");
+ await expectEvent(async () => {
+ gBrowser.selectedTabs = [tab1, tab2, tab3];
+ }, [tab1, tab2, tab3]);
+
+ info(
+ "Unmultiselection tab with removeFromMultiSelectedTabs should trigger event"
+ );
+ await expectEvent(async () => {
+ gBrowser.removeFromMultiSelectedTabs(tab3);
+ }, [tab1, tab2]);
+
+ info("Expect no event if the tab is not multiselected");
+ await expectNoEvent(async () => {
+ gBrowser.removeFromMultiSelectedTabs(tab3);
+ }, [tab1, tab2]);
+
+ info(
+ "Clearing multiselection with clearMultiSelectedTabs should trigger event"
+ );
+ await expectEvent(async () => {
+ gBrowser.clearMultiSelectedTabs();
+ }, [tab1]);
+
+ info("Expect no event if there is no multiselection to clear");
+ await expectNoEvent(async () => {
+ gBrowser.clearMultiSelectedTabs();
+ }, [tab1]);
+
+ info(
+ "Expect no event if clearMultiSelectedTabs counteracts addToMultiSelectedTabs"
+ );
+ await expectNoEvent(async () => {
+ gBrowser.addToMultiSelectedTabs(tab3);
+ gBrowser.clearMultiSelectedTabs();
+ }, [tab1]);
+
+ info(
+ "Multiselecting tab with gBrowser.addToMultiSelectedTabs should trigger event"
+ );
+ await expectEvent(async () => {
+ gBrowser.addToMultiSelectedTabs(tab2);
+ }, [tab1, tab2]);
+
+ info(
+ "Expect no event if addToMultiSelectedTabs counteracts clearMultiSelectedTabs"
+ );
+ await expectNoEvent(async () => {
+ gBrowser.clearMultiSelectedTabs();
+ gBrowser.addToMultiSelectedTabs(tab1);
+ gBrowser.addToMultiSelectedTabs(tab2);
+ }, [tab1, tab2]);
+
+ info(
+ "Expect no event if removeFromMultiSelectedTabs counteracts addToMultiSelectedTabs"
+ );
+ await expectNoEvent(async () => {
+ gBrowser.addToMultiSelectedTabs(tab3);
+ gBrowser.removeFromMultiSelectedTabs(tab3);
+ }, [tab1, tab2]);
+
+ info(
+ "Expect no event if addToMultiSelectedTabs counteracts removeFromMultiSelectedTabs"
+ );
+ await expectNoEvent(async () => {
+ gBrowser.removeFromMultiSelectedTabs(tab2);
+ gBrowser.addToMultiSelectedTabs(tab2);
+ }, [tab1, tab2]);
+
+ info("Multiselection with addRangeToMultiSelectedTabs should trigger event");
+ await expectEvent(async () => {
+ gBrowser.addRangeToMultiSelectedTabs(tab1, tab3);
+ }, [tab1, tab2, tab3]);
+
+ info("Switching to a just multiselected tab should multiselect the old one");
+ await expectEvent(async () => {
+ gBrowser.clearMultiSelectedTabs();
+ }, [tab1]);
+ await expectEvent(async () => {
+ is(tab1.multiselected, false, "tab1 is not multiselected");
+ gBrowser.addToMultiSelectedTabs(tab2);
+ gBrowser.lockClearMultiSelectionOnce();
+ gBrowser.selectedTab = tab2;
+ }, [tab1, tab2]);
+ is(tab1.multiselected, true, "tab1 becomes multiselected");
+
+ detectUnexpected = false;
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_move.js b/browser/base/content/test/tabs/browser_multiselect_tabs_move.js
new file mode 100644
index 0000000000..e5de60ea99
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move.js
@@ -0,0 +1,192 @@
+add_task(async function testMoveStartEnabledClickedFromNonSelectedTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let tabs = [tab2, tab3];
+
+ let menuItemMoveStartTab = document.getElementById("context_moveToStart");
+
+ await triggerClickOn(tab, {});
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab.multiselected, "Tab is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+
+ updateTabContextMenu(tab3);
+ is(menuItemMoveStartTab.disabled, false, "Move Tab to Start is enabled");
+
+ for (let tabToRemove of tabs) {
+ BrowserTestUtils.removeTab(tabToRemove);
+ }
+});
+
+add_task(async function testMoveStartDisabledFromFirstUnpinnedTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+
+ let menuItemMoveStartTab = document.getElementById("context_moveToStart");
+
+ gBrowser.pinTab(tab);
+
+ updateTabContextMenu(tab2);
+ is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
+
+ BrowserTestUtils.removeTab(tab2);
+ gBrowser.unpinTab(tab);
+});
+
+add_task(async function testMoveStartDisabledFromFirstPinnedTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+
+ let menuItemMoveStartTab = document.getElementById("context_moveToStart");
+
+ gBrowser.pinTab(tab);
+
+ updateTabContextMenu(tab);
+ is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
+
+ BrowserTestUtils.removeTab(tab2);
+ gBrowser.unpinTab(tab);
+});
+
+add_task(async function testMoveStartDisabledFromOnlyTab() {
+ let tab = gBrowser.selectedTab;
+
+ let menuItemMoveStartTab = document.getElementById("context_moveToStart");
+
+ updateTabContextMenu(tab);
+ is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
+});
+
+add_task(async function testMoveStartDisabledFromOnlyPinnedTab() {
+ let tab = gBrowser.selectedTab;
+
+ let menuItemMoveStartTab = document.getElementById("context_moveToStart");
+
+ gBrowser.pinTab(tab);
+
+ updateTabContextMenu(tab);
+ is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
+
+ gBrowser.unpinTab(tab);
+});
+
+add_task(async function testMoveStartEnabledFromLastPinnedTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let tabs = [tab2, tab3];
+
+ let menuItemMoveStartTab = document.getElementById("context_moveToStart");
+
+ gBrowser.pinTab(tab);
+ gBrowser.pinTab(tab2);
+
+ updateTabContextMenu(tab2);
+ is(menuItemMoveStartTab.disabled, false, "Move Tab to Start is enabled");
+
+ for (let tabToRemove of tabs) {
+ BrowserTestUtils.removeTab(tabToRemove);
+ }
+
+ gBrowser.unpinTab(tab);
+});
+
+add_task(async function testMoveStartDisabledFromFirstVisibleTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+
+ let menuItemMoveStartTab = document.getElementById("context_moveToStart");
+
+ gBrowser.selectTabAtIndex(1);
+ gBrowser.hideTab(tab);
+
+ updateTabContextMenu(tab2);
+ is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
+
+ BrowserTestUtils.removeTab(tab2);
+ gBrowser.showTab(tab);
+});
+
+add_task(async function testMoveEndEnabledClickedFromNonSelectedTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let tabs = [tab2, tab3];
+
+ let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
+
+ await triggerClickOn(tab2, {});
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+
+ updateTabContextMenu(tab);
+ is(menuItemMoveEndTab.disabled, false, "Move Tab to End is enabled");
+
+ for (let tabToRemove of tabs) {
+ BrowserTestUtils.removeTab(tabToRemove);
+ }
+});
+
+add_task(async function testMoveEndDisabledFromLastPinnedTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let tabs = [tab2, tab3];
+
+ let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
+
+ gBrowser.pinTab(tab);
+ gBrowser.pinTab(tab2);
+
+ updateTabContextMenu(tab2);
+ is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
+
+ for (let tabToRemove of tabs) {
+ BrowserTestUtils.removeTab(tabToRemove);
+ }
+});
+
+add_task(async function testMoveEndDisabledFromLastVisibleTab() {
+ let tab = gBrowser.selectedTab;
+ let tab2 = await addTab();
+
+ let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
+
+ gBrowser.hideTab(tab2);
+
+ updateTabContextMenu(tab);
+ is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
+
+ BrowserTestUtils.removeTab(tab2);
+ gBrowser.showTab(tab);
+});
+
+add_task(async function testMoveEndDisabledFromOnlyTab() {
+ let tab = gBrowser.selectedTab;
+
+ let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
+
+ updateTabContextMenu(tab);
+ is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
+});
+
+add_task(async function testMoveEndDisabledFromOnlyPinnedTab() {
+ let tab = gBrowser.selectedTab;
+
+ let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
+
+ gBrowser.pinTab(tab);
+
+ updateTabContextMenu(tab);
+ is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
+
+ gBrowser.unpinTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js
new file mode 100644
index 0000000000..111221c4ec
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js
@@ -0,0 +1,118 @@
+add_task(async function test() {
+ // Disable tab animations
+ gReduceMotionOverride = true;
+
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab("http://mochi.test:8888/3");
+ let tab4 = await addTab();
+ let tab5 = await addTab("http://mochi.test:8888/5");
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await triggerClickOn(tab1, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(!tab5.multiselected, "Tab5 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ let newWindow = gBrowser.replaceTabsWithWindow(tab1);
+
+ // waiting for tab2 to close ensure that the newWindow is created,
+ // thus newWindow.gBrowser used in the second waitForCondition
+ // will not be undefined.
+ await TestUtils.waitForCondition(
+ () => tab2.closing,
+ "Wait for tab2 to close"
+ );
+ await TestUtils.waitForCondition(
+ () => newWindow.gBrowser.visibleTabs.length == 2,
+ "Wait for all two tabs to get moved to the new window"
+ );
+
+ let gBrowser2 = newWindow.gBrowser;
+ tab1 = gBrowser2.visibleTabs[0];
+ tab2 = gBrowser2.visibleTabs[1];
+
+ if (gBrowser.selectedTab != tab3) {
+ await BrowserTestUtils.switchTab(gBrowser, tab3);
+ }
+
+ await triggerClickOn(tab5, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(tab5.multiselected, "Tab5 is multiselected");
+
+ await dragAndDrop(tab3, tab1, false, newWindow);
+
+ await TestUtils.waitForCondition(
+ () => gBrowser2.visibleTabs.length == 4,
+ "Moved tab3 and tab5 to second window"
+ );
+
+ tab3 = gBrowser2.visibleTabs[1];
+ tab5 = gBrowser2.visibleTabs[2];
+
+ await BrowserTestUtils.waitForCondition(
+ () => getUrl(tab3) == "http://mochi.test:8888/3"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => getUrl(tab5) == "http://mochi.test:8888/5"
+ );
+
+ ok(true, "Tab3 and tab5 are duplicated succesfully");
+
+ BrowserTestUtils.closeWindow(newWindow);
+ BrowserTestUtils.removeTab(tab4);
+});
+
+add_task(async function test_laziness() {
+ const params = { createLazyBrowser: true };
+ const url = "http://mochi.test:8888/?";
+ const tab1 = BrowserTestUtils.addTab(gBrowser, url + "1", params);
+ const tab2 = BrowserTestUtils.addTab(gBrowser, url + "2");
+ const tab3 = BrowserTestUtils.addTab(gBrowser, url + "3", params);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await triggerClickOn(tab1, { ctrlKey: true });
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ is(gBrowser.selectedTab, tab2, "Tab2 is selected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab1.linkedPanel, "Tab1 is lazy");
+ ok(tab2.linkedPanel, "Tab2 is not lazy");
+ ok(!tab3.linkedPanel, "Tab3 is lazy");
+
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const gBrowser2 = win2.gBrowser;
+ is(gBrowser2.tabs.length, 1, "Second window has 1 tab");
+
+ await dragAndDrop(tab2, gBrowser2.tabs[0], false, win2);
+ await TestUtils.waitForCondition(
+ () => gBrowser2.tabs.length == 4,
+ "Moved tabs into second window"
+ );
+ is(gBrowser2.tabs[1].linkedBrowser.currentURI.spec, url + "1");
+ is(gBrowser2.tabs[2].linkedBrowser.currentURI.spec, url + "2");
+ is(gBrowser2.tabs[3].linkedBrowser.currentURI.spec, url + "3");
+ is(gBrowser2.selectedTab, gBrowser2.tabs[2], "Tab2 is selected");
+ is(gBrowser2.multiSelectedTabsCount, 3, "Three multiselected tabs");
+ ok(gBrowser2.tabs[1].multiselected, "Tab1 is multiselected");
+ ok(gBrowser2.tabs[2].multiselected, "Tab2 is multiselected");
+ ok(gBrowser2.tabs[3].multiselected, "Tab3 is multiselected");
+ ok(!gBrowser2.tabs[1].linkedPanel, "Tab1 is lazy");
+ ok(gBrowser2.tabs[2].linkedPanel, "Tab2 is not lazy");
+ ok(!gBrowser2.tabs[3].linkedPanel, "Tab3 is lazy");
+
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js
new file mode 100644
index 0000000000..d668d21df8
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js
@@ -0,0 +1,129 @@
+add_task(async function test() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await triggerClickOn(tab1, { ctrlKey: true });
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ let newWindow = gBrowser.replaceTabsWithWindow(tab1);
+
+ // waiting for tab2 to close ensure that the newWindow is created,
+ // thus newWindow.gBrowser used in the second waitForCondition
+ // will not be undefined.
+ await TestUtils.waitForCondition(
+ () => tab2.closing,
+ "Wait for tab2 to close"
+ );
+ await TestUtils.waitForCondition(
+ () => newWindow.gBrowser.visibleTabs.length == 3,
+ "Wait for all three tabs to get moved to the new window"
+ );
+
+ let gBrowser2 = newWindow.gBrowser;
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+ is(gBrowser.visibleTabs.length, 2, "Two tabs now in the old window");
+ is(gBrowser2.visibleTabs.length, 3, "Three tabs in the new window");
+ is(
+ gBrowser2.visibleTabs.indexOf(gBrowser2.selectedTab),
+ 1,
+ "Previously active tab is still the active tab in the new window"
+ );
+
+ BrowserTestUtils.closeWindow(newWindow);
+ BrowserTestUtils.removeTab(tab4);
+});
+
+add_task(async function testLazyTabs() {
+ let params = { createLazyBrowser: true };
+ let oldTabs = [];
+ let numTabs = 4;
+ for (let i = 0; i < numTabs; ++i) {
+ oldTabs.push(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.addTab(gBrowser, `http://example.com/?${i}`, params)
+ );
+ }
+
+ await BrowserTestUtils.switchTab(gBrowser, oldTabs[0]);
+ for (let i = 1; i < numTabs; ++i) {
+ await triggerClickOn(oldTabs[i], { ctrlKey: true });
+ }
+
+ isnot(oldTabs[0].linkedPanel, "", `Old tab 0 shouldn't be lazy`);
+ for (let i = 1; i < numTabs; ++i) {
+ is(oldTabs[i].linkedPanel, "", `Old tab ${i} should be lazy`);
+ }
+
+ is(gBrowser.multiSelectedTabsCount, numTabs, `${numTabs} multiselected tabs`);
+ for (let i = 0; i < numTabs; ++i) {
+ ok(oldTabs[i].multiselected, `Old tab ${i} should be multiselected`);
+ }
+
+ let tabsMoved = new Promise(resolve => {
+ let numTabsMoved = 0;
+ window.addEventListener("TabClose", async function listener(event) {
+ let oldTab = event.target;
+ let i = oldTabs.indexOf(oldTab);
+ if (i == 0) {
+ isnot(
+ oldTab.linkedPanel,
+ "",
+ `Old tab ${i} should continue not being lazy`
+ );
+ } else if (i > 0) {
+ is(oldTab.linkedPanel, "", `Old tab ${i} should continue being lazy`);
+ } else {
+ return;
+ }
+ let newTab = event.detail.adoptedBy;
+ await TestUtils.waitForCondition(() => {
+ return newTab.linkedBrowser.currentURI.spec != "about:blank";
+ }, `Wait for the new tab to finish the adoption of the old tab`);
+ if (++numTabsMoved == numTabs) {
+ window.removeEventListener("TabClose", listener);
+ resolve();
+ }
+ });
+ });
+ let newWindow = gBrowser.replaceTabsWithWindow(oldTabs[0]);
+ await tabsMoved;
+ let newTabs = newWindow.gBrowser.tabs;
+
+ isnot(newTabs[0].linkedPanel, "", `New tab 0 should continue not being lazy`);
+ for (let i = 1; i < numTabs; ++i) {
+ is(newTabs[i].linkedPanel, "", `New tab ${i} should continue being lazy`);
+ }
+
+ is(
+ newTabs[0].linkedBrowser.currentURI.spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ `http://example.com/?0`,
+ `New tab 0 should have the right URL`
+ );
+ for (let i = 1; i < numTabs; ++i) {
+ is(
+ SessionStore.getLazyTabValue(newTabs[i], "url"),
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ `http://example.com/?${i}`,
+ `New tab ${i} should have the right lazy URL`
+ );
+ }
+
+ for (let i = 0; i < numTabs; ++i) {
+ ok(newTabs[i].multiselected, `New tab ${i} should be multiselected`);
+ }
+
+ BrowserTestUtils.closeWindow(newWindow);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js b/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js
new file mode 100644
index 0000000000..83de966e0c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js
@@ -0,0 +1,336 @@
+const PREF_DELAY_AUTOPLAY = "media.block-autoplay-until-in-foreground";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_DELAY_AUTOPLAY, true]],
+ });
+});
+
+add_task(async function muteTabs_usingButton() {
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+ let tab2 = await addMediaTab();
+ let tab3 = await addMediaTab();
+ let tab4 = await addMediaTab();
+
+ let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+ await BrowserTestUtils.switchTab(gBrowser, tab0);
+ await play(tab0);
+ await play(tab1, false);
+ await play(tab2, false);
+
+ // Multiselecting tab1, tab2 and tab3
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab3, { shiftKey: true });
+
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+ ok(!tab0.multiselected, "Tab0 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+
+ // tab1,tab2 and tab3 should be multiselected.
+ for (let i = 1; i <= 3; i++) {
+ ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
+ }
+
+ // All five tabs are unmuted
+ for (let i = 0; i < 5; i++) {
+ ok(!muted(tabs[i]), "Tab" + i + " is not muted");
+ }
+
+ // Mute tab0 which is not multiselected, thus other tabs muted state should not be affected
+ let tab0MuteAudioBtn = tab0.overlayIcon;
+ await test_mute_tab(tab0, tab0MuteAudioBtn, true);
+
+ ok(muted(tab0), "Tab0 is muted");
+ for (let i = 1; i <= 4; i++) {
+ ok(!muted(tabs[i]), "Tab" + i + " is not muted");
+ }
+
+ // Now we multiselect tab0
+ await triggerClickOn(tab0, { ctrlKey: true });
+
+ // tab0, tab1, tab2, tab3 are multiselected
+ for (let i = 0; i <= 3; i++) {
+ ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+ }
+ ok(!tab4.multiselected, "tab4 is not multiselected");
+
+ // Check mute state
+ ok(muted(tab0), "Tab0 is still muted");
+ ok(!muted(tab1), "Tab1 is not muted");
+ ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked");
+ ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+ ok(!muted(tab3), "Tab3 is not muted");
+ ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked");
+ ok(!muted(tab4), "Tab4 is not muted");
+ ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked");
+
+ // Mute tab1 which is multiselected, thus other multiselected tabs should be affected too
+ // in the following way:
+ // a) muted tabs (tab0) will remain muted.
+ // b) unmuted tabs (tab1, tab3) will become muted.
+ // b) media-blocked tabs (tab2) will remain media-blocked.
+ // However tab4 (unmuted) which is not multiselected should not be affected.
+ let tab1MuteAudioBtn = tab1.overlayIcon;
+ await test_mute_tab(tab1, tab1MuteAudioBtn, true);
+
+ // Check mute state
+ ok(muted(tab0), "Tab0 is still muted");
+ ok(muted(tab1), "Tab1 is muted");
+ ok(activeMediaBlocked(tab2), "Tab2 is still media-blocked");
+ ok(muted(tab3), "Tab3 is now muted");
+ ok(!muted(tab4), "Tab4 is not muted");
+ ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function unmuteTabs_usingButton() {
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+ let tab2 = await addMediaTab();
+ let tab3 = await addMediaTab();
+ let tab4 = await addMediaTab();
+
+ let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+ await BrowserTestUtils.switchTab(gBrowser, tab0);
+ await play(tab0);
+ await play(tab1, false);
+ await play(tab2, false);
+
+ // Mute tab3 and tab4
+ await toggleMuteAudio(tab3, true);
+ await toggleMuteAudio(tab4, true);
+
+ // Multiselecting tab0, tab1, tab2 and tab3
+ await triggerClickOn(tab3, { shiftKey: true });
+
+ // Check multiselection
+ for (let i = 0; i <= 3; i++) {
+ ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+ }
+ ok(!tab4.multiselected, "tab4 is not multiselected");
+
+ // Check tabs mute state
+ ok(!muted(tab0), "Tab0 is not muted");
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+ ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+ ok(muted(tab3), "Tab3 is muted");
+ ok(muted(tab4), "Tab4 is muted");
+ is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+ // unmute tab0 which is multiselected, thus other multiselected tabs should be affected too
+ // in the following way:
+ // a) muted tabs (tab3) will become unmuted.
+ // b) unmuted tabs (tab0) will remain unmuted.
+ // c) media-blocked tabs (tab1, tab2) will remain blocked.
+ // However tab4 (muted) which is not multiselected should not be affected.
+ let tab3MuteAudioBtn = tab3.overlayIcon;
+ await test_mute_tab(tab3, tab3MuteAudioBtn, false);
+
+ ok(!muted(tab0), "Tab0 is not muted");
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(!muted(tab1), "Tab1 is not muted");
+ ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked");
+ ok(!muted(tab2), "Tab2 is not muted");
+ ok(activeMediaBlocked(tab2), "Tab2 is activemedia-blocked");
+ ok(!muted(tab3), "Tab3 is not muted");
+ ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked");
+ ok(muted(tab4), "Tab4 is muted");
+ is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function muteAndUnmuteTabs_usingKeyboard() {
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+ let tab2 = await addMediaTab();
+ let tab3 = await addMediaTab();
+ let tab4 = await addMediaTab();
+
+ let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+ await BrowserTestUtils.switchTab(gBrowser, tab0);
+
+ let mutedPromise = get_wait_for_mute_promise(tab0, true);
+ EventUtils.synthesizeKey("M", { ctrlKey: true });
+ await mutedPromise;
+ ok(muted(tab0), "Tab0 should be muted");
+ ok(!muted(tab1), "Tab1 should not be muted");
+ ok(!muted(tab2), "Tab2 should not be muted");
+ ok(!muted(tab3), "Tab3 should not be muted");
+ ok(!muted(tab4), "Tab4 should not be muted");
+
+ // Multiselecting tab0, tab1, tab2 and tab3
+ await triggerClickOn(tab3, { shiftKey: true });
+
+ // Check multiselection
+ for (let i = 0; i <= 3; i++) {
+ ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+ }
+ ok(!tab4.multiselected, "tab4 is not multiselected");
+
+ mutedPromise = get_wait_for_mute_promise(tab0, false);
+ EventUtils.synthesizeKey("M", { ctrlKey: true });
+ await mutedPromise;
+ ok(!muted(tab0), "Tab0 should not be muted");
+ ok(!muted(tab1), "Tab1 should not be muted");
+ ok(!muted(tab2), "Tab2 should not be muted");
+ ok(!muted(tab3), "Tab3 should not be muted");
+ ok(!muted(tab4), "Tab4 should not be muted");
+
+ mutedPromise = get_wait_for_mute_promise(tab0, true);
+ EventUtils.synthesizeKey("M", { ctrlKey: true });
+ await mutedPromise;
+ ok(muted(tab0), "Tab0 should be muted");
+ ok(muted(tab1), "Tab1 should be muted");
+ ok(muted(tab2), "Tab2 should be muted");
+ ok(muted(tab3), "Tab3 should be muted");
+ ok(!muted(tab4), "Tab4 should not be muted");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function playTabs_usingButton() {
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+ let tab2 = await addMediaTab();
+ let tab3 = await addMediaTab();
+ let tab4 = await addMediaTab();
+
+ let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+ await BrowserTestUtils.switchTab(gBrowser, tab0);
+ await play(tab0);
+ await play(tab1, false);
+ await play(tab2, false);
+
+ // Multiselecting tab0, tab1, tab2 and tab3.
+ await triggerClickOn(tab3, { shiftKey: true });
+
+ // Mute tab0 and tab4
+ await toggleMuteAudio(tab0, true);
+ await toggleMuteAudio(tab4, true);
+
+ // Check multiselection
+ for (let i = 0; i <= 3; i++) {
+ ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+ }
+ ok(!tab4.multiselected, "tab4 is not multiselected");
+
+ // Check mute state
+ ok(muted(tab0), "Tab0 is muted");
+ ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+ ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+ ok(!muted(tab3), "Tab3 is not muted");
+ ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked");
+ ok(muted(tab4), "Tab4 is muted");
+ is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+ // play tab2 which is multiselected, thus other multiselected tabs should be affected too
+ // in the following way:
+ // a) muted tabs (tab0) will remain muted.
+ // b) unmuted tabs (tab3) will remain unmuted.
+ // c) media-blocked tabs (tab1, tab2) will become unblocked.
+ // However tab4 (muted) which is not multiselected should not be affected.
+ let tab2MuteAudioBtn = tab2.overlayIcon;
+ await test_mute_tab(tab2, tab2MuteAudioBtn, false);
+
+ ok(muted(tab0), "Tab0 is muted");
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(!muted(tab1), "Tab1 is not muted");
+ ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked");
+ ok(!muted(tab2), "Tab2 is not muted");
+ ok(!activeMediaBlocked(tab2), "Tab2 is not activemedia-blocked");
+ ok(!muted(tab3), "Tab3 is not muted");
+ ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked");
+ ok(muted(tab4), "Tab4 is muted");
+ is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function checkTabContextMenu() {
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+ let tab2 = await addMediaTab();
+ let tab3 = await addMediaTab();
+ let tabs = [tab0, tab1, tab2, tab3];
+
+ let menuItemToggleMuteTab = document.getElementById("context_toggleMuteTab");
+ let menuItemToggleMuteSelectedTabs = document.getElementById(
+ "context_toggleMuteSelectedTabs"
+ );
+
+ await play(tab0, false);
+ await toggleMuteAudio(tab0, true);
+ await play(tab1, false);
+ await toggleMuteAudio(tab2, true);
+
+ // multiselect tab0, tab1, tab2.
+ await triggerClickOn(tab0, { ctrlKey: true });
+ await triggerClickOn(tab1, { ctrlKey: true });
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ // Check multiselected tabs
+ for (let i = 0; i <= 2; i++) {
+ ok(tabs[i].multiselected, "Tab" + i + " is multi-selected");
+ }
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+
+ // Check mute state for tabs
+ ok(muted(tab0), "Tab0 is muted");
+ ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked");
+ ok(muted(tab2), "Tab2 is muted");
+ ok(!muted(tab3, "Tab3 is not muted"));
+
+ const l10nIds = [
+ "tabbrowser-context-unmute-selected-tabs",
+ "tabbrowser-context-mute-selected-tabs",
+ "tabbrowser-context-unmute-selected-tabs",
+ ];
+
+ for (let i = 0; i <= 2; i++) {
+ updateTabContextMenu(tabs[i]);
+ ok(
+ menuItemToggleMuteTab.hidden,
+ "toggleMuteAudio menu for one tab is hidden - contextTab" + i
+ );
+ ok(
+ !menuItemToggleMuteSelectedTabs.hidden,
+ "toggleMuteAudio menu for selected tab is not hidden - contextTab" + i
+ );
+ is(
+ menuItemToggleMuteSelectedTabs.dataset.l10nId,
+ l10nIds[i],
+ l10nIds[i] + " should be shown"
+ );
+ }
+
+ updateTabContextMenu(tab3);
+ ok(
+ !menuItemToggleMuteTab.hidden,
+ "toggleMuteAudio menu for one tab is not hidden"
+ );
+ ok(
+ menuItemToggleMuteSelectedTabs.hidden,
+ "toggleMuteAudio menu for selected tab is hidden"
+ );
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js
new file mode 100644
index 0000000000..7751c9c420
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js
@@ -0,0 +1,143 @@
+add_task(async function test() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab1 = await addTab("http://example.com/1");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab2 = await addTab("http://example.com/2");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab3 = await addTab("http://example.com/3");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+
+ let metaKeyEvent =
+ AppConstants.platform == "macosx" ? { metaKey: true } : { ctrlKey: true };
+
+ let newTabButton = gBrowser.tabContainer.newTabButton;
+ let promiseTabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent);
+ let openEvent = await promiseTabOpened;
+ let newTab = openEvent.target;
+
+ is(
+ newTab.previousElementSibling,
+ tab1,
+ "New tab should be opened after the selected tab (tab1)"
+ );
+ is(
+ newTab.nextElementSibling,
+ tab2,
+ "New tab should be opened after the selected tab (tab1) and before tab2"
+ );
+ BrowserTestUtils.removeTab(newTab);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ ok(!tab1.multiselected, "Tab1 is not multi-selected");
+ ok(!tab2.multiselected, "Tab2 is not multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+
+ promiseTabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent);
+ openEvent = await promiseTabOpened;
+ newTab = openEvent.target;
+ is(
+ newTab.previousElementSibling,
+ tab1,
+ "New tab should be opened after tab1 when only tab1 is selected"
+ );
+ is(
+ newTab.nextElementSibling,
+ tab2,
+ "New tab should be opened before tab2 when only tab1 is selected"
+ );
+ BrowserTestUtils.removeTab(newTab);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab3, { ctrlKey: true });
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(!tab2.multiselected, "Tab2 is not multi-selected");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+
+ promiseTabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent);
+ openEvent = await promiseTabOpened;
+ newTab = openEvent.target;
+ let previous = gBrowser.tabContainer.findNextTab(newTab, { direction: -1 });
+ is(previous, tab1, "New tab should be opened after the selected tab (tab1)");
+ let next = gBrowser.tabContainer.findNextTab(newTab, { direction: 1 });
+ is(
+ next,
+ tab2,
+ "New tab should be opened after the selected tab (tab1) and before tab2"
+ );
+ BrowserTestUtils.removeTab(newTab);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ ok(!tab1.multiselected, "Tab1 is not multi-selected");
+ ok(!tab2.multiselected, "Tab2 is not multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+
+ promiseTabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeMouseAtCenter(newTabButton, {});
+ openEvent = await promiseTabOpened;
+ newTab = openEvent.target;
+ previous = gBrowser.tabContainer.findNextTab(newTab, { direction: -1 });
+ is(
+ previous,
+ tab3,
+ "New tab should be opened after tab3 when ctrlKey is not used without multiselection"
+ );
+ next = gBrowser.tabContainer.findNextTab(newTab, { direction: 1 });
+ is(
+ next,
+ null,
+ "New tab should be opened at the end of the tabstrip when ctrlKey is not used without multiselection"
+ );
+ BrowserTestUtils.removeTab(newTab);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+
+ promiseTabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeMouseAtCenter(newTabButton, {});
+ openEvent = await promiseTabOpened;
+ newTab = openEvent.target;
+ previous = gBrowser.tabContainer.findNextTab(newTab, { direction: -1 });
+ is(
+ previous,
+ tab3,
+ "New tab should be opened after tab3 when ctrlKey is not used with multiselection"
+ );
+ next = gBrowser.tabContainer.findNextTab(newTab, { direction: 1 });
+ is(
+ next,
+ null,
+ "New tab should be opened at the end of the tabstrip when ctrlKey is not used with multiselection"
+ );
+ BrowserTestUtils.removeTab(newTab);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_pin_unpin.js b/browser/base/content/test/tabs/browser_multiselect_tabs_pin_unpin.js
new file mode 100644
index 0000000000..5cd71abbbe
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_pin_unpin.js
@@ -0,0 +1,75 @@
+add_task(async function test() {
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let menuItemPinTab = document.getElementById("context_pinTab");
+ let menuItemUnpinTab = document.getElementById("context_unpinTab");
+ let menuItemPinSelectedTabs = document.getElementById(
+ "context_pinSelectedTabs"
+ );
+ let menuItemUnpinSelectedTabs = document.getElementById(
+ "context_unpinSelectedTabs"
+ );
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+
+ // Check the context menu with a non-multiselected tab
+ updateTabContextMenu(tab3);
+ ok(!tab3.pinned, "Tab3 is unpinned");
+ is(menuItemPinTab.hidden, false, "Pin Tab is visible");
+ is(menuItemUnpinTab.hidden, true, "Unpin Tab is hidden");
+ is(menuItemPinSelectedTabs.hidden, true, "Pin Selected Tabs is hidden");
+ is(menuItemUnpinSelectedTabs.hidden, true, "Unpin Selected Tabs is hidden");
+
+ // Check the context menu with a multiselected and unpinned tab
+ updateTabContextMenu(tab2);
+ ok(!tab2.pinned, "Tab2 is unpinned");
+ is(menuItemPinTab.hidden, true, "Pin Tab is hidden");
+ is(menuItemUnpinTab.hidden, true, "Unpin Tab is hidden");
+ is(menuItemPinSelectedTabs.hidden, false, "Pin Selected Tabs is visible");
+ is(menuItemUnpinSelectedTabs.hidden, true, "Unpin Selected Tabs is hidden");
+
+ let tab1Pinned = BrowserTestUtils.waitForEvent(tab1, "TabPinned");
+ let tab2Pinned = BrowserTestUtils.waitForEvent(tab2, "TabPinned");
+ menuItemPinSelectedTabs.click();
+ await tab1Pinned;
+ await tab2Pinned;
+
+ ok(tab1.pinned, "Tab1 is pinned");
+ ok(tab2.pinned, "Tab2 is pinned");
+ ok(!tab3.pinned, "Tab3 is unpinned");
+ is(tab1._tPos, 0, "Tab1 should still be first after pinning");
+ is(tab2._tPos, 1, "Tab2 should still be second after pinning");
+ is(tab3._tPos, 2, "Tab3 should still be third after pinning");
+
+ // Check the context menu with a multiselected and pinned tab
+ updateTabContextMenu(tab2);
+ ok(tab2.pinned, "Tab2 is pinned");
+ is(menuItemPinTab.hidden, true, "Pin Tab is hidden");
+ is(menuItemUnpinTab.hidden, true, "Unpin Tab is hidden");
+ is(menuItemPinSelectedTabs.hidden, true, "Pin Selected Tabs is hidden");
+ is(menuItemUnpinSelectedTabs.hidden, false, "Unpin Selected Tabs is visible");
+
+ let tab1Unpinned = BrowserTestUtils.waitForEvent(tab1, "TabUnpinned");
+ let tab2Unpinned = BrowserTestUtils.waitForEvent(tab2, "TabUnpinned");
+ menuItemUnpinSelectedTabs.click();
+ await tab1Unpinned;
+ await tab2Unpinned;
+
+ ok(!tab1.pinned, "Tab1 is unpinned");
+ ok(!tab2.pinned, "Tab2 is unpinned");
+ ok(!tab3.pinned, "Tab3 is unpinned");
+ is(tab1._tPos, 0, "Tab1 should still be first after unpinning");
+ is(tab2._tPos, 1, "Tab2 should still be second after unpinning");
+ is(tab3._tPos, 2, "Tab3 should still be third after unpinning");
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_play.js b/browser/base/content/test/tabs/browser_multiselect_tabs_play.js
new file mode 100644
index 0000000000..281ed50c1b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_play.js
@@ -0,0 +1,254 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Ensure multiselected tabs that are active media blocked act correctly
+ * when we try to unblock them using the "Play Tabs" icon or by calling
+ * resumeDelayedMediaOnMultiSelectedTabs()
+ */
+
+"use strict";
+
+const PREF_DELAY_AUTOPLAY = "media.block-autoplay-until-in-foreground";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_DELAY_AUTOPLAY, true]],
+ });
+});
+
+/*
+ * Playing blocked media will not mute the selected tabs
+ */
+add_task(async function testDelayPlayWontAffectUnmuteStatus() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+
+ info("Play both tabs");
+ await play(tab0, false);
+ await play(tab1, false);
+
+ info("Multiselect tabs");
+ await triggerClickOn(tab1, { shiftKey: true });
+
+ // Check multiselection
+ ok(tab0.multiselected, "tab0 is multiselected");
+ ok(tab1.multiselected, "tab1 is multiselected");
+
+ // Check tabs are unmuted
+ ok(!muted(tab0), "Tab0 is unmuted");
+ ok(!muted(tab1), "Tab1 is unmuted");
+
+ let tab0BlockPromise = wait_for_tab_media_blocked_event(tab0, false);
+ let tab1BlockPromise = wait_for_tab_media_blocked_event(tab1, false);
+
+ // Play media on selected tabs
+ gBrowser.resumeDelayedMediaOnMultiSelectedTabs();
+
+ info("Wait for media to play");
+ await tab0BlockPromise;
+ await tab1BlockPromise;
+
+ // Check tabs are still unmuted
+ ok(!muted(tab0), "Tab0 is unmuted");
+ ok(!muted(tab1), "Tab1 is unmuted");
+
+ BrowserTestUtils.removeTab(tab0);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/*
+ * Playing blocked media will not unmute the selected tabs
+ */
+add_task(async function testDelayPlayWontAffectMuteStatus() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+
+ info("Play both tabs");
+ await play(tab0, false);
+ await play(tab1, false);
+
+ // Mute both tabs
+ toggleMuteAudio(tab0, true);
+ toggleMuteAudio(tab1, true);
+
+ info("Multiselect tabs");
+ await triggerClickOn(tab1, { shiftKey: true });
+
+ // Check multiselection
+ ok(tab0.multiselected, "tab0 is multiselected");
+ ok(tab1.multiselected, "tab1 is multiselected");
+
+ // Check tabs are muted
+ ok(muted(tab0), "Tab0 is muted");
+ ok(muted(tab1), "Tab1 is muted");
+
+ let tab0BlockPromise = wait_for_tab_media_blocked_event(tab0, false);
+ let tab1BlockPromise = wait_for_tab_media_blocked_event(tab1, false);
+
+ // Play media on selected tabs
+ gBrowser.resumeDelayedMediaOnMultiSelectedTabs();
+
+ info("Wait for media to play");
+ await tab0BlockPromise;
+ await tab1BlockPromise;
+
+ // Check tabs are still muted
+ ok(muted(tab0), "Tab0 is muted");
+ ok(muted(tab1), "Tab1 is muted");
+
+ BrowserTestUtils.removeTab(tab0);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/*
+ * The "Play Tabs" icon unblocks media
+ */
+add_task(async function testDelayPlayWhenUsingButton() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+ let tab2 = await addMediaTab();
+ let tab3 = await addMediaTab();
+ let tab4 = await addMediaTab();
+
+ let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+ // All tabs are initially unblocked due to not being played yet
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab2), "Tab2 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked");
+
+ // Playing tabs 0, 1, and 2 will block them
+ info("Play tabs 0, 1, and 2");
+ await play(tab0, false);
+ await play(tab1, false);
+ await play(tab2, false);
+ ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked");
+ ok(activeMediaBlocked(tab2), "Tab2 is activemedia-blocked");
+
+ // tab3 and tab4 are still unblocked
+ ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked");
+
+ // Multiselect tab0, tab1, tab2, and tab3.
+ info("Multiselect tabs");
+ await triggerClickOn(tab3, { shiftKey: true });
+
+ // Check multiselection
+ for (let i = 0; i <= 3; i++) {
+ ok(tabs[i].multiselected, `tab${i} is multiselected`);
+ }
+ ok(!tab4.multiselected, "tab4 is not multiselected");
+
+ let tab0BlockPromise = wait_for_tab_media_blocked_event(tab0, false);
+ let tab1BlockPromise = wait_for_tab_media_blocked_event(tab1, false);
+ let tab2BlockPromise = wait_for_tab_media_blocked_event(tab2, false);
+
+ // Use the overlay icon on tab2 to play media on the selected tabs
+ info("Press play tab2 icon");
+ await pressIcon(tab2.overlayIcon);
+
+ // tab0, tab1, and tab2 were played and multiselected
+ // They will now be unblocked and playing media
+ info("Wait for tabs to play");
+ await tab0BlockPromise;
+ await tab1BlockPromise;
+ await tab2BlockPromise;
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab2), "Tab2 is not activemedia-blocked");
+ // tab3 was also multiselected but never played
+ // It will be unblocked but not playing media
+ ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked");
+ // tab4 was not multiselected and was never played
+ // It remains in its original state
+ ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+/*
+ * Tab context menus have to show the menu icons "Play Tab" or "Play Tabs"
+ * depending on the number of tabs selected, and whether blocked media is present
+ */
+add_task(async function testTabContextMenu() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+ let tab2 = await addMediaTab();
+ let tab3 = await addMediaTab();
+ let tabs = [tab0, tab1, tab2, tab3];
+
+ let menuItemPlayTab = document.getElementById("context_playTab");
+ let menuItemPlaySelectedTabs = document.getElementById(
+ "context_playSelectedTabs"
+ );
+
+ // Multiselect tab0, tab1, and tab2.
+ info("Multiselect tabs");
+ await triggerClickOn(tab0, { ctrlKey: true });
+ await triggerClickOn(tab1, { ctrlKey: true });
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ // Check multiselected tabs
+ for (let i = 0; i <= 2; i++) {
+ ok(tabs[i].multiselected, `tab${i} is multi-selected`);
+ }
+ ok(!tab3.multiselected, "tab3 is not multiselected");
+
+ // No active media yet:
+ // - "Play Tab" is hidden
+ // - "Play Tabs" is hidden
+ for (let i = 0; i <= 2; i++) {
+ updateTabContextMenu(tabs[i]);
+ ok(menuItemPlayTab.hidden, `tab${i} "Play Tab" is hidden`);
+ ok(menuItemPlaySelectedTabs.hidden, `tab${i} "Play Tabs" is hidden`);
+ ok(!activeMediaBlocked(tabs[i]), `tab${i} is not active media blocked`);
+ }
+
+ info("Play tabs 0, 1, and 2");
+ await play(tab0, false);
+ await play(tab1, false);
+ await play(tab2, false);
+
+ // Active media blocked:
+ // - "Play Tab" is hidden
+ // - "Play Tabs" is visible
+ for (let i = 0; i <= 2; i++) {
+ updateTabContextMenu(tabs[i]);
+ ok(menuItemPlayTab.hidden, `tab${i} "Play Tab" is hidden`);
+ ok(!menuItemPlaySelectedTabs.hidden, `tab${i} "Play Tabs" is visible`);
+ ok(activeMediaBlocked(tabs[i]), `tab${i} is active media blocked`);
+ }
+
+ info("Play Media on tabs 0, 1, and 2");
+ gBrowser.resumeDelayedMediaOnMultiSelectedTabs();
+
+ // Active media is unblocked:
+ // - "Play Tab" is hidden
+ // - "Play Tabs" is hidden
+ for (let i = 0; i <= 2; i++) {
+ updateTabContextMenu(tabs[i]);
+ ok(menuItemPlayTab.hidden, `tab${i} "Play Tab" is hidden`);
+ ok(menuItemPlaySelectedTabs.hidden, `tab${i} "Play Tabs" is hidden`);
+ ok(!activeMediaBlocked(tabs[i]), `tab${i} is not active media blocked`);
+ }
+
+ // tab3 is untouched
+ updateTabContextMenu(tab3);
+ ok(menuItemPlayTab.hidden, 'tab3 "Play Tab" is hidden');
+ ok(menuItemPlaySelectedTabs.hidden, 'tab3 "Play Tabs" is hidden');
+ ok(!activeMediaBlocked(tab3), "tab3 is not active media blocked");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_reload.js b/browser/base/content/test/tabs/browser_multiselect_tabs_reload.js
new file mode 100644
index 0000000000..7a68fd66d5
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reload.js
@@ -0,0 +1,82 @@
+async function tabLoaded(tab) {
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return true;
+}
+
+add_task(async function test_usingTabContextMenu() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let menuItemReloadTab = document.getElementById("context_reloadTab");
+ let menuItemReloadSelectedTabs = document.getElementById(
+ "context_reloadSelectedTabs"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+
+ updateTabContextMenu(tab3);
+ is(menuItemReloadTab.hidden, false, "Reload Tab is visible");
+ is(menuItemReloadSelectedTabs.hidden, true, "Reload Tabs is hidden");
+
+ updateTabContextMenu(tab2);
+ is(menuItemReloadTab.hidden, true, "Reload Tab is hidden");
+ is(menuItemReloadSelectedTabs.hidden, false, "Reload Tabs is visible");
+
+ let tab1Loaded = tabLoaded(tab1);
+ let tab2Loaded = tabLoaded(tab2);
+ menuItemReloadSelectedTabs.click();
+ await tab1Loaded;
+ await tab2Loaded;
+
+ // We got here because tab1 and tab2 are reloaded. Otherwise the test would have timed out and failed.
+ ok(true, "Tab1 and Tab2 are reloaded");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
+
+add_task(async function test_usingKeyboardShortcuts() {
+ let keys = [
+ ["R", { accelKey: true }],
+ ["R", { accelKey: true, shift: true }],
+ ["VK_F5", {}],
+ ];
+
+ if (AppConstants.platform != "macosx") {
+ keys.push(["VK_F5", { accelKey: true }]);
+ }
+
+ for (let key of keys) {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+
+ let tab1Loaded = tabLoaded(tab1);
+ let tab2Loaded = tabLoaded(tab2);
+ EventUtils.synthesizeKey(key[0], key[1]);
+ await tab1Loaded;
+ await tab2Loaded;
+
+ // We got here because tab1 and tab2 are reloaded. Otherwise the test would have timed out and failed.
+ ok(true, "Tab1 and Tab2 are reloaded");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js b/browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js
new file mode 100644
index 0000000000..0c9c913844
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js
@@ -0,0 +1,133 @@
+"use strict";
+
+const PREF_PRIVACY_USER_CONTEXT_ENABLED = "privacy.userContext.enabled";
+
+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;
+}
+
+async function openReopenMenuForTab(tab) {
+ await openTabMenuFor(tab);
+
+ let reopenItem = tab.ownerDocument.getElementById(
+ "context_reopenInContainer"
+ );
+ ok(!reopenItem.hidden, "Reopen in Container item should be shown");
+
+ let reopenMenu = reopenItem.getElementsByTagName("menupopup")[0];
+ let reopenMenuShown = BrowserTestUtils.waitForEvent(reopenMenu, "popupshown");
+ reopenItem.openMenu(true);
+ await reopenMenuShown;
+
+ return reopenMenu;
+}
+
+function checkMenuItem(reopenMenu, shown, hidden) {
+ for (let id of shown) {
+ ok(
+ reopenMenu.querySelector(`menuitem[data-usercontextid="${id}"]`),
+ `User context id ${id} should exist`
+ );
+ }
+ for (let id of hidden) {
+ ok(
+ !reopenMenu.querySelector(`menuitem[data-usercontextid="${id}"]`),
+ `User context id ${id} shouldn't exist`
+ );
+ }
+}
+
+function openTabInContainer(gBrowser, tab, reopenMenu, id) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, getUrl(tab), true);
+ let menuitem = reopenMenu.querySelector(
+ `menuitem[data-usercontextid="${id}"]`
+ );
+ reopenMenu.activateItem(menuitem);
+ return tabPromise;
+}
+
+add_task(async function testReopen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_PRIVACY_USER_CONTEXT_ENABLED, true]],
+ });
+
+ let tab1 = await addTab("http://mochi.test:8888/1");
+ let tab2 = await addTab("http://mochi.test:8888/2");
+ let tab3 = await addTab("http://mochi.test:8888/3");
+ let tab4 = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/3", {
+ createLazyBrowser: true,
+ });
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ await triggerClickOn(tab2, { ctrlKey: true });
+ await triggerClickOn(tab4, { ctrlKey: true });
+
+ for (let tab of [tab1, tab2, tab3, tab4]) {
+ ok(
+ !tab.hasAttribute("usercontextid"),
+ "Tab with No Container should be opened"
+ );
+ }
+
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected");
+ ok(!tab3.multiselected, "Tab3 is not multi-selected");
+ ok(tab4.multiselected, "Tab4 is multi-selected");
+
+ is(gBrowser.visibleTabs.length, 5, "We have 5 tabs open");
+
+ let reopenMenu1 = await openReopenMenuForTab(tab1);
+ checkMenuItem(reopenMenu1, [1, 2, 3, 4], [0]);
+ let containerTab1 = await openTabInContainer(
+ gBrowser,
+ tab1,
+ reopenMenu1,
+ "1"
+ );
+
+ let tabs = gBrowser.visibleTabs;
+ is(tabs.length, 8, "Now we have 8 tabs open");
+
+ is(containerTab1._tPos, 2, "containerTab1 position is 3");
+ is(
+ containerTab1.getAttribute("usercontextid"),
+ "1",
+ "Tab(1) with UCI=1 should be opened"
+ );
+ is(getUrl(containerTab1), getUrl(tab1), "Same page (tab1) should be opened");
+
+ let containerTab2 = tabs[4];
+ is(
+ containerTab2.getAttribute("usercontextid"),
+ "1",
+ "Tab(2) with UCI=1 should be opened"
+ );
+ await TestUtils.waitForCondition(function () {
+ return getUrl(containerTab2) == getUrl(tab2);
+ }, "Same page (tab2) should be opened");
+
+ let containerTab4 = tabs[7];
+ is(
+ containerTab2.getAttribute("usercontextid"),
+ "1",
+ "Tab(4) with UCI=1 should be opened"
+ );
+ await TestUtils.waitForCondition(function () {
+ return getUrl(containerTab4) == getUrl(tab4);
+ }, "Same page (tab4) should be opened");
+
+ for (let tab of tabs.filter(t => t != tabs[0])) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js b/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js
new file mode 100644
index 0000000000..c3b3356608
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ // Disable tab animations
+ gReduceMotionOverride = true;
+
+ let tab0 = gBrowser.selectedTab;
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+ let tabs = [tab0, tab1, tab2, tab3, tab4, tab5];
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab3, { ctrlKey: true });
+ await triggerClickOn(tab5, { ctrlKey: true });
+
+ is(gBrowser.selectedTab, tab1, "Tab1 is active");
+ is(gBrowser.selectedTabs.length, 3, "Three selected tabs");
+
+ for (let i of [1, 3, 5]) {
+ ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
+ }
+ for (let i of [0, 2, 4]) {
+ ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected");
+ }
+ for (let i of [0, 1, 2, 3, 4, 5]) {
+ is(tabs[i]._tPos, i, "Tab" + i + " position is :" + i);
+ }
+
+ await dragAndDrop(tab3, tab4, false);
+
+ is(gBrowser.selectedTab, tab3, "Dragged tab (tab3) is now active");
+ is(gBrowser.selectedTabs.length, 3, "Three selected tabs");
+
+ for (let i of [1, 3, 5]) {
+ ok(tabs[i].multiselected, "Tab" + i + " is still multiselected");
+ }
+ for (let i of [0, 2, 4]) {
+ ok(!tabs[i].multiselected, "Tab" + i + " is still not multiselected");
+ }
+
+ is(tab0._tPos, 0, "Tab0 position (0) doesn't change");
+
+ // Multiselected tabs gets grouped at the start of the slide.
+ is(
+ tab1._tPos,
+ tab3._tPos - 1,
+ "Tab1 is located right at the left of the dragged tab (tab3)"
+ );
+ is(
+ tab5._tPos,
+ tab3._tPos + 1,
+ "Tab5 is located right at the right of the dragged tab (tab3)"
+ );
+ is(tab3._tPos, 4, "Dragged tab (tab3) position is 4");
+
+ is(tab4._tPos, 2, "Drag target (tab4) has shifted to position 2");
+
+ for (let tab of tabs.filter(t => t != tab0)) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js
new file mode 100644
index 0000000000..93a14a87a7
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js
@@ -0,0 +1,60 @@
+add_task(async function click() {
+ const initialFocusedTab = await addTab();
+ await BrowserTestUtils.switchTab(gBrowser, initialFocusedTab);
+ const tab = await addTab();
+
+ await triggerClickOn(tab, { ctrlKey: true });
+ ok(
+ tab.multiselected && gBrowser._multiSelectedTabsSet.has(tab),
+ "Tab should be (multi) selected after click"
+ );
+ isnot(gBrowser.selectedTab, tab, "Multi-selected tab is not focused");
+ is(gBrowser.selectedTab, initialFocusedTab, "Focused tab doesn't change");
+
+ await triggerClickOn(tab, { ctrlKey: true });
+ ok(
+ !tab.multiselected && !gBrowser._multiSelectedTabsSet.has(tab),
+ "Tab is not (multi) selected anymore"
+ );
+ is(
+ gBrowser.selectedTab,
+ initialFocusedTab,
+ "Focused tab still doesn't change"
+ );
+
+ BrowserTestUtils.removeTab(initialFocusedTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function clearSelection() {
+ const tab1 = await addTab();
+ const tab2 = await addTab();
+ const tab3 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ info("We multi-select tab2 with ctrl key down");
+ await triggerClickOn(tab2, { ctrlKey: true });
+
+ ok(
+ tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1),
+ "Tab1 is (multi) selected"
+ );
+ ok(
+ tab2.multiselected && gBrowser._multiSelectedTabsSet.has(tab2),
+ "Tab2 is (multi) selected"
+ );
+ is(gBrowser.multiSelectedTabsCount, 2, "Two tabs (multi) selected");
+ isnot(tab3, gBrowser.selectedTab, "Tab3 doesn't have focus");
+
+ info("We select tab3 with Ctrl key up");
+ await triggerClickOn(tab3, { ctrlKey: false });
+
+ ok(!tab1.multiselected, "Tab1 is not (multi) selected");
+ ok(!tab2.multiselected, "Tab2 is not (multi) selected");
+ is(gBrowser.multiSelectedTabsCount, 0, "Multi-selection is cleared");
+ is(tab3, gBrowser.selectedTab, "Tab3 has focus");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js
new file mode 100644
index 0000000000..ac647bae3c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js
@@ -0,0 +1,159 @@
+add_task(async function noItemsInTheCollectionBeforeShiftClicking() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+ gBrowser.hideTab(tab3);
+ ok(tab3.hidden, "Tab3 is hidden");
+
+ info("Click on tab4 while holding shift key");
+ await triggerClickOn(tab4, { shiftKey: true });
+
+ ok(
+ tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1),
+ "Tab1 is multi-selected"
+ );
+ ok(
+ tab2.multiselected && gBrowser._multiSelectedTabsSet.has(tab2),
+ "Tab2 is multi-selected"
+ );
+ ok(
+ !tab3.multiselected && !gBrowser._multiSelectedTabsSet.has(tab3),
+ "Hidden tab3 is not multi-selected"
+ );
+ ok(
+ tab4.multiselected && gBrowser._multiSelectedTabsSet.has(tab4),
+ "Tab4 is multi-selected"
+ );
+ ok(
+ !tab5.multiselected && !gBrowser._multiSelectedTabsSet.has(tab5),
+ "Tab5 is not multi-selected"
+ );
+ is(gBrowser.multiSelectedTabsCount, 3, "three multi-selected tabs");
+ is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+ BrowserTestUtils.removeTab(tab5);
+});
+
+add_task(async function itemsInTheCollectionBeforeShiftClicking() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, () => triggerClickOn(tab1, {}));
+
+ is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+ await triggerClickOn(tab3, { ctrlKey: true });
+ is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two tabs are multi-selected");
+ ok(
+ tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1),
+ "Tab1 is multi-selected"
+ );
+ ok(
+ tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3),
+ "Tab3 is multi-selected"
+ );
+
+ info("Click on tab5 while holding Shift key");
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ triggerClickOn(tab5, { shiftKey: true })
+ );
+
+ is(gBrowser.selectedTab, tab3, "Tab3 has focus");
+ ok(
+ !tab1.multiselected && !gBrowser._multiSelectedTabsSet.has(tab1),
+ "Tab1 is not multi-selected"
+ );
+ ok(
+ !tab2.multiselected && !gBrowser._multiSelectedTabsSet.has(tab2),
+ "Tab2 is not multi-selected "
+ );
+ ok(
+ tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3),
+ "Tab3 is multi-selected"
+ );
+ ok(
+ tab4.multiselected && gBrowser._multiSelectedTabsSet.has(tab4),
+ "Tab4 is multi-selected"
+ );
+ ok(
+ tab5.multiselected && gBrowser._multiSelectedTabsSet.has(tab5),
+ "Tab5 is multi-selected"
+ );
+ is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected");
+
+ info("Click on tab4 while holding Shift key");
+ await triggerClickOn(tab4, { shiftKey: true });
+
+ is(gBrowser.selectedTab, tab3, "Tab3 has focus");
+ ok(
+ !tab1.multiselected && !gBrowser._multiSelectedTabsSet.has(tab1),
+ "Tab1 is not multi-selected"
+ );
+ ok(
+ !tab2.multiselected && !gBrowser._multiSelectedTabsSet.has(tab2),
+ "Tab2 is not multi-selected "
+ );
+ ok(
+ tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3),
+ "Tab3 is multi-selected"
+ );
+ ok(
+ tab4.multiselected && gBrowser._multiSelectedTabsSet.has(tab4),
+ "Tab4 is multi-selected"
+ );
+ ok(
+ !tab5.multiselected && !gBrowser._multiSelectedTabsSet.has(tab5),
+ "Tab5 is not multi-selected"
+ );
+ is(gBrowser.multiSelectedTabsCount, 2, "Two tabs are multi-selected");
+
+ info("Click on tab1 while holding Shift key");
+ await triggerClickOn(tab1, { shiftKey: true });
+
+ is(gBrowser.selectedTab, tab3, "Tab3 has focus");
+ ok(
+ tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1),
+ "Tab1 is multi-selected"
+ );
+ ok(
+ tab2.multiselected && gBrowser._multiSelectedTabsSet.has(tab2),
+ "Tab2 is multi-selected "
+ );
+ ok(
+ tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3),
+ "Tab3 is multi-selected"
+ );
+ ok(
+ !tab4.multiselected && !gBrowser._multiSelectedTabsSet.has(tab4),
+ "Tab4 is not multi-selected"
+ );
+ ok(
+ !tab5.multiselected && !gBrowser._multiSelectedTabsSet.has(tab5),
+ "Tab5 is not multi-selected"
+ );
+ is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+ BrowserTestUtils.removeTab(tab5);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift_and_Ctrl.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift_and_Ctrl.js
new file mode 100644
index 0000000000..9e26a5562e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift_and_Ctrl.js
@@ -0,0 +1,75 @@
+add_task(async function selectionWithShiftPreviously() {
+ const tab1 = await addTab();
+ const tab2 = await addTab();
+ const tab3 = await addTab();
+ const tab4 = await addTab();
+ const tab5 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab3);
+
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+ info("Click on tab5 with Shift down");
+ await triggerClickOn(tab5, { shiftKey: true });
+
+ is(gBrowser.selectedTab, tab3, "Tab3 has focus");
+ ok(!tab1.multiselected, "Tab1 is not multi-selected");
+ ok(!tab2.multiselected, "Tab2 is not multi-selected ");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+ ok(tab4.multiselected, "Tab4 is multi-selected");
+ ok(tab5.multiselected, "Tab5 is multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected");
+
+ info("Click on tab1 with both Ctrl/Cmd and Shift down");
+ await triggerClickOn(tab1, { ctrlKey: true, shiftKey: true });
+
+ is(gBrowser.selectedTab, tab3, "Tab3 has focus");
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(tab2.multiselected, "Tab2 is multi-selected ");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+ ok(tab4.multiselected, "Tab4 is multi-selected");
+ ok(tab5.multiselected, "Tab5 is multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 5, "Five tabs are multi-selected");
+
+ for (let tab of [tab1, tab2, tab3, tab4, tab5]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function selectionWithCtrlPreviously() {
+ const tab1 = await addTab();
+ const tab2 = await addTab();
+ const tab3 = await addTab();
+ const tab4 = await addTab();
+ const tab5 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+ info("Click on tab3 with Ctrl key down");
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ is(gBrowser.selectedTab, tab1, "Tab1 has focus");
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(!tab2.multiselected, "Tab2 is not multi-selected ");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+ ok(!tab4.multiselected, "Tab4 is not multi-selected");
+ ok(!tab5.multiselected, "Tab5 is not multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two tabs are multi-selected");
+
+ info("Click on tab5 with both Ctrl/Cmd and Shift down");
+ await triggerClickOn(tab5, { ctrlKey: true, shiftKey: true });
+
+ is(gBrowser.selectedTab, tab1, "Tab3 has focus");
+ ok(tab1.multiselected, "Tab1 is multi-selected");
+ ok(!tab2.multiselected, "Tab2 is not multi-selected ");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+ ok(tab4.multiselected, "Tab4 is multi-selected");
+ ok(tab5.multiselected, "Tab5 is multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 4, "Four tabs are multi-selected");
+
+ for (let tab of [tab1, tab2, tab3, tab4, tab5]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js
new file mode 100644
index 0000000000..cdb0b7bf0c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function synthesizeKeyAndWaitForFocus(element, keyCode, options) {
+ let focused = BrowserTestUtils.waitForEvent(element, "focus");
+ EventUtils.synthesizeKey(keyCode, options);
+ return focused;
+}
+
+function synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab, keyCode, options) {
+ let focused = TestUtils.waitForCondition(() => {
+ return tab.classList.contains("keyboard-focused-tab");
+ }, "Waiting for tab to get keyboard focus");
+ EventUtils.synthesizeKey(keyCode, options);
+ return focused;
+}
+
+add_setup(async function () {
+ // The DevEdition has the DevTools button in the toolbar by default. Remove it
+ // to prevent branch-specific rules what button should be focused.
+ CustomizableUI.removeWidgetFromArea("developer-button");
+
+ let prevActiveElement = document.activeElement;
+ registerCleanupFunction(() => {
+ CustomizableUI.reset();
+ prevActiveElement.focus();
+ });
+});
+
+add_task(async function changeSelectionUsingKeyboard() {
+ const tab1 = await addTab("http://mochi.test:8888/1");
+ const tab2 = await addTab("http://mochi.test:8888/2");
+ const tab3 = await addTab("http://mochi.test:8888/3");
+ const tab4 = await addTab("http://mochi.test:8888/4");
+ const tab5 = await addTab("http://mochi.test:8888/5");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab3);
+ info("Move focus to location bar using the keyboard");
+ await synthesizeKeyAndWaitForFocus(gURLBar, "l", { accelKey: true });
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+
+ info("Move focus to the selected tab using the keyboard");
+ let trackingProtectionIconContainer = document.querySelector(
+ "#tracking-protection-icon-container"
+ );
+ await synthesizeKeyAndWaitForFocus(
+ trackingProtectionIconContainer,
+ "VK_TAB",
+ { shiftKey: true }
+ );
+ is(
+ document.activeElement,
+ trackingProtectionIconContainer,
+ "tracking protection icon container should be focused"
+ );
+ await synthesizeKeyAndWaitForFocus(
+ document.getElementById("reload-button"),
+ "VK_TAB",
+ { shiftKey: true }
+ );
+ await synthesizeKeyAndWaitForFocus(
+ document.getElementById("tabs-newtab-button"),
+ "VK_TAB",
+ { shiftKey: true }
+ );
+ await synthesizeKeyAndWaitForFocus(tab3, "VK_TAB", { shiftKey: true });
+ is(document.activeElement, tab3, "Tab3 should be focused");
+
+ info("Move focus to tab 1 using the keyboard");
+ await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab2, "KEY_ArrowLeft", {
+ accelKey: true,
+ });
+ await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab1, "KEY_ArrowLeft", {
+ accelKey: true,
+ });
+ is(
+ gBrowser.tabContainer.ariaFocusedItem,
+ tab1,
+ "Tab1 should be the ariaFocusedItem"
+ );
+
+ ok(!tab1.multiselected, "Tab1 shouldn't be multiselected");
+ info("Select tab1 using keyboard");
+ EventUtils.synthesizeKey("VK_SPACE", { accelKey: true });
+ ok(tab1.multiselected, "Tab1 should be multiselected");
+
+ info("Move focus to tab 5 using the keyboard");
+ await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab2, "KEY_ArrowRight", {
+ accelKey: true,
+ });
+ await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab3, "KEY_ArrowRight", {
+ accelKey: true,
+ });
+ await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab4, "KEY_ArrowRight", {
+ accelKey: true,
+ });
+ await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab5, "KEY_ArrowRight", {
+ accelKey: true,
+ });
+
+ ok(!tab5.multiselected, "Tab5 shouldn't be multiselected");
+ info("Select tab5 using keyboard");
+ EventUtils.synthesizeKey("VK_SPACE", { accelKey: true });
+ ok(tab5.multiselected, "Tab5 should be multiselected");
+
+ ok(
+ tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1),
+ "Tab1 is (multi) selected"
+ );
+ ok(
+ tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3),
+ "Tab3 is (multi) selected"
+ );
+ ok(
+ tab5.multiselected && gBrowser._multiSelectedTabsSet.has(tab5),
+ "Tab5 is (multi) selected"
+ );
+ is(gBrowser.multiSelectedTabsCount, 3, "Three tabs (multi) selected");
+ is(tab3, gBrowser.selectedTab, "Tab3 is still the selected tab");
+
+ await synthesizeKeyAndWaitForFocus(tab4, "KEY_ArrowLeft", {});
+ is(
+ tab4,
+ gBrowser.selectedTab,
+ "Tab4 is now selected tab since tab5 had keyboard focus"
+ );
+
+ is(tab4.previousElementSibling, tab3, "tab4 should be after tab3");
+ is(tab4.nextElementSibling, tab5, "tab4 should be before tab5");
+
+ let tabsReordered = BrowserTestUtils.waitForCondition(() => {
+ return (
+ tab4.previousElementSibling == tab2 && tab4.nextElementSibling == tab3
+ );
+ }, "tab4 should now be after tab2 and before tab3");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { accelKey: true, shiftKey: true });
+ await tabsReordered;
+
+ is(tab4.previousElementSibling, tab2, "tab4 should be after tab2");
+ is(tab4.nextElementSibling, tab3, "tab4 should be before tab3");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+ BrowserTestUtils.removeTab(tab5);
+});
diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js
new file mode 100644
index 0000000000..0db980bf6b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function () {
+ function testSelectedTabs(tabs) {
+ is(
+ gBrowser.tabContainer.getAttribute("aria-multiselectable"),
+ "true",
+ "tabbrowser should be marked as aria-multiselectable"
+ );
+ gBrowser.selectedTabs = tabs;
+ let { selectedTab, selectedTabs, _multiSelectedTabsSet } = gBrowser;
+ is(selectedTab, tabs[0], "The selected tab should be the expected one");
+ if (tabs.length == 1) {
+ ok(
+ !selectedTab.multiselected,
+ "Selected tab shouldn't be multi-selected because we are not in multi-select context yet"
+ );
+ ok(
+ !_multiSelectedTabsSet.has(selectedTab),
+ "Selected tab shouldn't be in _multiSelectedTabsSet"
+ );
+ is(selectedTabs.length, 1, "selectedTabs should contain a single tab");
+ is(
+ selectedTabs[0],
+ selectedTab,
+ "selectedTabs should contain the selected tab"
+ );
+ ok(
+ !selectedTab.hasAttribute("aria-selected"),
+ "Selected tab shouldn't be marked as aria-selected when only one tab is selected"
+ );
+ } else {
+ const uniqueTabs = [...new Set(tabs)];
+ is(
+ selectedTabs.length,
+ uniqueTabs.length,
+ "Check number of selected tabs"
+ );
+ for (let tab of uniqueTabs) {
+ ok(tab.multiselected, "Tab should be multi-selected");
+ ok(
+ _multiSelectedTabsSet.has(tab),
+ "Tab should be in _multiSelectedTabsSet"
+ );
+ ok(selectedTabs.includes(tab), "Tab should be in selectedTabs");
+ is(
+ tab.getAttribute("aria-selected"),
+ "true",
+ "Selected tab should be marked as aria-selected"
+ );
+ }
+ }
+ }
+
+ const tab1 = await addTab();
+ const tab2 = await addTab();
+ const tab3 = await addTab();
+
+ testSelectedTabs([tab1]);
+ testSelectedTabs([tab2]);
+ testSelectedTabs([tab2, tab1]);
+ testSelectedTabs([tab1, tab2]);
+ testSelectedTabs([tab3, tab2]);
+ testSelectedTabs([tab3, tab1]);
+ testSelectedTabs([tab1, tab2, tab1]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/base/content/test/tabs/browser_navigatePinnedTab.js b/browser/base/content/test/tabs/browser_navigatePinnedTab.js
new file mode 100644
index 0000000000..f1828af9c6
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_navigatePinnedTab.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ // Test that changing the URL in a pinned tab works correctly
+
+ let TEST_LINK_INITIAL = "about:mozilla";
+ let TEST_LINK_CHANGED = "about:support";
+
+ let appTab = BrowserTestUtils.addTab(gBrowser, TEST_LINK_INITIAL);
+ let browser = appTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ gBrowser.pinTab(appTab);
+ is(appTab.pinned, true, "Tab was successfully pinned");
+
+ let initialTabsNo = gBrowser.tabs.length;
+
+ gBrowser.selectedTab = appTab;
+ gURLBar.focus();
+ gURLBar.value = TEST_LINK_CHANGED;
+
+ gURLBar.goButton.click();
+ await BrowserTestUtils.browserLoaded(browser);
+
+ is(
+ appTab.linkedBrowser.currentURI.spec,
+ TEST_LINK_CHANGED,
+ "New page loaded in the app tab"
+ );
+ is(gBrowser.tabs.length, initialTabsNo, "No additional tabs were opened");
+
+ // Now check that opening a link that does create a new tab works,
+ // and also that it nulls out the opener.
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(
+ appTab.linkedBrowser,
+ false,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(appTab.linkedBrowser, "http://example.com/");
+ info("Started loading example.com");
+ await pageLoadPromise;
+ info("Loaded example.com");
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/"
+ );
+ await SpecialPowers.spawn(browser, [], async function () {
+ let link = content.document.createElement("a");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ link.href = "http://example.org/";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+ info("Created & clicked link");
+ let extraTab = await newTabPromise;
+ info("Got a new tab");
+ await SpecialPowers.spawn(extraTab.linkedBrowser, [], async function () {
+ is(content.opener, null, "No opener should be available");
+ });
+ BrowserTestUtils.removeTab(extraTab);
+});
+
+registerCleanupFunction(function () {
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js b/browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js
new file mode 100644
index 0000000000..55efdba851
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js
@@ -0,0 +1,21 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const TEST_HTTP = httpURL("dummy_page.html");
+
+// Test for Bug 1634272
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(TEST_HTTP, async function (browser) {
+ info("Tab ready");
+
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("home-button")
+ );
+
+ document.getElementById("home-button").click();
+ await BrowserTestUtils.browserLoaded(browser, false, HomePage.get());
+ is(gURLBar.value, "", "URL bar should be empty");
+ ok(gURLBar.focused, "URL bar should be focused");
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js b/browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js
new file mode 100644
index 0000000000..226817a350
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js
@@ -0,0 +1,177 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from helper_origin_attrs_testing.js */
+loadTestSubscript("helper_origin_attrs_testing.js");
+
+const PATH = "browser/browser/base/content/test/tabs/blank.html";
+
+var TEST_CASES = [
+ { uri: "https://example.com/" + PATH },
+ { uri: "https://example.org/" + PATH },
+ { uri: "about:preferences" },
+ { uri: "about:config" },
+];
+
+// 3 container tabs, 1 regular tab and 1 private tab
+const NUM_PAGES_OPEN_FOR_EACH_TEST_CASE = 5;
+var remoteTypes;
+var xulFrameLoaderCreatedCounter = {};
+
+function handleEventLocal(aEvent) {
+ if (aEvent.type != "XULFrameLoaderCreated") {
+ return;
+ }
+ // Ignore <browser> element in about:preferences and any other special pages
+ if ("gBrowser" in aEvent.target.ownerGlobal) {
+ xulFrameLoaderCreatedCounter.numCalledSoFar++;
+ }
+}
+
+var gPrevRemoteTypeRegularTab;
+var gPrevRemoteTypeContainerTab;
+var gPrevRemoteTypePrivateTab;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.userContext.enabled", true],
+ // don't preload tabs so we don't have extra XULFrameLoaderCreated events
+ // firing
+ ["browser.newtab.preload", false],
+ ],
+ });
+
+ requestLongerTimeout(4);
+});
+
+function setupRemoteTypes() {
+ gPrevRemoteTypeRegularTab = null;
+ gPrevRemoteTypeContainerTab = {};
+ gPrevRemoteTypePrivateTab = null;
+
+ remoteTypes = getExpectedRemoteTypes(
+ gFissionBrowser,
+ NUM_PAGES_OPEN_FOR_EACH_TEST_CASE
+ );
+}
+
+add_task(async function testNavigate() {
+ setupRemoteTypes();
+ /**
+ * Open a regular tab, 3 container tabs and a private window, load about:blank or about:privatebrowsing
+ * For each test case
+ * load the uri
+ * verify correct remote type
+ * close tabs
+ */
+
+ let regularPage = await openURIInRegularTab("about:blank", window);
+ gPrevRemoteTypeRegularTab = regularPage.tab.linkedBrowser.remoteType;
+ let containerPages = [];
+ for (
+ var user_context_id = 1;
+ user_context_id <= NUM_USER_CONTEXTS;
+ user_context_id++
+ ) {
+ let containerPage = await openURIInContainer(
+ "about:blank",
+ window,
+ user_context_id
+ );
+ gPrevRemoteTypeContainerTab[user_context_id] =
+ containerPage.tab.linkedBrowser.remoteType;
+ containerPages.push(containerPage);
+ }
+
+ let privatePage = await openURIInPrivateTab();
+ gPrevRemoteTypePrivateTab = privatePage.tab.linkedBrowser.remoteType;
+
+ for (const testCase of TEST_CASES) {
+ let uri = testCase.uri;
+
+ await loadURIAndCheckRemoteType(
+ regularPage.tab.linkedBrowser,
+ uri,
+ "regular tab",
+ gPrevRemoteTypeRegularTab
+ );
+ gPrevRemoteTypeRegularTab = regularPage.tab.linkedBrowser.remoteType;
+
+ for (const page of containerPages) {
+ await loadURIAndCheckRemoteType(
+ page.tab.linkedBrowser,
+ uri,
+ `container tab ${page.user_context_id}`,
+ gPrevRemoteTypeContainerTab[page.user_context_id]
+ );
+ gPrevRemoteTypeContainerTab[page.user_context_id] =
+ page.tab.linkedBrowser.remoteType;
+ }
+
+ await loadURIAndCheckRemoteType(
+ privatePage.tab.linkedBrowser,
+ uri,
+ "private tab",
+ gPrevRemoteTypePrivateTab
+ );
+ gPrevRemoteTypePrivateTab = privatePage.tab.linkedBrowser.remoteType;
+ }
+ // Close tabs
+ containerPages.forEach(containerPage => {
+ BrowserTestUtils.removeTab(containerPage.tab);
+ });
+ BrowserTestUtils.removeTab(regularPage.tab);
+ BrowserTestUtils.removeTab(privatePage.tab);
+});
+
+async function loadURIAndCheckRemoteType(
+ aBrowser,
+ aURI,
+ aText,
+ aPrevRemoteType
+) {
+ let expectedCurr = remoteTypes.shift();
+ initXulFrameLoaderCreatedCounter(xulFrameLoaderCreatedCounter);
+ aBrowser.ownerGlobal.gBrowser.addEventListener(
+ "XULFrameLoaderCreated",
+ handleEventLocal
+ );
+ let loaded = BrowserTestUtils.browserLoaded(aBrowser, false, aURI);
+ info(`About to load ${aURI} in ${aText}`);
+ await BrowserTestUtils.loadURIString(aBrowser, aURI);
+ await loaded;
+
+ // Verify correct remote type
+ is(
+ expectedCurr,
+ aBrowser.remoteType,
+ `correct remote type for ${aURI} ${aText}`
+ );
+
+ // Verify XULFrameLoaderCreated firing correct number of times
+ info(
+ `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedCounter.numCalledSoFar} time(s) for ${aURI} ${aText}`
+ );
+ var numExpected =
+ expectedCurr == aPrevRemoteType &&
+ // With BFCache in the parent we'll get a XULFrameLoaderCreated even if
+ // expectedCurr == aPrevRemoteType, because we store the old frameloader
+ // in the BFCache. We have to make an exception for loads in the parent
+ // process (which have a null aPrevRemoteType/expectedCurr) because
+ // BFCache in the parent disables caching for those loads.
+ (!SpecialPowers.Services.appinfo.sessionHistoryInParent || !expectedCurr)
+ ? 0
+ : 1;
+ is(
+ xulFrameLoaderCreatedCounter.numCalledSoFar,
+ numExpected,
+ `XULFrameLoaderCreated fired correct number of times for ${aURI} ${aText}
+ prev=${aPrevRemoteType} curr =${aBrowser.remoteType}`
+ );
+ aBrowser.ownerGlobal.gBrowser.removeEventListener(
+ "XULFrameLoaderCreated",
+ handleEventLocal
+ );
+}
diff --git a/browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js b/browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js
new file mode 100644
index 0000000000..9375f3f164
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js
@@ -0,0 +1,37 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_HTTP = "http://example.org/";
+
+// Test for bug 1378377.
+add_task(async function () {
+ // Set prefs to ensure file content process.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.remote.separateFileUriProcess", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(TEST_HTTP, async function (fileBrowser) {
+ ok(
+ E10SUtils.isWebRemoteType(fileBrowser.remoteType),
+ "Check that tab normally has web remote type."
+ );
+ });
+
+ // Set prefs to whitelist TEST_HTTP for file:// URI use.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["capability.policy.policynames", "allowFileURI"],
+ ["capability.policy.allowFileURI.sites", TEST_HTTP],
+ ["capability.policy.allowFileURI.checkloaduri.enabled", "allAccess"],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(TEST_HTTP, async function (fileBrowser) {
+ is(
+ fileBrowser.remoteType,
+ E10SUtils.FILE_REMOTE_TYPE,
+ "Check that tab now has file remote type."
+ );
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js
new file mode 100644
index 0000000000..6ab6ce198e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js
@@ -0,0 +1,230 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests to ensure that Activity Stream loads in the privileged about:
+ * content process. Normal http web pages should load in the web content
+ * process.
+ * Ref: Bug 1469072.
+ */
+
+const ABOUT_BLANK = "about:blank";
+const ABOUT_HOME = "about:home";
+const ABOUT_NEWTAB = "about:newtab";
+const ABOUT_WELCOME = "about:welcome";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_HTTP = "http://example.org/";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.newtab.preload", false],
+ ["browser.tabs.remote.separatePrivilegedContentProcess", true],
+ ["dom.ipc.processCount.privilegedabout", 1],
+ ["dom.ipc.keepProcessesAlive.privilegedabout", 1],
+ ],
+ });
+});
+
+/*
+ * Test to ensure that the Activity Stream tabs open in privileged about: content
+ * process. We will first open an about:newtab page that acts as a reference to
+ * the privileged about: content process. With the reference, we can then open
+ * Activity Stream links in a new tab and ensure that the new tab opens in the same
+ * privileged about: content process as our reference.
+ */
+add_task(async function activity_stream_in_privileged_content_process() {
+ Services.ppmm.releaseCachedProcesses();
+
+ await BrowserTestUtils.withNewTab(ABOUT_NEWTAB, async function (browser1) {
+ checkBrowserRemoteType(browser1, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE);
+
+ // Note the processID for about:newtab for comparison later.
+ let privilegedPid = browser1.frameLoader.remoteTab.osPid;
+
+ for (let url of [
+ ABOUT_NEWTAB,
+ ABOUT_WELCOME,
+ ABOUT_HOME,
+ `${ABOUT_NEWTAB}#foo`,
+ `${ABOUT_WELCOME}#bar`,
+ `${ABOUT_HOME}#baz`,
+ `${ABOUT_NEWTAB}?q=foo`,
+ `${ABOUT_WELCOME}?q=bar`,
+ `${ABOUT_HOME}?q=baz`,
+ ]) {
+ await BrowserTestUtils.withNewTab(url, async function (browser2) {
+ is(
+ browser2.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that about:newtab tabs are in the same privileged about: content process."
+ );
+ });
+ }
+ });
+
+ Services.ppmm.releaseCachedProcesses();
+});
+
+/*
+ * Test to ensure that a process switch occurs when navigating between normal
+ * web pages and Activity Stream pages in the same tab.
+ */
+add_task(async function process_switching_through_loading_in_the_same_tab() {
+ Services.ppmm.releaseCachedProcesses();
+
+ await BrowserTestUtils.withNewTab(TEST_HTTP, async function (browser) {
+ checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+ for (let [url, remoteType] of [
+ [ABOUT_NEWTAB, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [ABOUT_BLANK, E10SUtils.WEB_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [ABOUT_HOME, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [ABOUT_WELCOME, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [ABOUT_BLANK, E10SUtils.WEB_REMOTE_TYPE],
+ [`${ABOUT_NEWTAB}#foo`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [`${ABOUT_WELCOME}#bar`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [`${ABOUT_HOME}#baz`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [`${ABOUT_NEWTAB}?q=foo`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [`${ABOUT_WELCOME}?q=bar`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ [`${ABOUT_HOME}?q=baz`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE],
+ [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+ ]) {
+ BrowserTestUtils.loadURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+ checkBrowserRemoteType(browser, remoteType);
+ }
+ });
+
+ Services.ppmm.releaseCachedProcesses();
+});
+
+/*
+ * Test to ensure that a process switch occurs when navigating between normal
+ * web pages and Activity Stream pages using the browser's navigation features
+ * such as history and location change.
+ */
+add_task(async function process_switching_through_navigation_features() {
+ Services.ppmm.releaseCachedProcesses();
+
+ await BrowserTestUtils.withNewTab(
+ ABOUT_NEWTAB,
+ async function (initialBrowser) {
+ checkBrowserRemoteType(
+ initialBrowser,
+ E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
+ );
+
+ // Note the processID for about:newtab for comparison later.
+ let privilegedPid = initialBrowser.frameLoader.remoteTab.osPid;
+
+ function assertIsPrivilegedProcess(browser, desc) {
+ is(
+ browser.messageManager.remoteType,
+ E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE,
+ `Check that ${desc} is loaded in privileged about: content process.`
+ );
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ `Check that ${desc} is loaded in original privileged process.`
+ );
+ }
+
+ // Check that about:newtab opened from JS in about:newtab page is in the same process.
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ ABOUT_NEWTAB,
+ true
+ );
+ await SpecialPowers.spawn(initialBrowser, [ABOUT_NEWTAB], uri => {
+ content.open(uri, "_blank");
+ });
+ let newTab = await promiseTabOpened;
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(newTab);
+ });
+ let browser = newTab.linkedBrowser;
+ assertIsPrivilegedProcess(browser, "new tab opened from about:newtab");
+
+ // Check that reload does not break the privileged about: content process affinity.
+ BrowserReload();
+ await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB);
+ assertIsPrivilegedProcess(browser, "about:newtab after reload");
+
+ // Load http webpage
+ BrowserTestUtils.loadURIString(browser, TEST_HTTP);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_HTTP);
+ checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+ // Check that using the history back feature switches back to privileged about: content process.
+ let promiseLocation = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ ABOUT_NEWTAB
+ );
+ browser.goBack();
+ await promiseLocation;
+ // We will need to ensure that the process flip has fully completed so that
+ // the navigation history data will be available when we do browser.goForward();
+ await BrowserTestUtils.browserLoaded(browser);
+ assertIsPrivilegedProcess(browser, "about:newtab after history goBack");
+
+ // Check that using the history forward feature switches back to the web content process.
+ promiseLocation = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_HTTP
+ );
+ browser.goForward();
+ await promiseLocation;
+ // We will need to ensure that the process flip has fully completed so that
+ // the navigation history data will be available when we do browser.gotoIndex(0);
+ await BrowserTestUtils.browserLoaded(browser);
+ checkBrowserRemoteType(
+ browser,
+ E10SUtils.WEB_REMOTE_TYPE,
+ "Check that tab runs in the web content process after using history goForward."
+ );
+
+ // Check that goto history index does not break the affinity.
+ promiseLocation = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ ABOUT_NEWTAB
+ );
+ browser.gotoIndex(0);
+ await promiseLocation;
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that about:newtab is in privileged about: content process after history gotoIndex."
+ );
+ assertIsPrivilegedProcess(
+ browser,
+ "about:newtab after history goToIndex"
+ );
+
+ BrowserTestUtils.loadURIString(browser, TEST_HTTP);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_HTTP);
+ checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+ // Check that location change causes a change in process type as well.
+ await SpecialPowers.spawn(browser, [ABOUT_NEWTAB], uri => {
+ content.location = uri;
+ });
+ await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB);
+ assertIsPrivilegedProcess(browser, "about:newtab after location change");
+ }
+ );
+
+ Services.ppmm.releaseCachedProcesses();
+});
diff --git a/browser/base/content/test/tabs/browser_new_tab_insert_position.js b/browser/base/content/test/tabs/browser_new_tab_insert_position.js
new file mode 100644
index 0000000000..d54aed738b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_insert_position.js
@@ -0,0 +1,288 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+function promiseBrowserStateRestored(state) {
+ if (typeof state != "string") {
+ state = JSON.stringify(state);
+ }
+ // We wait for the notification that restore is done, and for the notification
+ // that the active tab is loaded and restored.
+ let promise = Promise.all([
+ TestUtils.topicObserved("sessionstore-browser-state-restored"),
+ BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"),
+ ]);
+ SessionStore.setBrowserState(state);
+ return promise;
+}
+
+function promiseRemoveThenUndoCloseTab(tab) {
+ // We wait for the notification that restore is done, and for the notification
+ // that the active tab is loaded and restored.
+ let promise = Promise.all([
+ TestUtils.topicObserved("sessionstore-closed-objects-changed"),
+ BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"),
+ ]);
+ BrowserTestUtils.removeTab(tab);
+ SessionStore.undoCloseTab(window, 0);
+ return promise;
+}
+
+// Compare the current browser tab order against the session state ordering, they should always match.
+function verifyTabState(state) {
+ let newStateTabs = JSON.parse(state).windows[0].tabs;
+ for (let i = 0; i < gBrowser.tabs.length; i++) {
+ is(
+ gBrowser.tabs[i].linkedBrowser.currentURI.spec,
+ newStateTabs[i].entries[0].url,
+ `tab pos ${i} matched ${gBrowser.tabs[i].linkedBrowser.currentURI.spec}`
+ );
+ }
+}
+
+const bulkLoad = [
+ "http://mochi.test:8888/#5",
+ "http://mochi.test:8888/#6",
+ "http://mochi.test:8888/#7",
+ "http://mochi.test:8888/#8",
+];
+
+const sessData = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://mochi.test:8888/#0", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://mochi.test:8888/#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://mochi.test:8888/#3", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://mochi.test:8888/#4", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+};
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const urlbarURL = "http://example.com/#urlbar";
+
+async function doTest(aInsertRelatedAfterCurrent, aInsertAfterCurrent) {
+ const kDescription =
+ "(aInsertRelatedAfterCurrent=" +
+ aInsertRelatedAfterCurrent +
+ ", aInsertAfterCurrent=" +
+ aInsertAfterCurrent +
+ "): ";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.opentabfor.middleclick", true],
+ ["browser.tabs.loadBookmarksInBackground", false],
+ ["browser.tabs.insertRelatedAfterCurrent", aInsertRelatedAfterCurrent],
+ ["browser.tabs.insertAfterCurrent", aInsertAfterCurrent],
+ ],
+ });
+
+ let oldState = SessionStore.getBrowserState();
+
+ await promiseBrowserStateRestored(sessData);
+
+ // Create a *opener* tab page which has a link to "example.com".
+ let pageURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+ );
+ pageURL = `${pageURL}file_new_tab_page.html`;
+ let openerTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ pageURL
+ );
+ const openerTabIndex = 1;
+ gBrowser.moveTabTo(openerTab, openerTabIndex);
+
+ // Open a related tab via Middle click on the cell and test its position.
+ let openTabIndex =
+ aInsertRelatedAfterCurrent || aInsertAfterCurrent
+ ? openerTabIndex + 1
+ : gBrowser.tabs.length;
+ let openTabDescription =
+ aInsertRelatedAfterCurrent || aInsertAfterCurrent
+ ? "immediately to the right"
+ : "at rightmost";
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/#linkclick",
+ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link_to_example_com",
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ let openTab = await newTabPromise;
+ is(
+ openTab.linkedBrowser.currentURI.spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/#linkclick",
+ "Middle click should open site to correct url."
+ );
+ is(
+ openTab._tPos,
+ openTabIndex,
+ kDescription +
+ "Middle click should open site in a new tab " +
+ openTabDescription
+ );
+ if (aInsertRelatedAfterCurrent || aInsertAfterCurrent) {
+ is(openTab.owner, openerTab, "tab owner is set correctly");
+ }
+ is(openTab.openerTab, openerTab, "opener tab is set");
+
+ // Open an unrelated tab from the URL bar and test its position.
+ openTabIndex = aInsertAfterCurrent
+ ? openerTabIndex + 1
+ : gBrowser.tabs.length;
+ openTabDescription = aInsertAfterCurrent
+ ? "immediately to the right"
+ : "at rightmost";
+
+ gURLBar.focus();
+ gURLBar.select();
+ newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, urlbarURL, true);
+ EventUtils.sendString(urlbarURL);
+ EventUtils.synthesizeKey("KEY_Alt", {
+ altKey: true,
+ code: "AltLeft",
+ type: "keydown",
+ });
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true, code: "Enter" });
+ EventUtils.synthesizeKey("KEY_Alt", {
+ altKey: false,
+ code: "AltLeft",
+ type: "keyup",
+ });
+ let unrelatedTab = await newTabPromise;
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ unrelatedTab.linkedBrowser.currentURI.spec,
+ `${kDescription} ${urlbarURL} should be loaded in the current tab.`
+ );
+ is(
+ unrelatedTab._tPos,
+ openTabIndex,
+ `${kDescription} Alt+Enter in the URL bar should open page in a new tab ${openTabDescription}`
+ );
+ is(unrelatedTab.owner, openerTab, "owner tab is set correctly");
+ ok(!unrelatedTab.openerTab, "no opener tab is set");
+
+ // Closing this should go back to the last selected tab, which just happens to be "openerTab"
+ // but is not in fact the opener.
+ BrowserTestUtils.removeTab(unrelatedTab);
+ is(
+ gBrowser.selectedTab,
+ openerTab,
+ kDescription + `openerTab should be selected after closing unrelated tab`
+ );
+
+ // Go back to the opener tab. Closing the child tab should return to the opener.
+ BrowserTestUtils.removeTab(openTab);
+ is(
+ gBrowser.selectedTab,
+ openerTab,
+ kDescription + "openerTab should be selected after closing related tab"
+ );
+
+ // Flush before messing with browser state.
+ for (let tab of gBrowser.tabs) {
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ }
+
+ // Get the session state, verify SessionStore gives us expected data.
+ let newState = SessionStore.getBrowserState();
+ verifyTabState(newState);
+
+ // Remove the tab at the end, then undo. It should reappear where it was.
+ await promiseRemoveThenUndoCloseTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ verifyTabState(newState);
+
+ // Remove a tab in the middle, then undo. It should reappear where it was.
+ await promiseRemoveThenUndoCloseTab(gBrowser.tabs[2]);
+ verifyTabState(newState);
+
+ // Bug 1442679 - Test bulk opening with loadTabs loads the tabs in order
+
+ let loadPromises = Promise.all(
+ bulkLoad.map(url =>
+ BrowserTestUtils.waitForNewTab(gBrowser, url, false, true)
+ )
+ );
+ // loadTabs will insertAfterCurrent
+ let nextTab = aInsertAfterCurrent
+ ? gBrowser.selectedTab._tPos + 1
+ : gBrowser.tabs.length;
+
+ gBrowser.loadTabs(bulkLoad, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ await loadPromises;
+ for (let i = nextTab, j = 0; j < bulkLoad.length; i++, j++) {
+ is(
+ gBrowser.tabs[i].linkedBrowser.currentURI.spec,
+ bulkLoad[j],
+ `bulkLoad tab pos ${i} matched`
+ );
+ }
+
+ // Now we want to test that positioning remains correct after a session restore.
+
+ // Restore pre-test state so we can restore and test tab ordering.
+ await promiseBrowserStateRestored(oldState);
+
+ // Restore test state and verify it is as it was.
+ await promiseBrowserStateRestored(newState);
+ verifyTabState(newState);
+
+ // Restore pre-test state for next test.
+ await promiseBrowserStateRestored(oldState);
+}
+
+add_task(async function test_settings_insertRelatedAfter() {
+ // Firefox default settings.
+ await doTest(true, false);
+});
+
+add_task(async function test_settings_insertAfter() {
+ await doTest(true, true);
+});
+
+add_task(async function test_settings_always_insertAfter() {
+ await doTest(false, true);
+});
+
+add_task(async function test_settings_always_insertAtEnd() {
+ await doTest(false, false);
+});
diff --git a/browser/base/content/test/tabs/browser_new_tab_url.js b/browser/base/content/test/tabs/browser_new_tab_url.js
new file mode 100644
index 0000000000..233cb4e59e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_url.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+add_task(async function test_browser_open_newtab_default_url() {
+ BrowserOpenTab();
+ const tab = gBrowser.selectedTab;
+
+ if (tab.linkedBrowser.currentURI.spec !== window.BROWSER_NEW_TAB_URL) {
+ // If about:newtab is not loaded immediately, wait for any location change.
+ await BrowserTestUtils.waitForLocationChange(gBrowser);
+ }
+
+ is(tab.linkedBrowser.currentURI.spec, window.BROWSER_NEW_TAB_URL);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_browser_open_newtab_specific_url() {
+ const url = "https://example.com";
+
+ BrowserOpenTab({ url });
+ const tab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ is(tab.linkedBrowser.currentURI.spec, "https://example.com/");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js b/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js
new file mode 100644
index 0000000000..f2577cc8b2
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 DEFAULT_THEME = "default-theme@mozilla.org";
+
+async function selectTheme(id) {
+ let theme = await AddonManager.getAddonByID(id || DEFAULT_THEME);
+ await theme.enable();
+}
+
+registerCleanupFunction(() => {
+ return selectTheme(null);
+});
+
+add_task(async function withoutLWT() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ ok(
+ !win.gBrowser.tabContainer.hasAttribute("overflow"),
+ "tab container not overflowing"
+ );
+ ok(
+ !win.gBrowser.tabContainer.arrowScrollbox.hasAttribute("overflowing"),
+ "arrow scrollbox not overflowing"
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function withLWT() {
+ await selectTheme("firefox-compact-light@mozilla.org");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ ok(
+ !win.gBrowser.tabContainer.hasAttribute("overflow"),
+ "tab container not overflowing"
+ );
+ ok(
+ !win.gBrowser.tabContainer.arrowScrollbox.hasAttribute("overflowing"),
+ "arrow scrollbox not overflowing"
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js
new file mode 100644
index 0000000000..cb9fc3c6d7
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js
@@ -0,0 +1,28 @@
+"use strict";
+
+add_task(async function test_browser_open_newtab_start_observer_notification() {
+ let observerFiredPromise = new Promise(resolve => {
+ function observe(subject) {
+ Services.obs.removeObserver(observe, "browser-open-newtab-start");
+ resolve(subject.wrappedJSObject);
+ }
+ Services.obs.addObserver(observe, "browser-open-newtab-start");
+ });
+
+ // We're calling BrowserOpenTab() (rather the using BrowserTestUtils
+ // because we want to be sure that it triggers the event to fire, since
+ // it's very close to where various user-actions are triggered.
+ BrowserOpenTab();
+ const newTabCreatedPromise = await observerFiredPromise;
+ const browser = await newTabCreatedPromise;
+ const tab = gBrowser.selectedTab;
+
+ ok(true, "browser-open-newtab-start observer not called");
+ Assert.deepEqual(
+ browser,
+ tab.linkedBrowser,
+ "browser-open-newtab-start notified with the created browser"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js b/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js
new file mode 100644
index 0000000000..e6b30a207d
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const TEST_FILE = "dummy_page.html";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const WEB_ADDRESS = "http://example.org/";
+
+// Test for bug 1321020.
+add_task(async function () {
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append(TEST_FILE);
+
+ // The file can be a symbolic link on local build. Normalize it to make sure
+ // the path matches to the actual URI opened in the new tab.
+ dir.normalize();
+
+ const uriString = Services.io.newFileURI(dir).spec;
+ const openedUriString = uriString + "?opened";
+
+ // Open first file:// page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString);
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ // Open new file:// tab from JavaScript in first file:// page.
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ openedUriString,
+ true
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [openedUriString], uri => {
+ content.open(uri, "_blank");
+ });
+
+ let openedTab = await promiseTabOpened;
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(openedTab);
+ });
+
+ let openedBrowser = openedTab.linkedBrowser;
+
+ // Ensure that new file:// tab can be navigated to web content.
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ BrowserTestUtils.loadURIString(openedBrowser, "http://example.org/");
+ let href = await BrowserTestUtils.browserLoaded(
+ openedBrowser,
+ false,
+ WEB_ADDRESS
+ );
+ is(
+ href,
+ WEB_ADDRESS,
+ "Check that new file:// page has navigated successfully to web content"
+ );
+});
diff --git a/browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js b/browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js
new file mode 100644
index 0000000000..00bdb83cdd
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* import-globals-from helper_origin_attrs_testing.js */
+loadTestSubscript("helper_origin_attrs_testing.js");
+
+const PATH = "browser/browser/base/content/test/tabs/blank.html";
+
+var TEST_CASES = [
+ { uri: "https://example.com/" + PATH },
+ { uri: "https://example.org/" + PATH },
+ { uri: "about:preferences" },
+ { uri: "about:config" },
+ // file:// uri will be added in setup()
+];
+
+// 3 container tabs, 1 regular tab and 1 private tab
+const NUM_PAGES_OPEN_FOR_EACH_TEST_CASE = 5;
+var remoteTypes;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.userContext.enabled", true],
+ // don't preload tabs so we don't have extra XULFrameLoaderCreated events
+ // firing
+ ["browser.newtab.preload", false],
+ ],
+ });
+ requestLongerTimeout(5);
+
+ // Add a file:// uri
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append("blank.html");
+ // The file can be a symbolic link on local build. Normalize it to make sure
+ // the path matches to the actual URI opened in the new tab.
+ dir.normalize();
+ const uriString = Services.io.newFileURI(dir).spec;
+ TEST_CASES.push({ uri: uriString });
+});
+
+function setupRemoteTypes() {
+ remoteTypes = getExpectedRemoteTypes(
+ gFissionBrowser,
+ NUM_PAGES_OPEN_FOR_EACH_TEST_CASE
+ );
+ remoteTypes = remoteTypes.concat(
+ Array(NUM_PAGES_OPEN_FOR_EACH_TEST_CASE).fill("file")
+ ); // file uri
+}
+
+add_task(async function test_user_identity_simple() {
+ setupRemoteTypes();
+ var currentRemoteType;
+
+ for (let testData of TEST_CASES) {
+ info(`Will open ${testData.uri} in different tabs`);
+ // Open uri without a container
+ info(`About to open a regular page`);
+ currentRemoteType = remoteTypes.shift();
+ let page_regular = await openURIInRegularTab(testData.uri, window);
+ is(
+ page_regular.tab.linkedBrowser.remoteType,
+ currentRemoteType,
+ "correct remote type"
+ );
+
+ // Open the same uri in different user contexts
+ info(`About to open container pages`);
+ let containerPages = [];
+ for (
+ var user_context_id = 1;
+ user_context_id <= NUM_USER_CONTEXTS;
+ user_context_id++
+ ) {
+ currentRemoteType = remoteTypes.shift();
+ let containerPage = await openURIInContainer(
+ testData.uri,
+ window,
+ user_context_id
+ );
+ is(
+ containerPage.tab.linkedBrowser.remoteType,
+ currentRemoteType,
+ "correct remote type"
+ );
+ containerPages.push(containerPage);
+ }
+
+ // Open the same uri in a private browser
+ currentRemoteType = remoteTypes.shift();
+ let page_private = await openURIInPrivateTab(testData.uri);
+ let privateRemoteType = page_private.tab.linkedBrowser.remoteType;
+ is(privateRemoteType, currentRemoteType, "correct remote type");
+
+ // Close all the tabs
+ containerPages.forEach(page => {
+ BrowserTestUtils.removeTab(page.tab);
+ });
+ BrowserTestUtils.removeTab(page_regular.tab);
+ BrowserTestUtils.removeTab(page_private.tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_origin_attrs_rel.js b/browser/base/content/test/tabs/browser_origin_attrs_rel.js
new file mode 100644
index 0000000000..b4a2a826f4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_origin_attrs_rel.js
@@ -0,0 +1,281 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* import-globals-from helper_origin_attrs_testing.js */
+loadTestSubscript("helper_origin_attrs_testing.js");
+
+const PATH =
+ "browser/browser/base/content/test/tabs/file_rel_opener_noopener.html";
+const URI_EXAMPLECOM =
+ "https://example.com/browser/browser/base/content/test/tabs/blank.html";
+const URI_EXAMPLEORG =
+ "https://example.org/browser/browser/base/content/test/tabs/blank.html";
+var TEST_CASES = ["https://example.com/" + PATH, "https://example.org/" + PATH];
+// How many times we navigate (exclude going back)
+const NUM_NAVIGATIONS = 5;
+// Remote types we expect for all pages that we open, in the order of being opened
+// (we don't include remote type for when we navigate back after clicking on a link)
+var remoteTypes;
+var xulFrameLoaderCreatedCounter = {};
+var LINKS_INFO = [
+ {
+ uri: URI_EXAMPLECOM,
+ id: "link_noopener_examplecom",
+ },
+ {
+ uri: URI_EXAMPLECOM,
+ id: "link_opener_examplecom",
+ },
+ {
+ uri: URI_EXAMPLEORG,
+ id: "link_noopener_exampleorg",
+ },
+ {
+ uri: URI_EXAMPLEORG,
+ id: "link_opener_exampleorg",
+ },
+];
+
+function handleEventLocal(aEvent) {
+ if (aEvent.type != "XULFrameLoaderCreated") {
+ return;
+ }
+ // Ignore <browser> element in about:preferences and any other special pages
+ if ("gBrowser" in aEvent.target.ownerGlobal) {
+ xulFrameLoaderCreatedCounter.numCalledSoFar++;
+ }
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.userContext.enabled", true],
+ // don't preload tabs so we don't have extra XULFrameLoaderCreated events
+ // firing
+ ["browser.newtab.preload", false],
+ ],
+ });
+ requestLongerTimeout(3);
+});
+
+function setupRemoteTypes() {
+ if (gFissionBrowser) {
+ remoteTypes = {
+ initial: [
+ "webIsolated=https://example.com",
+ "webIsolated=https://example.com^userContextId=1",
+ "webIsolated=https://example.com^userContextId=2",
+ "webIsolated=https://example.com^userContextId=3",
+ "webIsolated=https://example.com^privateBrowsingId=1",
+ "webIsolated=https://example.org",
+ "webIsolated=https://example.org^userContextId=1",
+ "webIsolated=https://example.org^userContextId=2",
+ "webIsolated=https://example.org^userContextId=3",
+ "webIsolated=https://example.org^privateBrowsingId=1",
+ ],
+ regular: {},
+ 1: {},
+ 2: {},
+ 3: {},
+ private: {},
+ };
+ remoteTypes.regular[URI_EXAMPLECOM] = "webIsolated=https://example.com";
+ remoteTypes.regular[URI_EXAMPLEORG] = "webIsolated=https://example.org";
+ remoteTypes["1"][URI_EXAMPLECOM] =
+ "webIsolated=https://example.com^userContextId=1";
+ remoteTypes["1"][URI_EXAMPLEORG] =
+ "webIsolated=https://example.org^userContextId=1";
+ remoteTypes["2"][URI_EXAMPLECOM] =
+ "webIsolated=https://example.com^userContextId=2";
+ remoteTypes["2"][URI_EXAMPLEORG] =
+ "webIsolated=https://example.org^userContextId=2";
+ remoteTypes["3"][URI_EXAMPLECOM] =
+ "webIsolated=https://example.com^userContextId=3";
+ remoteTypes["3"][URI_EXAMPLEORG] =
+ "webIsolated=https://example.org^userContextId=3";
+ remoteTypes.private[URI_EXAMPLECOM] =
+ "webIsolated=https://example.com^privateBrowsingId=1";
+ remoteTypes.private[URI_EXAMPLEORG] =
+ "webIsolated=https://example.org^privateBrowsingId=1";
+ } else {
+ let web = Array(NUM_NAVIGATIONS).fill("web");
+ remoteTypes = {
+ initial: [...web, ...web],
+ regular: {},
+ 1: {},
+ 2: {},
+ 3: {},
+ private: {},
+ };
+ remoteTypes.regular[URI_EXAMPLECOM] = "web";
+ remoteTypes.regular[URI_EXAMPLEORG] = "web";
+ remoteTypes["1"][URI_EXAMPLECOM] = "web";
+ remoteTypes["1"][URI_EXAMPLEORG] = "web";
+ remoteTypes["2"][URI_EXAMPLECOM] = "web";
+ remoteTypes["2"][URI_EXAMPLEORG] = "web";
+ remoteTypes["3"][URI_EXAMPLECOM] = "web";
+ remoteTypes["3"][URI_EXAMPLEORG] = "web";
+ remoteTypes.private[URI_EXAMPLECOM] = "web";
+ remoteTypes.private[URI_EXAMPLEORG] = "web";
+ }
+}
+
+add_task(async function test_user_identity_simple() {
+ setupRemoteTypes();
+ /**
+ * For each test case
+ * - open regular, private and container tabs and load uri
+ * - in all the tabs, click on 4 links, going back each time in between clicks
+ * and verifying the remote type stays the same throughout
+ * - close tabs
+ */
+
+ for (var idx = 0; idx < TEST_CASES.length; idx++) {
+ var uri = TEST_CASES[idx];
+ info(`Will open ${uri} in different tabs`);
+
+ // Open uri without a container
+ let page_regular = await openURIInRegularTab(uri);
+ is(
+ page_regular.tab.linkedBrowser.remoteType,
+ remoteTypes.initial.shift(),
+ "correct remote type"
+ );
+
+ let pages_usercontexts = [];
+ for (
+ var user_context_id = 1;
+ user_context_id <= NUM_USER_CONTEXTS;
+ user_context_id++
+ ) {
+ let containerPage = await openURIInContainer(
+ uri,
+ window,
+ user_context_id.toString()
+ );
+ is(
+ containerPage.tab.linkedBrowser.remoteType,
+ remoteTypes.initial.shift(),
+ "correct remote type"
+ );
+ pages_usercontexts.push(containerPage);
+ }
+
+ // Open the same uri in a private browser
+ let page_private = await openURIInPrivateTab(uri);
+ is(
+ page_private.tab.linkedBrowser.remoteType,
+ remoteTypes.initial.shift(),
+ "correct remote type"
+ );
+
+ info(`Opened initial set of pages`);
+
+ for (const linkInfo of LINKS_INFO) {
+ info(
+ `Will make all tabs click on link ${linkInfo.uri} id ${linkInfo.id}`
+ );
+ info(`Will click on link ${linkInfo.uri} in regular tab`);
+ await clickOnLink(
+ page_regular.tab.linkedBrowser,
+ uri,
+ linkInfo,
+ "regular"
+ );
+
+ info(`Will click on link ${linkInfo.uri} in private tab`);
+ await clickOnLink(
+ page_private.tab.linkedBrowser,
+ uri,
+ linkInfo,
+ "private"
+ );
+
+ for (const page of pages_usercontexts) {
+ info(
+ `Will click on link ${linkInfo.uri} in container ${page.user_context_id}`
+ );
+ await clickOnLink(
+ page.tab.linkedBrowser,
+ uri,
+ linkInfo,
+ page.user_context_id.toString()
+ );
+ }
+ }
+
+ // Close all the tabs
+ pages_usercontexts.forEach(page => {
+ BrowserTestUtils.removeTab(page.tab);
+ });
+ BrowserTestUtils.removeTab(page_regular.tab);
+ BrowserTestUtils.removeTab(page_private.tab);
+ }
+});
+
+async function clickOnLink(aBrowser, aCurrURI, aLinkInfo, aIdxForRemoteTypes) {
+ var remoteTypeBeforeNavigation = aBrowser.remoteType;
+ var currRemoteType;
+
+ // Add a listener
+ initXulFrameLoaderCreatedCounter(xulFrameLoaderCreatedCounter);
+ aBrowser.ownerGlobal.gBrowser.addEventListener(
+ "XULFrameLoaderCreated",
+ handleEventLocal
+ );
+
+ // Retrieve the expected remote type
+ var expectedRemoteType = remoteTypes[aIdxForRemoteTypes][aLinkInfo.uri];
+
+ // Click on the link
+ info(`Clicking on link, expected remote type= ${expectedRemoteType}`);
+ let newTabLoaded = BrowserTestUtils.waitForNewTab(
+ aBrowser.ownerGlobal.gBrowser,
+ aLinkInfo.uri,
+ true
+ );
+ SpecialPowers.spawn(aBrowser, [aLinkInfo.id], link_id => {
+ content.document.getElementById(link_id).click();
+ });
+
+ // Wait for the new tab to be opened
+ info(`About to wait for the clicked link to load in browser`);
+ let newTab = await newTabLoaded;
+
+ // Check remote type, once we have opened a new tab
+ info(`Finished waiting for the clicked link to load in browser`);
+ currRemoteType = newTab.linkedBrowser.remoteType;
+ is(currRemoteType, expectedRemoteType, "Got correct remote type");
+
+ // Verify firing of XULFrameLoaderCreated event
+ info(
+ `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedCounter.numCalledSoFar}
+ time(s) for tab ${aIdxForRemoteTypes} when clicking on ${aLinkInfo.id} from page ${aCurrURI}`
+ );
+ var numExpected;
+ if (!gFissionBrowser && aLinkInfo.id.includes("noopener")) {
+ numExpected = 1;
+ } else {
+ numExpected = currRemoteType == remoteTypeBeforeNavigation ? 1 : 2;
+ }
+ info(
+ `num XULFrameLoaderCreated events expected ${numExpected}, curr ${currRemoteType} prev ${remoteTypeBeforeNavigation}`
+ );
+ is(
+ xulFrameLoaderCreatedCounter.numCalledSoFar,
+ numExpected,
+ `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedCounter.numCalledSoFar}
+ time(s) for tab ${aIdxForRemoteTypes} when clicking on ${aLinkInfo.id} from page ${aCurrURI}`
+ );
+
+ // Remove the event listener
+ aBrowser.ownerGlobal.gBrowser.removeEventListener(
+ "XULFrameLoaderCreated",
+ handleEventLocal
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+}
diff --git a/browser/base/content/test/tabs/browser_originalURI.js b/browser/base/content/test/tabs/browser_originalURI.js
new file mode 100644
index 0000000000..4c644832e2
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_originalURI.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/. */
+
+/*
+ These tests ensure the originalURI property of the <browser> element
+ has consistent behavior when the URL of a <browser> changes.
+*/
+
+const EXAMPLE_URL = "https://example.com/some/path";
+const EXAMPLE_URL_2 = "http://mochi.test:8888/";
+
+/*
+ Load a page with no redirect.
+*/
+add_task(async function no_redirect() {
+ await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => {
+ info("Page loaded.");
+ assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI);
+ });
+});
+
+/*
+ Load a page, go to another page, then go back and forth.
+*/
+add_task(async function back_and_forth() {
+ await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => {
+ info("Page loaded.");
+
+ info("Try loading another page.");
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ EXAMPLE_URL_2
+ );
+ BrowserTestUtils.loadURIString(browser, EXAMPLE_URL_2);
+ await pageLoadPromise;
+ info("Other page finished loading.");
+ assertUrlEqualsOriginalURI(EXAMPLE_URL_2, browser.originalURI);
+
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goBack();
+ info("Go back.");
+ await pageShowPromise;
+
+ info("Loaded previous page.");
+ assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI);
+
+ pageShowPromise = BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ browser.goForward();
+ info("Go forward.");
+ await pageShowPromise;
+
+ info("Loaded next page.");
+ assertUrlEqualsOriginalURI(EXAMPLE_URL_2, browser.originalURI);
+ });
+});
+
+/*
+ Redirect using the Location interface.
+*/
+add_task(async function location_href() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ EXAMPLE_URL
+ );
+ info("Loading page with location.href interface.");
+ await SpecialPowers.spawn(browser, [EXAMPLE_URL], href => {
+ content.document.location.href = href;
+ });
+ await pageLoadPromise;
+ info("Page loaded.");
+ assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI);
+ });
+});
+
+/*
+ Redirect using History API, should not update the originalURI.
+*/
+add_task(async function push_state() {
+ await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => {
+ info("Page loaded.");
+
+ info("Pushing state via History API.");
+ await SpecialPowers.spawn(browser, [], () => {
+ let newUrl = content.document.location.href + "/after?page=images";
+ content.history.pushState(null, "", newUrl);
+ });
+ Assert.equal(
+ browser.currentURI.displaySpec,
+ EXAMPLE_URL + "/after?page=images",
+ "Current URI should be modified by push state."
+ );
+ assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI);
+ });
+});
+
+/*
+ Redirect using the <meta> tag.
+*/
+add_task(async function meta_tag() {
+ let URL = httpURL("redirect_via_meta_tag.html");
+ await BrowserTestUtils.withNewTab(URL, async browser => {
+ info("Page loaded.");
+
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ EXAMPLE_URL_2
+ );
+ await pageLoadPromise;
+ info("Redirected to ", EXAMPLE_URL_2);
+ assertUrlEqualsOriginalURI(URL, browser.originalURI);
+ });
+});
+
+/*
+ Redirect using header from a server.
+*/
+add_task(async function server_header() {
+ let URL = httpURL("redirect_via_header.html");
+ await BrowserTestUtils.withNewTab(URL, async browser => {
+ info("Page loaded.");
+
+ Assert.equal(
+ browser.currentURI.displaySpec,
+ EXAMPLE_URL,
+ `Browser should be re-directed to ${EXAMPLE_URL}`
+ );
+ assertUrlEqualsOriginalURI(URL, browser.originalURI);
+ });
+});
+
+/*
+ Load a page with an iFrame and then try having the
+ iFrame load another page.
+*/
+add_task(async function page_with_iframe() {
+ let URL = httpURL("page_with_iframe.html");
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ info("Blank page loaded.");
+
+ info("Load URL.");
+ BrowserTestUtils.loadURIString(browser, URL);
+ // Make sure the iFrame is finished loading.
+ await BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ "https://example.com/another/site"
+ );
+ info("iFrame finished loading.");
+ assertUrlEqualsOriginalURI(URL, browser.originalURI);
+
+ info("Change location of the iframe.");
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ EXAMPLE_URL_2
+ );
+ await SpecialPowers.spawn(browser, [EXAMPLE_URL_2], url => {
+ content.document.getElementById("hidden-iframe").contentWindow.location =
+ url;
+ });
+ await pageLoadPromise;
+ info("iFrame finished loading.");
+ assertUrlEqualsOriginalURI(URL, browser.originalURI);
+ });
+});
+
+function assertUrlEqualsOriginalURI(url, originalURI) {
+ let uri = Services.io.newURI(url);
+ Assert.ok(
+ uri.equals(gBrowser.selectedBrowser.originalURI),
+ `URI - ${uri.displaySpec} is not equal to the originalURI - ${originalURI.displaySpec}`
+ );
+}
diff --git a/browser/base/content/test/tabs/browser_overflowScroll.js b/browser/base/content/test/tabs/browser_overflowScroll.js
new file mode 100644
index 0000000000..e30311bef1
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_overflowScroll.js
@@ -0,0 +1,111 @@
+"use strict";
+
+requestLongerTimeout(2);
+
+/**
+ * Tests that scrolling the tab strip via the scroll buttons scrolls the right
+ * amount in non-smoothscroll mode.
+ */
+add_task(async function () {
+ let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+ let scrollbox = arrowScrollbox.scrollbox;
+
+ let rect = ele => ele.getBoundingClientRect();
+ let width = ele => rect(ele).width;
+
+ let left = ele => rect(ele).left;
+ let right = ele => rect(ele).right;
+ let isLeft = (ele, msg) => is(left(ele), left(scrollbox), msg);
+ let isRight = (ele, msg) => is(right(ele), right(scrollbox), msg);
+ let elementFromPoint = x => arrowScrollbox._elementFromPoint(x);
+ let nextLeftElement = () => elementFromPoint(left(scrollbox) - 1);
+ let nextRightElement = () => elementFromPoint(right(scrollbox) + 1);
+ let firstScrollable = () => gBrowser.tabs[gBrowser._numPinnedTabs];
+ let waitForNextFrame = async function () {
+ await new Promise(requestAnimationFrame);
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ };
+
+ await BrowserTestUtils.overflowTabs(registerCleanupFunction, window, {
+ overflowAtStart: false,
+ overflowTabFactor: 3,
+ });
+
+ gBrowser.pinTab(gBrowser.tabs[0]);
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
+ });
+
+ ok(
+ arrowScrollbox.hasAttribute("overflowing"),
+ "Tab strip should be overflowing"
+ );
+
+ let upButton = arrowScrollbox._scrollButtonUp;
+ let downButton = arrowScrollbox._scrollButtonDown;
+ let element;
+
+ gBrowser.selectedTab = firstScrollable();
+ await TestUtils.waitForTick();
+
+ ok(
+ left(scrollbox) <= left(firstScrollable()),
+ "Selecting the first tab scrolls it into view " +
+ "(" +
+ left(scrollbox) +
+ " <= " +
+ left(firstScrollable()) +
+ ")"
+ );
+
+ element = nextRightElement();
+ EventUtils.synthesizeMouseAtCenter(downButton, {});
+ await waitForNextFrame();
+ isRight(element, "Scrolled one tab to the right with a single click");
+
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ await waitForNextFrame();
+ ok(
+ right(gBrowser.selectedTab) <= right(scrollbox),
+ "Selecting the last tab scrolls it into view " +
+ "(" +
+ right(gBrowser.selectedTab) +
+ " <= " +
+ right(scrollbox) +
+ ")"
+ );
+
+ element = nextLeftElement();
+ EventUtils.synthesizeMouseAtCenter(upButton, {});
+ await waitForNextFrame();
+ isLeft(element, "Scrolled one tab to the left with a single click");
+
+ let elementPoint = left(scrollbox) - width(scrollbox);
+ element = elementFromPoint(elementPoint);
+ element = element.nextElementSibling;
+
+ EventUtils.synthesizeMouseAtCenter(upButton, { clickCount: 2 });
+ await waitForNextFrame();
+ await BrowserTestUtils.waitForCondition(
+ () => !gBrowser.tabContainer.arrowScrollbox._isScrolling
+ );
+ isLeft(element, "Scrolled one page of tabs with a double click");
+
+ EventUtils.synthesizeMouseAtCenter(upButton, { clickCount: 3 });
+ await waitForNextFrame();
+ var firstScrollableLeft = left(firstScrollable());
+ ok(
+ left(scrollbox) <= firstScrollableLeft,
+ "Scrolled to the start with a triple click " +
+ "(" +
+ left(scrollbox) +
+ " <= " +
+ firstScrollableLeft +
+ ")"
+ );
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[0]);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js b/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js
new file mode 100644
index 0000000000..a6b7f96410
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js
@@ -0,0 +1,156 @@
+"use strict";
+
+add_task(async function doCheckPasteEventAtMiddleClickOnAnchorElement() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.opentabfor.middleclick", true],
+ ["middlemouse.paste", true],
+ ["middlemouse.contentLoadURL", false],
+ ["general.autoScroll", false],
+ ],
+ });
+
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ "Text in the clipboard",
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString("Text in the clipboard");
+ },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+
+ is(
+ gBrowser.tabs.length,
+ 1,
+ "Number of tabs should be 1 at starting this test #1"
+ );
+
+ let pageURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ pageURL = `${pageURL}file_anchor_elements.html`;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL);
+
+ let pasteEventCount = 0;
+ BrowserTestUtils.addContentEventListener(
+ gBrowser.selectedBrowser,
+ "paste",
+ () => {
+ ++pasteEventCount;
+ }
+ );
+
+ // Click the usual link.
+ ok(true, "Clicking on usual link...");
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://example.com/#a_with_href",
+ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#a_with_href",
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ let openTabForUsualLink = await newTabPromise;
+ is(
+ openTabForUsualLink.linkedBrowser.currentURI.spec,
+ "https://example.com/#a_with_href",
+ "Middle click should open site to correct url at clicking on usual link"
+ );
+ is(
+ pasteEventCount,
+ 0,
+ "paste event should be suppressed when clicking on usual link"
+ );
+
+ // Click the link in editing host.
+ is(
+ gBrowser.tabs.length,
+ 3,
+ "Number of tabs should be 3 at starting this test #2"
+ );
+ ok(true, "Clicking on editable link...");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#editable_a_with_href",
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ await TestUtils.waitForCondition(
+ () => pasteEventCount >= 1,
+ "Waiting for paste event caused by clicking on editable link"
+ );
+ is(
+ pasteEventCount,
+ 1,
+ "paste event should be suppressed when clicking on editable link"
+ );
+ is(
+ gBrowser.tabs.length,
+ 3,
+ "Clicking on editable link shouldn't open new tab"
+ );
+
+ // Click the link in non-editable area in editing host.
+ ok(true, "Clicking on non-editable link in an editing host...");
+ newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://example.com/#non-editable_a_with_href",
+ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#non-editable_a_with_href",
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ let openTabForNonEditableLink = await newTabPromise;
+ is(
+ openTabForNonEditableLink.linkedBrowser.currentURI.spec,
+ "https://example.com/#non-editable_a_with_href",
+ "Middle click should open site to correct url at clicking on non-editable link in an editing host."
+ );
+ is(
+ pasteEventCount,
+ 1,
+ "paste event should be suppressed when clicking on non-editable link in an editing host"
+ );
+
+ // Click the <a> element without href attribute.
+ is(
+ gBrowser.tabs.length,
+ 4,
+ "Number of tabs should be 4 at starting this test #3"
+ );
+ ok(true, "Clicking on anchor element without href...");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#a_with_name",
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ await TestUtils.waitForCondition(
+ () => pasteEventCount >= 2,
+ "Waiting for paste event caused by clicking on anchor element without href"
+ );
+ is(
+ pasteEventCount,
+ 2,
+ "paste event should be suppressed when clicking on anchor element without href"
+ );
+ is(
+ gBrowser.tabs.length,
+ 4,
+ "Clicking on anchor element without href shouldn't open new tab"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(openTabForUsualLink);
+ BrowserTestUtils.removeTab(openTabForNonEditableLink);
+});
diff --git a/browser/base/content/test/tabs/browser_pinnedTabs.js b/browser/base/content/test/tabs/browser_pinnedTabs.js
new file mode 100644
index 0000000000..856a08093d
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_pinnedTabs.js
@@ -0,0 +1,97 @@
+var tabs;
+
+function index(tab) {
+ return Array.prototype.indexOf.call(gBrowser.tabs, tab);
+}
+
+function indexTest(tab, expectedIndex, msg) {
+ var diag = "tab " + tab + " should be at index " + expectedIndex;
+ if (msg) {
+ msg = msg + " (" + diag + ")";
+ } else {
+ msg = diag;
+ }
+ is(index(tabs[tab]), expectedIndex, msg);
+}
+
+function PinUnpinHandler(tab, eventName) {
+ this.eventCount = 0;
+ var self = this;
+ tab.addEventListener(
+ eventName,
+ function () {
+ self.eventCount++;
+ },
+ { capture: true, once: true }
+ );
+ gBrowser.tabContainer.addEventListener(
+ eventName,
+ function (e) {
+ if (e.originalTarget == tab) {
+ self.eventCount++;
+ }
+ },
+ { capture: true, once: true }
+ );
+}
+
+function test() {
+ tabs = [
+ gBrowser.selectedTab,
+ BrowserTestUtils.addTab(gBrowser),
+ BrowserTestUtils.addTab(gBrowser),
+ BrowserTestUtils.addTab(gBrowser),
+ ];
+ indexTest(0, 0);
+ indexTest(1, 1);
+ indexTest(2, 2);
+ indexTest(3, 3);
+
+ // Discard one of the test tabs to verify that pinning/unpinning
+ // discarded tabs does not regress (regression test for Bug 1852391).
+ gBrowser.discardBrowser(tabs[1], true);
+
+ var eh = new PinUnpinHandler(tabs[3], "TabPinned");
+ gBrowser.pinTab(tabs[3]);
+ is(eh.eventCount, 2, "TabPinned event should be fired");
+ indexTest(0, 1);
+ indexTest(1, 2);
+ indexTest(2, 3);
+ indexTest(3, 0);
+
+ eh = new PinUnpinHandler(tabs[1], "TabPinned");
+ gBrowser.pinTab(tabs[1]);
+ is(eh.eventCount, 2, "TabPinned event should be fired");
+ indexTest(0, 2);
+ indexTest(1, 1);
+ indexTest(2, 3);
+ indexTest(3, 0);
+
+ gBrowser.moveTabTo(tabs[3], 3);
+ indexTest(3, 1, "shouldn't be able to mix a pinned tab into normal tabs");
+
+ gBrowser.moveTabTo(tabs[2], 0);
+ indexTest(2, 2, "shouldn't be able to mix a normal tab into pinned tabs");
+
+ eh = new PinUnpinHandler(tabs[1], "TabUnpinned");
+ gBrowser.unpinTab(tabs[1]);
+ is(eh.eventCount, 2, "TabUnpinned event should be fired");
+ indexTest(
+ 1,
+ 1,
+ "unpinning a tab should move a tab to the start of normal tabs"
+ );
+
+ eh = new PinUnpinHandler(tabs[3], "TabUnpinned");
+ gBrowser.unpinTab(tabs[3]);
+ is(eh.eventCount, 2, "TabUnpinned event should be fired");
+ indexTest(
+ 3,
+ 0,
+ "unpinning a tab should move a tab to the start of normal tabs"
+ );
+
+ gBrowser.removeTab(tabs[1]);
+ gBrowser.removeTab(tabs[2]);
+ gBrowser.removeTab(tabs[3]);
+}
diff --git a/browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js b/browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js
new file mode 100644
index 0000000000..04420814b0
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function index(tab) {
+ return Array.prototype.indexOf.call(gBrowser.tabs, tab);
+}
+
+async function testNewTabPosition(expectedPosition, modifiers = {}) {
+ let opening = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link",
+ modifiers,
+ gBrowser.selectedBrowser
+ );
+ let newtab = await opening;
+ is(index(newtab), expectedPosition, "clicked tab is in correct position");
+ return newtab;
+}
+
+// Test that a tab opened from a pinned tab is not in the pinned region.
+add_task(async function test_pinned_content_click() {
+ let testUri =
+ 'data:text/html;charset=utf-8,<a href="http://mochi.test:8888/" target="_blank" id="link">link</a>';
+ let tabs = [
+ gBrowser.selectedTab,
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, testUri),
+ BrowserTestUtils.addTab(gBrowser),
+ ];
+ gBrowser.pinTab(tabs[1]);
+ gBrowser.pinTab(tabs[2]);
+
+ // First test new active tabs open at the start of non-pinned tabstrip.
+ let newtab1 = await testNewTabPosition(2);
+ // Switch back to our test tab.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+ let newtab2 = await testNewTabPosition(2);
+
+ gBrowser.removeTab(newtab1);
+ gBrowser.removeTab(newtab2);
+
+ // Second test new background tabs open in order.
+ let modifiers =
+ AppConstants.platform == "macosx" ? { metaKey: true } : { ctrlKey: true };
+ await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+
+ newtab1 = await testNewTabPosition(2, modifiers);
+ newtab2 = await testNewTabPosition(3, modifiers);
+
+ gBrowser.removeTab(tabs[1]);
+ gBrowser.removeTab(tabs[2]);
+ gBrowser.removeTab(newtab1);
+ gBrowser.removeTab(newtab2);
+});
diff --git a/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js b/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js
new file mode 100644
index 0000000000..fbcd0bb492
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ function testState(aPinned) {
+ function elemAttr(id, attr) {
+ return document.getElementById(id).getAttribute(attr);
+ }
+
+ is(
+ elemAttr("key_close", "disabled"),
+ "",
+ "key_closed should always be enabled"
+ );
+ is(
+ elemAttr("menu_close", "key"),
+ "key_close",
+ "menu_close should always have key_close set"
+ );
+ }
+
+ let unpinnedTab = gBrowser.selectedTab;
+ ok(!unpinnedTab.pinned, "We should have started with a regular tab selected");
+
+ testState(false);
+
+ let pinnedTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.pinTab(pinnedTab);
+
+ // Just pinning the tab shouldn't change the key state.
+ testState(false);
+
+ // Test key state after selecting a tab.
+ gBrowser.selectedTab = pinnedTab;
+ testState(true);
+
+ gBrowser.selectedTab = unpinnedTab;
+ testState(false);
+
+ gBrowser.selectedTab = pinnedTab;
+ testState(true);
+
+ // Test the key state after un/pinning the tab.
+ gBrowser.unpinTab(pinnedTab);
+ testState(false);
+
+ gBrowser.pinTab(pinnedTab);
+ testState(true);
+
+ // Test that accel+w in a pinned tab selects the next tab.
+ let pinnedTab2 = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.pinTab(pinnedTab2);
+ gBrowser.selectedTab = pinnedTab;
+
+ EventUtils.synthesizeKey("w", { accelKey: true });
+ is(gBrowser.tabs.length, 3, "accel+w in a pinned tab didn't close it");
+ is(
+ gBrowser.selectedTab,
+ unpinnedTab,
+ "accel+w in a pinned tab selected the first unpinned tab"
+ );
+
+ // Test the key state after removing the tab.
+ gBrowser.removeTab(pinnedTab);
+ gBrowser.removeTab(pinnedTab2);
+ testState(false);
+
+ finish();
+}
diff --git a/browser/base/content/test/tabs/browser_positional_attributes.js b/browser/base/content/test/tabs/browser_positional_attributes.js
new file mode 100644
index 0000000000..619c5cc517
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_positional_attributes.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var tabs = [];
+
+function addTab(aURL) {
+ tabs.push(
+ BrowserTestUtils.addTab(gBrowser, aURL, {
+ skipAnimation: true,
+ })
+ );
+}
+
+function switchTab(index) {
+ return BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[index]);
+}
+
+function testAttrib(tabIndex, attrib, expected) {
+ is(
+ gBrowser.tabs[tabIndex].hasAttribute(attrib),
+ expected,
+ `tab #${tabIndex} should${
+ expected ? "" : "n't"
+ } have the ${attrib} attribute`
+ );
+}
+
+add_setup(async function () {
+ is(gBrowser.tabs.length, 1, "one tab is open initially");
+
+ addTab("http://mochi.test:8888/#0");
+ addTab("http://mochi.test:8888/#1");
+ addTab("http://mochi.test:8888/#2");
+ addTab("http://mochi.test:8888/#3");
+
+ is(gBrowser.tabs.length, 5, "five tabs are open after setup");
+});
+
+// Add several new tabs in sequence, hiding some, to ensure that the
+// correct attributes get set
+add_task(async function test() {
+ testAttrib(0, "visuallyselected", true);
+
+ await switchTab(2);
+
+ testAttrib(2, "visuallyselected", true);
+});
+
+add_task(async function test_pinning() {
+ await switchTab(3);
+ testAttrib(3, "visuallyselected", true);
+ // Causes gBrowser.tabs to change indices
+ gBrowser.pinTab(gBrowser.tabs[3]);
+ testAttrib(0, "visuallyselected", true);
+});
+
+add_task(function cleanup() {
+ tabs.forEach(gBrowser.removeTab, gBrowser);
+});
diff --git a/browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js b/browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js
new file mode 100644
index 0000000000..698cf82022
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js
@@ -0,0 +1,89 @@
+"use strict";
+
+const ZOOM_CHANGE_TOPIC = "browser-fullZoom:location-change";
+
+/**
+ * Helper to check the zoom level of the preloaded browser
+ */
+async function checkPreloadedZoom(level, message) {
+ // Clear up any previous preloaded to test a fresh version
+ NewTabPagePreloading.removePreloadedBrowser(window);
+ NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
+
+ // Wait for zoom handling of preloaded
+ const browser = gBrowser.preloadedBrowser;
+ await new Promise(resolve =>
+ Services.obs.addObserver(function obs(subject) {
+ if (subject === browser) {
+ Services.obs.removeObserver(obs, ZOOM_CHANGE_TOPIC);
+ resolve();
+ }
+ }, ZOOM_CHANGE_TOPIC)
+ );
+
+ is(browser.fullZoom.toFixed(2), level, message);
+
+ // Clean up for other tests
+ NewTabPagePreloading.removePreloadedBrowser(window);
+}
+
+add_task(async function test_default_zoom() {
+ await checkPreloadedZoom("1.00", "default preloaded zoom is 1");
+});
+
+/**
+ * Helper to open about:newtab and zoom then check matching preloaded zoom
+ */
+async function zoomNewTab(changeZoom, message) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:newtab"
+ );
+ changeZoom();
+ const level = tab.linkedBrowser.fullZoom.toFixed(2);
+ BrowserTestUtils.removeTab(tab);
+
+ // Wait for the the update of the full-zoom content pref value, that happens
+ // asynchronously after changing the zoom level.
+ let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return new Promise(resolve => {
+ cps2.getByDomainAndName(
+ "about:newtab",
+ "browser.content.full-zoom",
+ null,
+ {
+ handleResult(pref) {
+ resolve(level == pref.value);
+ },
+ handleCompletion() {
+ console.log("handleCompletion");
+ },
+ }
+ );
+ });
+ });
+
+ await checkPreloadedZoom(level, `${message}: ${level}`);
+}
+
+add_task(async function test_preloaded_zoom_out() {
+ await zoomNewTab(() => FullZoom.reduce(), "zoomed out applied to preloaded");
+});
+
+add_task(async function test_preloaded_zoom_in() {
+ await zoomNewTab(() => {
+ FullZoom.enlarge();
+ FullZoom.enlarge();
+ }, "zoomed in applied to preloaded");
+});
+
+add_task(async function test_preloaded_zoom_default() {
+ await zoomNewTab(
+ () => FullZoom.reduce(),
+ "zoomed back to default applied to preloaded"
+ );
+});
diff --git a/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js b/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js
new file mode 100644
index 0000000000..86ef66c936
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js
@@ -0,0 +1,212 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests to ensure that Mozilla Privileged Webpages load in the privileged
+ * mozilla web content process. Normal http web pages should load in the web
+ * content process.
+ * Ref: Bug 1539595.
+ */
+
+// High and Low Privilege
+const TEST_HIGH1 = "https://example.org/";
+const TEST_HIGH2 = "https://test1.example.org/";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_LOW1 = "http://example.org/";
+const TEST_LOW2 = "https://example.com/";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
+ ["browser.tabs.remote.separatedMozillaDomains", "example.org"],
+ ["dom.ipc.processCount.privilegedmozilla", 1],
+ ],
+ });
+});
+
+/*
+ * Test to ensure that the tabs open in privileged mozilla content process. We
+ * will first open a page that acts as a reference to the privileged mozilla web
+ * content process. With the reference, we can then open other links in a new tab
+ * and ensure that the new tab opens in the same privileged mozilla content process
+ * as our reference.
+ */
+add_task(async function webpages_in_privileged_content_process() {
+ Services.ppmm.releaseCachedProcesses();
+
+ await BrowserTestUtils.withNewTab(TEST_HIGH1, async function (browser1) {
+ checkBrowserRemoteType(browser1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE);
+
+ // Note the processID for about:newtab for comparison later.
+ let privilegedPid = browser1.frameLoader.remoteTab.osPid;
+
+ for (let url of [
+ TEST_HIGH1,
+ `${TEST_HIGH1}#foo`,
+ `${TEST_HIGH1}?q=foo`,
+ TEST_HIGH2,
+ `${TEST_HIGH2}#foo`,
+ `${TEST_HIGH2}?q=foo`,
+ ]) {
+ await BrowserTestUtils.withNewTab(url, async function (browser2) {
+ is(
+ browser2.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that privileged pages are in the same privileged mozilla content process."
+ );
+ });
+ }
+ });
+
+ Services.ppmm.releaseCachedProcesses();
+});
+
+/*
+ * Test to ensure that a process switch occurs when navigating between normal
+ * web pages and unprivileged pages in the same tab.
+ */
+add_task(async function process_switching_through_loading_in_the_same_tab() {
+ Services.ppmm.releaseCachedProcesses();
+
+ await BrowserTestUtils.withNewTab(TEST_LOW1, async function (browser) {
+ checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+ for (let [url, remoteType] of [
+ [TEST_HIGH1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE],
+ [TEST_HIGH1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE],
+ [TEST_HIGH1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE],
+ [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE],
+ [`${TEST_HIGH1}#foo`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE],
+ [`${TEST_HIGH1}#bar`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE],
+ [`${TEST_HIGH1}#baz`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE],
+ [`${TEST_HIGH1}?q=foo`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE],
+ [`${TEST_HIGH1}?q=bar`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE],
+ [`${TEST_HIGH1}?q=baz`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE],
+ [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE],
+ ]) {
+ BrowserTestUtils.loadURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+ checkBrowserRemoteType(browser, remoteType);
+ }
+ });
+
+ Services.ppmm.releaseCachedProcesses();
+});
+
+/*
+ * Test to ensure that a process switch occurs when navigating between normal
+ * web pages and privileged pages using the browser's navigation features
+ * such as history and location change.
+ */
+add_task(async function process_switching_through_navigation_features() {
+ Services.ppmm.releaseCachedProcesses();
+
+ await BrowserTestUtils.withNewTab(TEST_HIGH1, async function (browser) {
+ checkBrowserRemoteType(browser, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE);
+
+ // Note the processID for about:newtab for comparison later.
+ let privilegedPid = browser.frameLoader.remoteTab.osPid;
+
+ // Check that about:newtab opened from JS in about:newtab page is in the same process.
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_HIGH1,
+ true
+ );
+ await SpecialPowers.spawn(browser, [TEST_HIGH1], uri => {
+ content.open(uri, "_blank");
+ });
+ let newTab = await promiseTabOpened;
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(newTab);
+ });
+ browser = newTab.linkedBrowser;
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that new tab opened from privileged page is loaded in privileged mozilla content process."
+ );
+
+ // Check that reload does not break the privileged mozilla content process affinity.
+ BrowserReload();
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_HIGH1);
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that privileged page is still in privileged mozilla content process after reload."
+ );
+
+ // Load http webpage
+ BrowserTestUtils.loadURIString(browser, TEST_LOW1);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_LOW1);
+ checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+ // Check that using the history back feature switches back to privileged mozilla content process.
+ let promiseLocation = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_HIGH1
+ );
+ browser.goBack();
+ await promiseLocation;
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that privileged page is still in privileged mozilla content process after history goBack."
+ );
+
+ // Check that using the history forward feature switches back to the web content process.
+ promiseLocation = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_LOW1
+ );
+ browser.goForward();
+ await promiseLocation;
+ checkBrowserRemoteType(
+ browser,
+ E10SUtils.WEB_REMOTE_TYPE,
+ "Check that tab runs in the web content process after using history goForward."
+ );
+
+ // Check that goto history index does not break the affinity.
+ promiseLocation = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_HIGH1
+ );
+ browser.gotoIndex(0);
+ await promiseLocation;
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that privileged page is in privileged mozilla content process after history gotoIndex."
+ );
+
+ BrowserTestUtils.loadURIString(browser, TEST_LOW2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_LOW2);
+ checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+ // Check that location change causes a change in process type as well.
+ await SpecialPowers.spawn(browser, [TEST_HIGH1], uri => {
+ content.location = uri;
+ });
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_HIGH1);
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that privileged page is in privileged mozilla content process after location change."
+ );
+ });
+
+ Services.ppmm.releaseCachedProcesses();
+});
diff --git a/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js b/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js
new file mode 100644
index 0000000000..648cda9332
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+const kButton = document.getElementById("reload-button");
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.fixup.dns_first_for_single_words", true]],
+ });
+
+ // Create an engine to use for the test.
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://example.com/",
+ search_url_get_params: "q={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+});
+
+/*
+ * When loading a keyword search as a result of an unknown host error,
+ * check that we can stop the load.
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=235825
+ */
+add_task(async function test_unknown_host() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ const kNonExistingHost = "idontreallyexistonthisnetwork";
+ let searchPromise = BrowserTestUtils.browserStarted(
+ browser,
+ Services.uriFixup.keywordToURI(kNonExistingHost).preferredURI.spec
+ );
+
+ gURLBar.value = kNonExistingHost;
+ gURLBar.select();
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await searchPromise;
+ // With parent initiated loads, we need to give XULBrowserWindow
+ // time to process the STATE_START event and set the attribute to true.
+ await new Promise(resolve => executeSoon(resolve));
+
+ ok(kButton.hasAttribute("displaystop"), "Should be showing stop");
+
+ await TestUtils.waitForCondition(
+ () => !kButton.hasAttribute("displaystop")
+ );
+ ok(
+ !kButton.hasAttribute("displaystop"),
+ "Should no longer be showing stop after search"
+ );
+ });
+});
+
+/*
+ * When NOT loading a keyword search as a result of an unknown host error,
+ * check that the stop button goes back to being a reload button.
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1591183
+ */
+add_task(async function test_unknown_host_without_search() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ const kNonExistingHost = "idontreallyexistonthisnetwork.example.com";
+ let searchPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://" + kNonExistingHost + "/",
+ true /* want an error page */
+ );
+ gURLBar.value = kNonExistingHost;
+ gURLBar.select();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ await TestUtils.waitForCondition(
+ () => !kButton.hasAttribute("displaystop")
+ );
+ ok(
+ !kButton.hasAttribute("displaystop"),
+ "Should not be showing stop on error page"
+ );
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_relatedTabs_reset.js b/browser/base/content/test/tabs/browser_relatedTabs_reset.js
new file mode 100644
index 0000000000..531a9e723d
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_relatedTabs_reset.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/. */
+
+add_task(async function () {
+ is(gBrowser.tabs.length, 1, "one tab is open initially");
+
+ const TestPage =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/dummy_page.html";
+
+ // Add several new tabs in sequence
+ let tabs = [];
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+
+ function getPrincipal(url, attrs) {
+ let uri = Services.io.newURI(url);
+ if (!attrs) {
+ attrs = {};
+ }
+ return Services.scriptSecurityManager.createContentPrincipal(uri, attrs);
+ }
+
+ function addTab(aURL, aReferrer) {
+ let referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ aReferrer
+ );
+ let triggeringPrincipal = getPrincipal(aURL);
+ let tab = BrowserTestUtils.addTab(gBrowser, aURL, {
+ referrerInfo,
+ triggeringPrincipal,
+ });
+ tabs.push(tab);
+ return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ }
+
+ function loadTab(tab, url) {
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ info("Loading page: " + url);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ return loaded;
+ }
+
+ function testPosition(tabNum, expectedPosition, msg) {
+ is(
+ Array.prototype.indexOf.call(gBrowser.tabs, tabs[tabNum]),
+ expectedPosition,
+ msg
+ );
+ }
+
+ // Initial selected tab
+ await addTab("http://mochi.test:8888/#0");
+ testPosition(0, 1, "Initial tab opened in position 1");
+ gBrowser.selectedTab = tabs[0];
+
+ // Related tabs
+ await addTab("http://mochi.test:8888/#1", gBrowser.currentURI);
+ testPosition(1, 2, "Related tab was opened to the far right");
+
+ await addTab("http://mochi.test:8888/#2", gBrowser.currentURI);
+ testPosition(2, 3, "Related tab was opened to the far right");
+
+ // Load a new page
+ await loadTab(tabs[0], TestPage);
+
+ // Add a new related tab after the page load
+ await addTab("http://mochi.test:8888/#3", gBrowser.currentURI);
+ testPosition(
+ 3,
+ 2,
+ "Tab opened to the right of initial tab after system navigation"
+ );
+
+ tabs.forEach(gBrowser.removeTab, gBrowser);
+});
diff --git a/browser/base/content/test/tabs/browser_reload_deleted_file.js b/browser/base/content/test/tabs/browser_reload_deleted_file.js
new file mode 100644
index 0000000000..2051dbfac7
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_reload_deleted_file.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const uuidGenerator = Services.uuid;
+
+const DUMMY_FILE = "dummy_page.html";
+
+// Test for bug 1327942.
+add_task(async function () {
+ // Copy dummy page to unique file in TmpD, so that we can safely delete it.
+ let dummyPage = getChromeDir(getResolvedURI(gTestPath));
+ dummyPage.append(DUMMY_FILE);
+ let disappearingPage = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ let uniqueName = uuidGenerator.generateUUID().toString();
+ dummyPage.copyTo(disappearingPage, uniqueName);
+ disappearingPage.append(uniqueName);
+
+ // Get file:// URI for new page and load in a new tab.
+ const uriString = Services.io.newFileURI(disappearingPage).spec;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString);
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ // Delete the page, simulate a click of the reload button and check that we
+ // get a neterror page.
+ disappearingPage.remove(false);
+ document.getElementById("reload-button").doCommand();
+ await BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ ok(
+ content.document.documentURI.startsWith("about:neterror"),
+ "Check that a neterror page was loaded."
+ );
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_removeTabsToTheEnd.js b/browser/base/content/test/tabs/browser_removeTabsToTheEnd.js
new file mode 100644
index 0000000000..a9540f708b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_removeTabsToTheEnd.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function removeTabsToTheEnd() {
+ // Add three new tabs after the original tab. Pin the second one.
+ let firstTab = await addTab();
+ let pinnedTab = await addTab();
+ let lastTab = await addTab();
+ gBrowser.pinTab(pinnedTab);
+
+ // Check that there is only one closable tab from firstTab to the end
+ is(
+ gBrowser.getTabsToTheEndFrom(firstTab).length,
+ 1,
+ "One unpinned tab towards the end"
+ );
+
+ // Remove tabs to the end
+ gBrowser.removeTabsToTheEndFrom(firstTab);
+
+ ok(!firstTab.closing, "First tab is not closing");
+ ok(!pinnedTab.closing, "Pinned tab is not closing");
+ ok(lastTab.closing, "Last tab is closing");
+
+ // cleanup
+ for (let tab of [firstTab, pinnedTab]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_removeTabsToTheStart.js b/browser/base/content/test/tabs/browser_removeTabsToTheStart.js
new file mode 100644
index 0000000000..685da35881
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_removeTabsToTheStart.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 removeTabsToTheStart() {
+ // don't mess with the original tab
+ let originalTab = gBrowser.selectedTab;
+ gBrowser.pinTab(originalTab);
+
+ // Add three new tabs after the original tab. Pin the second one.
+ let firstTab = await addTab();
+ let pinnedTab = await addTab();
+ let lastTab = await addTab();
+ gBrowser.pinTab(pinnedTab);
+
+ // Check that there is only one closable tab from lastTab to the start
+ is(
+ gBrowser.getTabsToTheStartFrom(lastTab).length,
+ 1,
+ "One unpinned tab towards the start"
+ );
+
+ // Remove tabs to the start
+ gBrowser.removeTabsToTheStartFrom(lastTab);
+
+ ok(firstTab.closing, "First tab is closing");
+ ok(!pinnedTab.closing, "Pinned tab is not closing");
+ ok(!lastTab.closing, "Last tab is not closing");
+
+ // cleanup
+ gBrowser.unpinTab(originalTab);
+ for (let tab of [pinnedTab, lastTab]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_removeTabs_order.js b/browser/base/content/test/tabs/browser_removeTabs_order.js
new file mode 100644
index 0000000000..071cc03716
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_removeTabs_order.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+add_task(async function () {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tabs = [tab1, tab2, tab3];
+
+ // Add a beforeunload event listener in one of the tabs; it should be called
+ // before closing any of the tabs.
+ await ContentTask.spawn(tab2.linkedBrowser, null, async function () {
+ content.window.addEventListener("beforeunload", function (event) {}, true);
+ });
+
+ let permitUnloadSpy = sinon.spy(tab2.linkedBrowser, "asyncPermitUnload");
+ let removeTabSpy = sinon.spy(gBrowser, "removeTab");
+
+ gBrowser.removeTabs(tabs);
+
+ Assert.ok(permitUnloadSpy.calledOnce, "permitUnload was called only once");
+ Assert.equal(
+ removeTabSpy.callCount,
+ tabs.length,
+ "removeTab was called for every tab"
+ );
+ Assert.ok(
+ permitUnloadSpy.lastCall.calledBefore(removeTabSpy.firstCall),
+ "permitUnload was called before for first removeTab call"
+ );
+
+ removeTabSpy.restore();
+ permitUnloadSpy.restore();
+});
diff --git a/browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js b/browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js
new file mode 100644
index 0000000000..1016ead94c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for waiting for beforeunload before replacing a session.
+ */
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+// The first two urls are intentionally different domains to force pages
+// to load in different tabs.
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_URL = "https://example.com/";
+
+const BUILDER_URL = "https://example.com/document-builder.sjs?html=";
+const PAGE_MARKUP = `
+<html>
+<head>
+ <script>
+ window.onbeforeunload = function() {
+ return true;
+ };
+ </script>
+</head>
+<body>TEST PAGE</body>
+</html>
+`;
+const TEST_URL2 = BUILDER_URL + encodeURI(PAGE_MARKUP);
+
+let win;
+let nonBeforeUnloadTab;
+let beforeUnloadTab;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ // Run tests in a new window to avoid affecting the main test window.
+ win = await BrowserTestUtils.openNewBrowserWindow();
+
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ TEST_URL
+ );
+ nonBeforeUnloadTab = win.gBrowser.selectedTab;
+ beforeUnloadTab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_URL2
+ );
+
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
+
+add_task(async function test_runBeforeUnloadForTabs() {
+ let unloadDialogPromise = PromptTestUtils.handleNextPrompt(
+ win,
+ {
+ modalType: Ci.nsIPrompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ },
+ // Click the cancel button.
+ { buttonNumClick: 1 }
+ );
+
+ let unloadBlocked = await win.gBrowser.runBeforeUnloadForTabs(
+ win.gBrowser.tabs
+ );
+
+ await unloadDialogPromise;
+
+ Assert.ok(unloadBlocked, "Should have reported the unload was blocked");
+ Assert.equal(win.gBrowser.tabs.length, 2, "Should have left all tabs open");
+
+ unloadDialogPromise = PromptTestUtils.handleNextPrompt(
+ win,
+ {
+ modalType: Ci.nsIPrompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ },
+ // Click the ok button.
+ { buttonNumClick: 0 }
+ );
+
+ unloadBlocked = await win.gBrowser.runBeforeUnloadForTabs(win.gBrowser.tabs);
+
+ await unloadDialogPromise;
+
+ Assert.ok(!unloadBlocked, "Should have reported the unload was not blocked");
+ Assert.equal(win.gBrowser.tabs.length, 2, "Should have left all tabs open");
+});
+
+add_task(async function test_skipPermitUnload() {
+ let closePromise = BrowserTestUtils.waitForTabClosing(beforeUnloadTab);
+
+ await win.gBrowser.removeAllTabsBut(nonBeforeUnloadTab, {
+ animate: false,
+ skipPermitUnload: true,
+ });
+
+ await closePromise;
+
+ Assert.equal(win.gBrowser.tabs.length, 1, "Should have left one tab open");
+});
diff --git a/browser/base/content/test/tabs/browser_replacewithwindow_commands.js b/browser/base/content/test/tabs/browser_replacewithwindow_commands.js
new file mode 100644
index 0000000000..1e6f2b8f57
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_replacewithwindow_commands.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test verifies that focus is handled correctly when a
+// tab is dragged out to a new window, by checking that the
+// copy and select all commands are enabled properly.
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://www.example.com"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://www.example.com"
+ );
+
+ let delayedStartupPromise = BrowserTestUtils.waitForNewWindow();
+ let win = gBrowser.replaceTabWithWindow(tab2);
+ await delayedStartupPromise;
+
+ let copyCommand = win.document.getElementById("cmd_copy");
+ info("Waiting for copy to be enabled");
+ await BrowserTestUtils.waitForMutationCondition(
+ copyCommand,
+ { attributes: true },
+ () => {
+ return !copyCommand.hasAttribute("disabled");
+ }
+ );
+
+ ok(
+ !win.document.getElementById("cmd_copy").hasAttribute("disabled"),
+ "copy is enabled"
+ );
+ ok(
+ !win.document.getElementById("cmd_selectAll").hasAttribute("disabled"),
+ "select all is enabled"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/base/content/test/tabs/browser_switch_by_scrolling.js b/browser/base/content/test/tabs/browser_switch_by_scrolling.js
new file mode 100644
index 0000000000..7d62234d7f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_switch_by_scrolling.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function wheel_switches_tabs() {
+ Services.prefs.setBoolPref("toolkit.tabbox.switchByScrolling", true);
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, () => {
+ EventUtils.synthesizeWheel(newTab, 4, 4, {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaY: -1.0,
+ });
+ });
+ ok(!newTab.selected, "New tab should no longer be selected.");
+ BrowserTestUtils.removeTab(newTab);
+});
+
+add_task(async function wheel_switches_tabs_overflow() {
+ Services.prefs.setBoolPref("toolkit.tabbox.switchByScrolling", true);
+
+ let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+ let tabs = [];
+
+ while (!arrowScrollbox.hasAttribute("overflowing")) {
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank")
+ );
+ }
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, () => {
+ EventUtils.synthesizeWheel(newTab, 4, 4, {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaY: -1.0,
+ });
+ });
+ ok(!newTab.selected, "New tab should no longer be selected.");
+
+ BrowserTestUtils.removeTab(newTab);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_tabCloseProbes.js b/browser/base/content/test/tabs/browser_tabCloseProbes.js
new file mode 100644
index 0000000000..4e5aca8482
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabCloseProbes.js
@@ -0,0 +1,112 @@
+"use strict";
+
+var gAnimHistogram = Services.telemetry.getHistogramById(
+ "FX_TAB_CLOSE_TIME_ANIM_MS"
+);
+var gNoAnimHistogram = Services.telemetry.getHistogramById(
+ "FX_TAB_CLOSE_TIME_NO_ANIM_MS"
+);
+
+/**
+ * Takes a Telemetry histogram snapshot and returns the sum of all counts.
+ *
+ * @param snapshot (Object)
+ * The Telemetry histogram snapshot to examine.
+ * @return (int)
+ * The sum of all counts in the snapshot.
+ */
+function snapshotCount(snapshot) {
+ // Use Array.prototype.reduce to sum up all of the
+ // snapshot.count entries
+ return Object.values(snapshot.values).reduce((a, b) => a + b, 0);
+}
+
+/**
+ * Takes a Telemetry histogram snapshot and makes sure
+ * that the sum of all counts equals expectedCount.
+ *
+ * @param snapshot (Object)
+ * The Telemetry histogram snapshot to examine.
+ * @param expectedCount (int)
+ * What we expect the number of incremented counts to be. For example,
+ * If we expect this probe to have only had a single recording, this
+ * would be 1. If we expected it to have not recorded any data at all,
+ * this would be 0.
+ */
+function assertCount(snapshot, expectedCount) {
+ Assert.equal(
+ snapshotCount(snapshot),
+ expectedCount,
+ `Should only be ${expectedCount} collected value.`
+ );
+}
+
+/**
+ * Takes a Telemetry histogram and waits for the sum of all counts becomes
+ * equal to expectedCount.
+ *
+ * @param histogram (Object)
+ * The Telemetry histogram to examine.
+ * @param expectedCount (int)
+ * What we expect the number of incremented counts to become.
+ * @return (Promise)
+ * @resolves When the histogram snapshot count becomes the expected count.
+ */
+function waitForSnapshotCount(histogram, expectedCount) {
+ return BrowserTestUtils.waitForCondition(() => {
+ return snapshotCount(histogram.snapshot()) == expectedCount;
+ }, `Collected value should become ${expectedCount}.`);
+}
+
+add_setup(async function () {
+ // Force-enable tab animations
+ gReduceMotionOverride = false;
+
+ // These probes are opt-in, meaning we only capture them if extended
+ // Telemetry recording is enabled.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+});
+
+/**
+ * Tests the FX_TAB_CLOSE_TIME_ANIM_MS probe by closing a tab with the tab
+ * close animation.
+ */
+add_task(async function test_close_time_anim_probe() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.waitForCondition(() => tab._fullyOpen);
+
+ gAnimHistogram.clear();
+ gNoAnimHistogram.clear();
+
+ BrowserTestUtils.removeTab(tab, { animate: true });
+
+ await waitForSnapshotCount(gAnimHistogram, 1);
+ assertCount(gNoAnimHistogram.snapshot(), 0);
+
+ gAnimHistogram.clear();
+ gNoAnimHistogram.clear();
+});
+
+/**
+ * Tests the FX_TAB_CLOSE_TIME_NO_ANIM_MS probe by closing a tab without the
+ * tab close animation.
+ */
+add_task(async function test_close_time_no_anim_probe() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.waitForCondition(() => tab._fullyOpen);
+
+ gAnimHistogram.clear();
+ gNoAnimHistogram.clear();
+
+ BrowserTestUtils.removeTab(tab, { animate: false });
+
+ await waitForSnapshotCount(gNoAnimHistogram, 1);
+ assertCount(gAnimHistogram.snapshot(), 0);
+
+ gAnimHistogram.clear();
+ gNoAnimHistogram.clear();
+});
diff --git a/browser/base/content/test/tabs/browser_tabCloseSpacer.js b/browser/base/content/test/tabs/browser_tabCloseSpacer.js
new file mode 100644
index 0000000000..6996546be2
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabCloseSpacer.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that while clicking to close tabs, the close button remains under the mouse
+ * even when an underflow happens.
+ */
+add_task(async function () {
+ // Disable tab animations
+ gReduceMotionOverride = true;
+
+ let downButton = gBrowser.tabContainer.arrowScrollbox._scrollButtonDown;
+ let closingTabsSpacer = gBrowser.tabContainer._closingTabsSpacer;
+ let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+
+ await BrowserTestUtils.overflowTabs(registerCleanupFunction, window);
+
+ // Make sure scrolling finished.
+ await new Promise(resolve => {
+ arrowScrollbox.addEventListener("scrollend", resolve, { once: true });
+ });
+
+ ok(
+ gBrowser.tabContainer.hasAttribute("overflow"),
+ "Tab strip should be overflowing"
+ );
+ isnot(downButton.clientWidth, 0, "down button has some width");
+ is(closingTabsSpacer.clientWidth, 0, "spacer has no width");
+
+ let originalCloseButtonLocation = getLastCloseButtonLocation();
+
+ info(
+ "Removing half the tabs and making sure the last close button doesn't move"
+ );
+ let numTabs = gBrowser.tabs.length / 2;
+ while (gBrowser.tabs.length > numTabs) {
+ let lastCloseButtonLocation = getLastCloseButtonLocation();
+ Assert.equal(
+ lastCloseButtonLocation.top,
+ originalCloseButtonLocation.top,
+ "The top of all close buttons should be equal"
+ );
+ Assert.equal(
+ lastCloseButtonLocation.bottom,
+ originalCloseButtonLocation.bottom,
+ "The bottom of all close buttons should be equal"
+ );
+ Assert.equal(
+ lastCloseButtonLocation.right,
+ originalCloseButtonLocation.right,
+ "The right side of the close button should remain consistent"
+ );
+ // Ignore 'left' since non-hovered tabs have their close button
+ // narrower to display more tab label.
+
+ EventUtils.synthesizeMouseAtCenter(getLastCloseButton(), {});
+ await new Promise(r => requestAnimationFrame(r));
+ }
+
+ ok(!gBrowser.tabContainer.hasAttribute("overflow"), "not overflowing");
+ ok(
+ gBrowser.tabContainer.hasAttribute("using-closing-tabs-spacer"),
+ "using spacer"
+ );
+
+ is(downButton.clientWidth, 0, "down button has no width");
+ isnot(closingTabsSpacer.clientWidth, 0, "spacer has some width");
+});
+
+function getLastCloseButton() {
+ let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ return lastTab.closeButton;
+}
+
+function getLastCloseButtonLocation() {
+ let rect = getLastCloseButton().getBoundingClientRect();
+ return {
+ left: Math.round(rect.left),
+ top: Math.round(rect.top),
+ width: Math.round(rect.width),
+ height: Math.round(rect.height),
+ };
+}
+
+registerCleanupFunction(() => {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[0]);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js b/browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js
new file mode 100644
index 0000000000..f3a2066653
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+async function openContextMenu() {
+ let contextMenu = document.getElementById("tabContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShown;
+}
+
+async function closeContextMenu() {
+ let contextMenu = document.getElementById("tabContextMenu");
+ let popupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await popupHidden;
+}
+
+add_task(async function test() {
+ if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) {
+ ok(
+ true,
+ "This bug is not possible when native context menus are enabled on macOS."
+ );
+ return;
+ }
+ // Ensure tabs are focusable.
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+
+ // There should be one tab when we start the test.
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = BrowserTestUtils.addTab(gBrowser);
+ tab1.focus();
+ is(document.activeElement, tab1, "tab1 should be focused");
+
+ // Ensure that DownArrow doesn't switch to tab2 while the context menu is open.
+ await openContextMenu();
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await closeContextMenu();
+ is(gBrowser.selectedTab, tab1, "tab1 should still be active");
+ if (AppConstants.platform == "macosx") {
+ // On Mac, focus doesn't return to the tab after dismissing the context menu.
+ // Since we're not testing that here, work around it by just focusing again.
+ tab1.focus();
+ }
+ is(document.activeElement, tab1, "tab1 should be focused");
+
+ // Switch to tab2 by pressing DownArrow.
+ await BrowserTestUtils.switchTab(gBrowser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+ is(gBrowser.selectedTab, tab2, "should have switched to tab2");
+ is(document.activeElement, tab2, "tab2 should now be focused");
+ // Ensure that UpArrow doesn't switch to tab1 while the context menu is open.
+ await openContextMenu();
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ await closeContextMenu();
+ is(gBrowser.selectedTab, tab2, "tab2 should still be active");
+
+ gBrowser.removeTab(tab2);
+});
diff --git a/browser/base/content/test/tabs/browser_tabReorder.js b/browser/base/content/test/tabs/browser_tabReorder.js
new file mode 100644
index 0000000000..c5ae459065
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabReorder.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ let initialTabsLength = gBrowser.tabs.length;
+
+ let newTab1 = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:robots",
+ { skipAnimation: true }
+ ));
+ let newTab2 = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:about",
+ { skipAnimation: true }
+ ));
+ let newTab3 = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:config",
+ { skipAnimation: true }
+ ));
+ registerCleanupFunction(function () {
+ while (gBrowser.tabs.length > initialTabsLength) {
+ gBrowser.removeTab(gBrowser.tabs[initialTabsLength]);
+ }
+ });
+
+ is(gBrowser.tabs.length, initialTabsLength + 3, "new tabs are opened");
+ is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct");
+ is(
+ gBrowser.tabs[initialTabsLength + 1],
+ newTab2,
+ "newTab2 position is correct"
+ );
+ is(
+ gBrowser.tabs[initialTabsLength + 2],
+ newTab3,
+ "newTab3 position is correct"
+ );
+
+ await dragAndDrop(newTab1, newTab2, false);
+ is(gBrowser.tabs.length, initialTabsLength + 3, "tabs are still there");
+ is(
+ gBrowser.tabs[initialTabsLength],
+ newTab2,
+ "newTab2 and newTab1 are swapped"
+ );
+ is(
+ gBrowser.tabs[initialTabsLength + 1],
+ newTab1,
+ "newTab1 and newTab2 are swapped"
+ );
+ is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
+
+ await dragAndDrop(newTab2, newTab1, true);
+ is(gBrowser.tabs.length, initialTabsLength + 4, "a tab is duplicated");
+ is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 stays same place");
+ is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 stays same place");
+ is(
+ gBrowser.tabs[initialTabsLength + 3],
+ newTab3,
+ "a new tab is inserted before newTab3"
+ );
+});
diff --git a/browser/base/content/test/tabs/browser_tabReorder_overflow.js b/browser/base/content/test/tabs/browser_tabReorder_overflow.js
new file mode 100644
index 0000000000..a74677204f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabReorder_overflow.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(async function () {
+ let initialTabsLength = gBrowser.tabs.length;
+
+ let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+ let tabMinWidth = parseInt(
+ getComputedStyle(gBrowser.selectedTab, null).minWidth
+ );
+
+ let width = ele => ele.getBoundingClientRect().width;
+
+ let tabCountForOverflow = Math.ceil(
+ (width(arrowScrollbox) / tabMinWidth) * 1.1
+ );
+
+ let newTab1 = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:robots",
+ { skipAnimation: true }
+ ));
+ let newTab2 = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:about",
+ { skipAnimation: true }
+ ));
+ let newTab3 = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:config",
+ { skipAnimation: true }
+ ));
+
+ await BrowserTestUtils.overflowTabs(registerCleanupFunction, window, {
+ overflowAtStart: false,
+ });
+
+ registerCleanupFunction(function () {
+ while (gBrowser.tabs.length > initialTabsLength) {
+ gBrowser.removeTab(
+ gBrowser.tabContainer.getItemAtIndex(initialTabsLength)
+ );
+ }
+ });
+
+ let tabs = gBrowser.tabs;
+ is(tabs.length, tabCountForOverflow, "new tabs are opened");
+ is(tabs[initialTabsLength], newTab1, "newTab1 position is correct");
+ is(tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
+ is(tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
+
+ await dragAndDrop(newTab1, newTab2, false);
+ tabs = gBrowser.tabs;
+ is(tabs.length, tabCountForOverflow, "tabs are still there");
+ is(tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
+ is(tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
+ is(tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
+});
diff --git a/browser/base/content/test/tabs/browser_tabSpinnerProbe.js b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js
new file mode 100644
index 0000000000..9115d4fc6c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js
@@ -0,0 +1,101 @@
+"use strict";
+
+/**
+ * Tests the FX_TAB_SWITCH_SPINNER_VISIBLE_MS and
+ * FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS telemetry probes
+ */
+const MIN_HANG_TIME = 500; // ms
+const MAX_HANG_TIME = 5 * 1000; // ms
+
+/**
+ * Returns the sum of all values in an array.
+ * @param {Array} aArray An array of integers
+ * @return {Number} The sum of the integers in the array
+ */
+function sum(aArray) {
+ return aArray.reduce(function (previousValue, currentValue) {
+ return previousValue + currentValue;
+ });
+}
+
+/**
+ * Causes the content process for a remote <xul:browser> to run
+ * some busy JS for aMs milliseconds.
+ *
+ * @param {<xul:browser>} browser
+ * The browser that's running in the content process that we're
+ * going to hang.
+ * @param {int} aMs
+ * The amount of time, in milliseconds, to hang the content process.
+ *
+ * @return {Promise}
+ * Resolves once the hang is done.
+ */
+function hangContentProcess(browser, aMs) {
+ return ContentTask.spawn(browser, aMs, function (ms) {
+ let then = Date.now();
+ while (Date.now() - then < ms) {
+ // Let's burn some CPU...
+ }
+ });
+}
+
+/**
+ * A generator intended to be run as a Task. It tests one of the tab spinner
+ * telemetry probes.
+ * @param {String} aProbe The probe to test. Should be one of:
+ * - FX_TAB_SWITCH_SPINNER_VISIBLE_MS
+ * - FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS
+ */
+async function testProbe(aProbe) {
+ info(`Testing probe: ${aProbe}`);
+ let histogram = Services.telemetry.getHistogramById(aProbe);
+ let delayTime = MIN_HANG_TIME + 1; // Pick a bucket arbitrarily
+
+ // The tab spinner does not show up instantly. We need to hang for a little
+ // bit of extra time to account for the tab spinner delay.
+ delayTime += gBrowser.selectedTab.linkedBrowser
+ .getTabBrowser()
+ ._getSwitcher().TAB_SWITCH_TIMEOUT;
+
+ // In order for a spinner to be shown, the tab must have presented before.
+ let origTab = gBrowser.selectedTab;
+ let hangTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let hangBrowser = hangTab.linkedBrowser;
+ ok(hangBrowser.isRemoteBrowser, "New tab should be remote.");
+ ok(hangBrowser.frameLoader.remoteTab.hasPresented, "New tab has presented.");
+
+ // Now switch back to the original tab and set up our hang.
+ await BrowserTestUtils.switchTab(gBrowser, origTab);
+
+ let tabHangPromise = hangContentProcess(hangBrowser, delayTime);
+ histogram.clear();
+ let hangTabSwitch = BrowserTestUtils.switchTab(gBrowser, hangTab);
+ await tabHangPromise;
+ await hangTabSwitch;
+
+ // Now we should have a hang in our histogram.
+ let snapshot = histogram.snapshot();
+ BrowserTestUtils.removeTab(hangTab);
+ ok(
+ sum(Object.values(snapshot.values)) > 0,
+ `Spinner probe should now have a value in some bucket`
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ // We can interrupt JS to paint now, which is great for
+ // users, but bad for testing spinners. We temporarily
+ // disable that feature for this test so that we can
+ // easily get ourselves into a predictable tab spinner
+ // state.
+ ["browser.tabs.remote.force-paint", false],
+ ],
+ });
+});
+
+add_task(testProbe.bind(null, "FX_TAB_SWITCH_SPINNER_VISIBLE_MS"));
+add_task(testProbe.bind(null, "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS"));
diff --git a/browser/base/content/test/tabs/browser_tabSuccessors.js b/browser/base/content/test/tabs/browser_tabSuccessors.js
new file mode 100644
index 0000000000..9f577b6200
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSuccessors.js
@@ -0,0 +1,131 @@
+add_task(async function test() {
+ const tabs = [gBrowser.selectedTab];
+ for (let i = 0; i < 6; ++i) {
+ tabs.push(BrowserTestUtils.addTab(gBrowser));
+ }
+
+ // Check that setSuccessor works.
+ gBrowser.setSuccessor(tabs[0], tabs[2]);
+ is(tabs[0].successor, tabs[2], "setSuccessor sets successor");
+ ok(tabs[2].predecessors.has(tabs[0]), "setSuccessor adds predecessor");
+
+ BrowserTestUtils.removeTab(tabs[0]);
+ is(
+ gBrowser.selectedTab,
+ tabs[2],
+ "When closing a selected tab, select its successor"
+ );
+
+ // Check that the successor of a hidden tab becomes the successor of the
+ // tab's predecessors.
+ gBrowser.setSuccessor(tabs[1], tabs[2]);
+ gBrowser.setSuccessor(tabs[3], tabs[1]);
+ ok(!tabs[2].predecessors.has(tabs[3]));
+
+ gBrowser.hideTab(tabs[1]);
+ is(
+ tabs[3].successor,
+ tabs[2],
+ "A predecessor of a hidden tab should take as its successor the hidden tab's successor"
+ );
+ ok(tabs[2].predecessors.has(tabs[3]));
+
+ gBrowser.showTab(tabs[1]);
+
+ // Check that the successor of a closed tab also becomes the successor of the
+ // tab's predecessors.
+ gBrowser.setSuccessor(tabs[1], tabs[2]);
+ gBrowser.setSuccessor(tabs[3], tabs[1]);
+ ok(!tabs[2].predecessors.has(tabs[3]));
+
+ BrowserTestUtils.removeTab(tabs[1]);
+ is(
+ tabs[3].successor,
+ tabs[2],
+ "A predecessor of a closed tab should take as its successor the closed tab's successor"
+ );
+ ok(tabs[2].predecessors.has(tabs[3]));
+
+ // Check that clearing a successor makes the browser fall back to selecting
+ // the owner or next tab.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[3]);
+ gBrowser.setSuccessor(tabs[3], null);
+ is(tabs[3].successor, null, "setSuccessor(..., null) should clear successor");
+ ok(
+ !tabs[2].predecessors.has(tabs[3]),
+ "setSuccessor(..., null) should remove the old successor from predecessors"
+ );
+
+ BrowserTestUtils.removeTab(tabs[3]);
+ is(
+ gBrowser.selectedTab,
+ tabs[4],
+ "When the active tab is closed and its successor has been cleared, select the next tab"
+ );
+
+ // Like closing or hiding a tab, moving a tab to another window should also
+ // result in its successor becoming the successor of the moved tab's
+ // predecessors.
+ gBrowser.setSuccessor(tabs[4], tabs[2]);
+ gBrowser.setSuccessor(tabs[2], tabs[5]);
+ const secondWin = gBrowser.replaceTabsWithWindow(tabs[2]);
+ await TestUtils.waitForCondition(
+ () => tabs[2].closing,
+ "Wait for tab to be transferred"
+ );
+ is(
+ tabs[4].successor,
+ tabs[5],
+ "A predecessor of a tab moved to another window should take as its successor the moved tab's successor"
+ );
+
+ // Trying to set a successor across windows should fail.
+ let threw = false;
+ try {
+ gBrowser.setSuccessor(tabs[4], secondWin.gBrowser.selectedTab);
+ } catch (ex) {
+ threw = true;
+ }
+ ok(threw, "No cross window successors");
+ is(tabs[4].successor, tabs[5], "Successor should remain unchanged");
+
+ threw = false;
+ try {
+ secondWin.gBrowser.setSuccessor(tabs[4], null);
+ } catch (ex) {
+ threw = true;
+ }
+ ok(threw, "No setting successors for another window's tab");
+ is(tabs[4].successor, tabs[5], "Successor should remain unchanged");
+
+ BrowserTestUtils.closeWindow(secondWin);
+
+ // A tab can't be its own successor
+ gBrowser.setSuccessor(tabs[4], tabs[4]);
+ is(
+ tabs[4].successor,
+ null,
+ "Successor should be cleared instead of pointing to itself"
+ );
+
+ gBrowser.setSuccessor(tabs[4], tabs[5]);
+ gBrowser.setSuccessor(tabs[5], tabs[4]);
+ is(
+ tabs[4].successor,
+ tabs[5],
+ "Successors can form cycles of length > 1 [a]"
+ );
+ is(
+ tabs[5].successor,
+ tabs[4],
+ "Successors can form cycles of length > 1 [b]"
+ );
+ BrowserTestUtils.removeTab(tabs[5]);
+ is(
+ tabs[4].successor,
+ null,
+ "Successor should be cleared instead of pointing to itself"
+ );
+
+ gBrowser.removeTab(tabs[4]);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_a11y_description.js b/browser/base/content/test/tabs/browser_tab_a11y_description.js
new file mode 100644
index 0000000000..04f9a54a1b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_a11y_description.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function waitForFocusAfterKey(ariaFocus, element, key, accel = false) {
+ let event = ariaFocus ? "AriaFocus" : "focus";
+ let friendlyKey = key;
+ if (accel) {
+ friendlyKey = "Accel+" + key;
+ }
+ key = "KEY_" + key;
+ let focused = BrowserTestUtils.waitForEvent(element, event);
+ EventUtils.synthesizeKey(key, { accelKey: accel });
+ await focused;
+ ok(true, element.label + " got " + event + " after " + friendlyKey);
+}
+
+function getA11yDescription(element) {
+ let descId = element.getAttribute("aria-describedby");
+ if (!descId) {
+ return null;
+ }
+ let descElem = document.getElementById(descId);
+ if (!descElem) {
+ return null;
+ }
+ return descElem.textContent;
+}
+
+add_task(async function testTabA11yDescription() {
+ const tab1 = await addTab("http://mochi.test:8888/1", { userContextId: 1 });
+ tab1.label = "tab1";
+ const context1 = ContextualIdentityService.getUserContextLabel(1);
+ const tab2 = await addTab("http://mochi.test:8888/2", { userContextId: 2 });
+ tab2.label = "tab2";
+ const context2 = ContextualIdentityService.getUserContextLabel(2);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ let focused = BrowserTestUtils.waitForEvent(tab1, "focus");
+ tab1.focus();
+ await focused;
+ ok(true, "tab1 initially focused");
+ ok(
+ getA11yDescription(tab1).endsWith(context1),
+ "tab1 has correct a11y description"
+ );
+ ok(!getA11yDescription(tab2), "tab2 has no a11y description");
+
+ info("Moving DOM focus to tab2");
+ await waitForFocusAfterKey(false, tab2, "ArrowRight");
+ ok(
+ getA11yDescription(tab2).endsWith(context2),
+ "tab2 has correct a11y description"
+ );
+ ok(!getA11yDescription(tab1), "tab1 has no a11y description");
+
+ info("Moving ARIA focus to tab1");
+ await waitForFocusAfterKey(true, tab1, "ArrowLeft", true);
+ ok(
+ getA11yDescription(tab1).endsWith(context1),
+ "tab1 has correct a11y description"
+ );
+ ok(!getA11yDescription(tab2), "tab2 has no a11y description");
+
+ info("Removing ARIA focus (reverting to DOM focus)");
+ await waitForFocusAfterKey(true, tab2, "ArrowRight");
+ ok(
+ getA11yDescription(tab2).endsWith(context2),
+ "tab2 has correct a11y description"
+ );
+ ok(!getA11yDescription(tab1), "tab1 has no a11y description");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_label_during_reload.js b/browser/base/content/test/tabs/browser_tab_label_during_reload.js
new file mode 100644
index 0000000000..ec0728c34a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_label_during_reload.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:preferences"
+ ));
+ let browser = tab.linkedBrowser;
+ let labelChanges = 0;
+ let attrModifiedListener = event => {
+ if (event.detail.changed.includes("label")) {
+ labelChanges++;
+ }
+ };
+ tab.addEventListener("TabAttrModified", attrModifiedListener);
+
+ await BrowserTestUtils.browserLoaded(browser);
+ is(labelChanges, 1, "number of label changes during initial load");
+ isnot(tab.label, "", "about:preferences tab label isn't empty");
+ isnot(
+ tab.label,
+ "about:preferences",
+ "about:preferences tab label isn't the URI"
+ );
+ is(
+ tab.label,
+ browser.contentTitle,
+ "about:preferences tab label matches browser.contentTitle"
+ );
+
+ labelChanges = 0;
+ browser.reload();
+ await BrowserTestUtils.browserLoaded(browser);
+ is(labelChanges, 0, "number of label changes during reload");
+
+ tab.removeEventListener("TabAttrModified", attrModifiedListener);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js b/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js
new file mode 100644
index 0000000000..dae4ffc444
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.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 test_pip_label_changes_tab() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ let pipTab = newWin.document.querySelector(".tabbrowser-tab[selected]");
+ pipTab.setAttribute("pictureinpicture", true);
+
+ let pipLabel = pipTab.querySelector(".tab-icon-sound-pip-label");
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let selectedTab = newWin.document.querySelector(
+ ".tabbrowser-tab[selected]"
+ );
+ Assert.ok(
+ selectedTab != pipTab,
+ "Picture in picture tab is not selected tab"
+ );
+
+ selectedTab = await BrowserTestUtils.switchTab(newWin.gBrowser, () =>
+ pipLabel.click()
+ );
+ Assert.ok(selectedTab == pipTab, "Picture in picture tab is selected tab");
+ });
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_manager_close.js b/browser/base/content/test/tabs/browser_tab_manager_close.js
new file mode 100644
index 0000000000..d04b2a6c0a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_close.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL1 = "data:text/plain,tab1";
+const URL2 = "data:text/plain,tab2";
+const URL3 = "data:text/plain,tab3";
+const URL4 = "data:text/plain,tab4";
+const URL5 = "data:text/plain,tab5";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.tabmanager.enabled", true]],
+ });
+});
+
+/**
+ * Tests that middle-clicking on a tab in the Tab Manager will close it.
+ */
+add_task(async function test_tab_manager_close_middle_click() {
+ let win =
+ await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
+ win.gTabsPanel.init();
+ await addTabTo(win.gBrowser, URL1);
+ await addTabTo(win.gBrowser, URL2);
+ await addTabTo(win.gBrowser, URL3);
+ await addTabTo(win.gBrowser, URL4);
+ await addTabTo(win.gBrowser, URL5);
+
+ let button = win.document.getElementById("alltabs-button");
+ let allTabsView = win.document.getElementById("allTabsMenu-allTabsView");
+ let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
+ allTabsView,
+ "ViewShown"
+ );
+ button.click();
+ await allTabsPopupShownPromise;
+
+ let list = win.document.getElementById("allTabsMenu-allTabsView-tabs");
+ while (win.gBrowser.tabs.length > 1) {
+ let row = list.lastElementChild;
+ let tabClosing = BrowserTestUtils.waitForTabClosing(row.tab);
+ EventUtils.synthesizeMouseAtCenter(row, { button: 1 }, win);
+ await tabClosing;
+ Assert.ok(true, "Closed a tab with middle-click.");
+ }
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests that clicking the close button next to a tab manager item
+ * will close it.
+ */
+add_task(async function test_tab_manager_close_button() {
+ let win =
+ await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
+ win.gTabsPanel.init();
+ await addTabTo(win.gBrowser, URL1);
+ await addTabTo(win.gBrowser, URL2);
+ await addTabTo(win.gBrowser, URL3);
+ await addTabTo(win.gBrowser, URL4);
+ await addTabTo(win.gBrowser, URL5);
+
+ let button = win.document.getElementById("alltabs-button");
+ let allTabsView = win.document.getElementById("allTabsMenu-allTabsView");
+ let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
+ allTabsView,
+ "ViewShown"
+ );
+ button.click();
+ await allTabsPopupShownPromise;
+
+ let list = win.document.getElementById("allTabsMenu-allTabsView-tabs");
+ while (win.gBrowser.tabs.length > 1) {
+ let row = list.lastElementChild;
+ let tabClosing = BrowserTestUtils.waitForTabClosing(row.tab);
+ let closeButton = row.lastElementChild;
+ EventUtils.synthesizeMouseAtCenter(closeButton, { button: 1 }, win);
+ await tabClosing;
+ Assert.ok(true, "Closed a tab with the close button.");
+ }
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_manager_drag.js b/browser/base/content/test/tabs/browser_tab_manager_drag.js
new file mode 100644
index 0000000000..1dc1933e3e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_drag.js
@@ -0,0 +1,259 @@
+/**
+ * Test reordering the tabs in the Tab Manager, moving the tab between the
+ * Tab Manager and tab bar.
+ */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const URL1 = "data:text/plain,tab1";
+const URL2 = "data:text/plain,tab2";
+const URL3 = "data:text/plain,tab3";
+const URL4 = "data:text/plain,tab4";
+const URL5 = "data:text/plain,tab5";
+
+function assertOrder(order, expected, message) {
+ is(
+ JSON.stringify(order),
+ JSON.stringify(expected),
+ `The order of the tabs ${message}`
+ );
+}
+
+function toIndex(url) {
+ const m = url.match(/^data:text\/plain,tab(\d)/);
+ if (m) {
+ return parseInt(m[1]);
+ }
+ return 0;
+}
+
+function getOrderOfList(list) {
+ return [...list.querySelectorAll("toolbaritem")].map(row => {
+ const url = row.firstElementChild.tab.linkedBrowser.currentURI.spec;
+ return toIndex(url);
+ });
+}
+
+function getOrderOfTabs(tabs) {
+ return tabs.map(tab => {
+ const url = tab.linkedBrowser.currentURI.spec;
+ return toIndex(url);
+ });
+}
+
+async function testWithNewWindow(func) {
+ Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", true);
+
+ const newWindow =
+ await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
+
+ await Promise.all([
+ addTabTo(newWindow.gBrowser, URL1),
+ addTabTo(newWindow.gBrowser, URL2),
+ addTabTo(newWindow.gBrowser, URL3),
+ addTabTo(newWindow.gBrowser, URL4),
+ addTabTo(newWindow.gBrowser, URL5),
+ ]);
+
+ newWindow.gTabsPanel.init();
+
+ const button = newWindow.document.getElementById("alltabs-button");
+
+ const allTabsView = newWindow.document.getElementById(
+ "allTabsMenu-allTabsView"
+ );
+ const allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
+ allTabsView,
+ "ViewShown"
+ );
+ button.click();
+ await allTabsPopupShownPromise;
+
+ await func(newWindow);
+
+ await BrowserTestUtils.closeWindow(newWindow);
+
+ Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled");
+}
+
+add_task(async function test_reorder() {
+ await testWithNewWindow(async function (newWindow) {
+ Services.telemetry.clearScalars();
+
+ const list = newWindow.document.getElementById(
+ "allTabsMenu-allTabsView-tabs"
+ );
+
+ assertOrder(getOrderOfList(list), [0, 1, 2, 3, 4, 5], "before reorder");
+
+ let rows;
+ rows = list.querySelectorAll("toolbaritem");
+ EventUtils.synthesizeDrop(
+ rows[3],
+ rows[1],
+ null,
+ "move",
+ newWindow,
+ newWindow,
+ { clientX: 0, clientY: 0 }
+ );
+
+ assertOrder(getOrderOfList(list), [0, 3, 1, 2, 4, 5], "after moving up");
+
+ rows = list.querySelectorAll("toolbaritem");
+ EventUtils.synthesizeDrop(
+ rows[1],
+ rows[5],
+ null,
+ "move",
+ newWindow,
+ newWindow,
+ { clientX: 0, clientY: 0 }
+ );
+
+ assertOrder(getOrderOfList(list), [0, 1, 2, 4, 3, 5], "after moving down");
+
+ rows = list.querySelectorAll("toolbaritem");
+ EventUtils.synthesizeDrop(
+ rows[4],
+ rows[3],
+ null,
+ "move",
+ newWindow,
+ newWindow,
+ { clientX: 0, clientY: 0 }
+ );
+
+ assertOrder(
+ getOrderOfList(list),
+ [0, 1, 2, 3, 4, 5],
+ "after moving up again"
+ );
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
+ 3
+ );
+ });
+});
+
+function tabOf(row) {
+ return row.firstElementChild.tab;
+}
+
+add_task(async function test_move_to_tab_bar() {
+ await testWithNewWindow(async function (newWindow) {
+ Services.telemetry.clearScalars();
+
+ const list = newWindow.document.getElementById(
+ "allTabsMenu-allTabsView-tabs"
+ );
+
+ assertOrder(getOrderOfList(list), [0, 1, 2, 3, 4, 5], "before reorder");
+
+ let rows;
+ rows = list.querySelectorAll("toolbaritem");
+ EventUtils.synthesizeDrop(
+ rows[3],
+ tabOf(rows[1]),
+ null,
+ "move",
+ newWindow,
+ newWindow,
+ { clientX: 0, clientY: 0 }
+ );
+
+ assertOrder(
+ getOrderOfList(list),
+ [0, 3, 1, 2, 4, 5],
+ "after moving up with tab bar"
+ );
+
+ rows = list.querySelectorAll("toolbaritem");
+ EventUtils.synthesizeDrop(
+ rows[1],
+ tabOf(rows[4]),
+ null,
+ "move",
+ newWindow,
+ newWindow,
+ { clientX: 0, clientY: 0 }
+ );
+
+ assertOrder(
+ getOrderOfList(list),
+ [0, 1, 2, 3, 4, 5],
+ "after moving down with tab bar"
+ );
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
+ 2
+ );
+ });
+});
+
+add_task(async function test_move_to_different_tab_bar() {
+ const newWindow2 =
+ await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
+
+ await testWithNewWindow(async function (newWindow) {
+ Services.telemetry.clearScalars();
+
+ const list = newWindow.document.getElementById(
+ "allTabsMenu-allTabsView-tabs"
+ );
+
+ assertOrder(
+ getOrderOfList(list),
+ [0, 1, 2, 3, 4, 5],
+ "before reorder in newWindow"
+ );
+ assertOrder(
+ getOrderOfTabs(newWindow2.gBrowser.tabs),
+ [0],
+ "before reorder in newWindow2"
+ );
+
+ let rows;
+ rows = list.querySelectorAll("toolbaritem");
+ EventUtils.synthesizeDrop(
+ rows[3],
+ newWindow2.gBrowser.tabs[0],
+ null,
+ "move",
+ newWindow,
+ newWindow2,
+ { clientX: 0, clientY: 0 }
+ );
+
+ assertOrder(
+ getOrderOfList(list),
+ [0, 1, 2, 4, 5],
+ "after moving to other window in newWindow"
+ );
+
+ assertOrder(
+ getOrderOfTabs(newWindow2.gBrowser.tabs),
+ [3, 0],
+ "after moving to other window in newWindow2"
+ );
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
+ 1
+ );
+ });
+
+ await BrowserTestUtils.closeWindow(newWindow2);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js b/browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js
new file mode 100644
index 0000000000..444db5d3be
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check we can open the tab manager using the keyboard.
+ * Note that navigation to buttons in the toolbar is covered
+ * by other tests.
+ */
+add_task(async function test_open_tabmanager_keyboard() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.tabmanager.enabled", true]],
+ });
+ let newWindow =
+ await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
+ let elem = newWindow.document.getElementById("alltabs-button");
+
+ // Borrowed from forceFocus() in the keyboard directory head.js
+ elem.setAttribute("tabindex", "-1");
+ elem.focus();
+ elem.removeAttribute("tabindex");
+
+ let focused = BrowserTestUtils.waitForEvent(newWindow, "focus", true);
+ EventUtils.synthesizeKey(" ", {}, newWindow);
+ let event = await focused;
+ ok(
+ event.originalTarget.closest("#allTabsMenu-allTabsView"),
+ "Focus inside all tabs menu after toolbar button pressed"
+ );
+ let hidden = BrowserTestUtils.waitForEvent(
+ event.target.closest("panel"),
+ "popuphidden"
+ );
+ EventUtils.synthesizeKey("KEY_Escape", { shiftKey: false }, newWindow);
+ await hidden;
+ await BrowserTestUtils.closeWindow(newWindow);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_manager_visibility.js b/browser/base/content/test/tabs/browser_tab_manager_visibility.js
new file mode 100644
index 0000000000..cfec0fa528
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_visibility.js
@@ -0,0 +1,55 @@
+/**
+ * Test the Tab Manager visibility respects browser.tabs.tabmanager.enabled preference
+ * */
+
+"use strict";
+
+// The hostname for the test URIs.
+const TEST_HOSTNAME = "https://example.com";
+const DUMMY_PAGE_PATH = "/browser/base/content/test/tabs/dummy_page.html";
+
+add_task(async function tab_manager_visibility_preference_on() {
+ Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", true);
+
+ let newWindow =
+ await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: newWindow.gBrowser,
+ url: TEST_HOSTNAME + DUMMY_PAGE_PATH,
+ },
+ async function (browser) {
+ await Assert.ok(
+ BrowserTestUtils.is_visible(
+ newWindow.document.getElementById("alltabs-button")
+ ),
+ "tab manage menu is visible when browser.tabs.tabmanager.enabled preference is set to true"
+ );
+ }
+ );
+ Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled");
+ BrowserTestUtils.closeWindow(newWindow);
+});
+
+add_task(async function tab_manager_visibility_preference_off() {
+ Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", false);
+
+ let newWindow =
+ await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: newWindow.gBrowser,
+ url: TEST_HOSTNAME + DUMMY_PAGE_PATH,
+ },
+ async function (browser) {
+ await Assert.ok(
+ BrowserTestUtils.is_hidden(
+ newWindow.document.getElementById("alltabs-button")
+ ),
+ "tab manage menu is hidden when browser.tabs.tabmanager.enabled preference is set to true"
+ );
+ }
+ );
+ Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled");
+ BrowserTestUtils.closeWindow(newWindow);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_move_to_new_window_reload.js b/browser/base/content/test/tabs/browser_tab_move_to_new_window_reload.js
new file mode 100644
index 0000000000..6af8f440fd
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_move_to_new_window_reload.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+// Move a tab to a new window the reload it. In Bug 1691135 it would not
+// reload.
+add_task(async function test() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ let prevBrowser = tab1.linkedBrowser;
+
+ let delayedStartupPromise = BrowserTestUtils.waitForNewWindow();
+ let newWindow = gBrowser.replaceTabsWithWindow(tab1);
+ await delayedStartupPromise;
+
+ ok(
+ !prevBrowser.frameLoader,
+ "the swapped-from browser's frameloader has been destroyed"
+ );
+
+ let gBrowser2 = newWindow.gBrowser;
+
+ is(gBrowser.visibleTabs.length, 2, "Two tabs now in the old window");
+ is(gBrowser2.visibleTabs.length, 1, "One tabs in the new window");
+
+ tab1 = gBrowser2.visibleTabs[0];
+ ok(tab1, "Got a tab1");
+ await tab1.focus();
+
+ await TabStateFlusher.flush(tab1.linkedBrowser);
+
+ info("Reloading");
+ let tab1Loaded = BrowserTestUtils.browserLoaded(
+ gBrowser2.getBrowserForTab(tab1)
+ );
+
+ gBrowser2.reload();
+ ok(await tab1Loaded, "Tab reloaded");
+
+ await BrowserTestUtils.closeWindow(newWindow);
+ await BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_play.js b/browser/base/content/test/tabs/browser_tab_play.js
new file mode 100644
index 0000000000..f98956eeb4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_play.js
@@ -0,0 +1,216 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Ensure tabs that are active media blocked act correctly
+ * when we try to unblock them using the "Play Tab" icon or by calling
+ * resumeDelayedMedia()
+ */
+
+"use strict";
+
+const PREF_DELAY_AUTOPLAY = "media.block-autoplay-until-in-foreground";
+
+async function playMedia(tab, { expectBlocked }) {
+ let blockedPromise = wait_for_tab_media_blocked_event(tab, expectBlocked);
+ tab.resumeDelayedMedia();
+ await blockedPromise;
+ is(activeMediaBlocked(tab), expectBlocked, "tab has wrong media block state");
+}
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_DELAY_AUTOPLAY, true]],
+ });
+});
+
+/*
+ * Playing blocked media will not mute the selected tabs
+ */
+add_task(async function testDelayPlayWontAffectUnmuteStatus() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+
+ let tabs = [tab0, tab1];
+
+ info("Play both tabs");
+ await play(tab0, false);
+ await play(tab1, false);
+
+ // Check tabs are unmuted
+ ok(!muted(tab0), "Tab0 is unmuted");
+ ok(!muted(tab1), "Tab1 is unmuted");
+
+ info("Play media on tab0");
+ await playMedia(tab0, { expectBlocked: false });
+
+ // Check tabs are still unmuted
+ ok(!muted(tab0), "Tab0 is unmuted");
+ ok(!muted(tab1), "Tab1 is unmuted");
+
+ info("Play media on tab1");
+ await playMedia(tab1, { expectBlocked: false });
+
+ // Check tabs are still unmuted
+ ok(!muted(tab0), "Tab0 is unmuted");
+ ok(!muted(tab1), "Tab1 is unmuted");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+/*
+ * Playing blocked media will not unmute the selected tabs
+ */
+add_task(async function testDelayPlayWontAffectMuteStatus() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+
+ info("Play both tabs");
+ await play(tab0, false);
+ await play(tab1, false);
+
+ // Mute both tabs
+ toggleMuteAudio(tab0, true);
+ toggleMuteAudio(tab1, true);
+
+ // Check tabs are muted
+ ok(muted(tab0), "Tab0 is muted");
+ ok(muted(tab1), "Tab1 is muted");
+
+ info("Play media on tab0");
+ await playMedia(tab0, { expectBlocked: false });
+
+ // Check tabs are still muted
+ ok(muted(tab0), "Tab0 is muted");
+ ok(muted(tab1), "Tab1 is muted");
+
+ info("Play media on tab1");
+ await playMedia(tab1, { expectBlocked: false });
+
+ // Check tabs are still muted
+ ok(muted(tab0), "Tab0 is muted");
+ ok(muted(tab1), "Tab1 is muted");
+
+ BrowserTestUtils.removeTab(tab0);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/*
+ * Switching tabs will unblock media
+ */
+add_task(async function testDelayPlayWhenSwitchingTab() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+
+ info("Play both tabs");
+ await play(tab0, false);
+ await play(tab1, false);
+
+ // Both tabs are initially active media blocked after being played
+ ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked");
+
+ info("Switch to tab0");
+ await BrowserTestUtils.switchTab(gBrowser, tab0);
+ is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+ // tab0 unblocked, tab1 blocked
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked");
+
+ info("Switch to tab1");
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ is(gBrowser.selectedTab, tab1, "Tab1 is active");
+
+ // tab0 unblocked, tab1 unblocked
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked");
+
+ BrowserTestUtils.removeTab(tab0);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/*
+ * The "Play Tab" icon unblocks media
+ */
+add_task(async function testDelayPlayWhenUsingButton() {
+ info("Add media tabs");
+ let tab0 = await addMediaTab();
+ let tab1 = await addMediaTab();
+
+ info("Play both tabs");
+ await play(tab0, false);
+ await play(tab1, false);
+
+ // Both tabs are initially active media blocked after being played
+ ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked");
+
+ info("Press the Play Tab icon on tab0");
+ await pressIcon(tab0.overlayIcon);
+
+ // tab0 unblocked, tab1 blocked
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked");
+
+ info("Press the Play Tab icon on tab1");
+ await pressIcon(tab1.overlayIcon);
+
+ // tab0 unblocked, tab1 unblocked
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked");
+
+ BrowserTestUtils.removeTab(tab0);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/*
+ * Tab context menus have to show the menu icons "Play Tab" or "Play Tabs"
+ * depending on the number of tabs selected, and whether blocked media is present
+ */
+add_task(async function testTabContextMenu() {
+ info("Add media tab");
+ let tab0 = await addMediaTab();
+
+ let menuItemPlayTab = document.getElementById("context_playTab");
+ let menuItemPlaySelectedTabs = document.getElementById(
+ "context_playSelectedTabs"
+ );
+
+ // No active media yet:
+ // - "Play Tab" is hidden
+ // - "Play Tabs" is hidden
+ updateTabContextMenu(tab0);
+ ok(menuItemPlayTab.hidden, 'tab0 "Play Tab" is hidden');
+ ok(menuItemPlaySelectedTabs.hidden, 'tab0 "Play Tabs" is hidden');
+ ok(!activeMediaBlocked(tab0), "tab0 is not active media blocked");
+
+ info("Play tab0");
+ await play(tab0, false);
+
+ // Active media blocked:
+ // - "Play Tab" is visible
+ // - "Play Tabs" is hidden
+ updateTabContextMenu(tab0);
+ ok(!menuItemPlayTab.hidden, 'tab0 "Play Tab" is visible');
+ ok(menuItemPlaySelectedTabs.hidden, 'tab0 "Play Tabs" is hidden');
+ ok(activeMediaBlocked(tab0), "tab0 is active media blocked");
+
+ info("Play media on tab0");
+ await playMedia(tab0, { expectBlocked: false });
+
+ // Media is playing:
+ // - "Play Tab" is hidden
+ // - "Play Tabs" is hidden
+ updateTabContextMenu(tab0);
+ ok(menuItemPlayTab.hidden, 'tab0 "Play Tab" is hidden');
+ ok(menuItemPlaySelectedTabs.hidden, 'tab0 "Play Tabs" is hidden');
+ ok(!activeMediaBlocked(tab0), "tab0 is not active media blocked");
+
+ BrowserTestUtils.removeTab(tab0);
+});
diff --git a/browser/base/content/test/tabs/browser_tab_tooltips.js b/browser/base/content/test/tabs/browser_tab_tooltips.js
new file mode 100644
index 0000000000..0fb70c5a07
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_tooltips.js
@@ -0,0 +1,108 @@
+// Offset within the tab for the mouse event
+const MOUSE_OFFSET = 7;
+
+// Normal tooltips are positioned vertically at least this amount
+const MIN_VERTICAL_TOOLTIP_OFFSET = 18;
+
+function openTooltip(node, tooltip) {
+ let tooltipShownPromise = BrowserTestUtils.waitForEvent(
+ tooltip,
+ "popupshown"
+ );
+ window.windowUtils.disableNonTestMouseEvents(true);
+ EventUtils.synthesizeMouse(node, 2, 2, { type: "mouseover" });
+ EventUtils.synthesizeMouse(node, 4, 4, { type: "mousemove" });
+ EventUtils.synthesizeMouse(node, MOUSE_OFFSET, MOUSE_OFFSET, {
+ type: "mousemove",
+ });
+ EventUtils.synthesizeMouse(node, 2, 2, { type: "mouseout" });
+ window.windowUtils.disableNonTestMouseEvents(false);
+ return tooltipShownPromise;
+}
+
+function closeTooltip(node, tooltip) {
+ let tooltipHiddenPromise = BrowserTestUtils.waitForEvent(
+ tooltip,
+ "popuphidden"
+ );
+ EventUtils.synthesizeMouse(document.documentElement, 2, 2, {
+ type: "mousemove",
+ });
+ return tooltipHiddenPromise;
+}
+
+// This test verifies that the tab tooltip appears at the correct location, aligned
+// with the bottom of the tab, and that the tooltip appears near the close button.
+add_task(async function () {
+ const tabUrl =
+ "data:text/html,<html><head><title>A Tab</title></head><body>Hello</body></html>";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl);
+
+ let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+ await openTooltip(tab, tooltip);
+
+ let tabRect = tab.getBoundingClientRect();
+ let tooltipRect = tooltip.getBoundingClientRect();
+
+ isfuzzy(
+ tooltipRect.left,
+ tabRect.left + MOUSE_OFFSET,
+ 1,
+ "tooltip left position for tab"
+ );
+ ok(
+ tooltipRect.top >= tabRect.top + MIN_VERTICAL_TOOLTIP_OFFSET + MOUSE_OFFSET,
+ "tooltip top position for tab"
+ );
+ is(
+ tooltip.getAttribute("position"),
+ "",
+ "tooltip position attribute for tab"
+ );
+
+ await closeTooltip(tab, tooltip);
+
+ await openTooltip(tab.closeButton, tooltip);
+
+ let closeButtonRect = tab.closeButton.getBoundingClientRect();
+ tooltipRect = tooltip.getBoundingClientRect();
+
+ isfuzzy(
+ tooltipRect.left,
+ closeButtonRect.left + MOUSE_OFFSET,
+ 1,
+ "tooltip left position for close button"
+ );
+ ok(
+ tooltipRect.top >
+ closeButtonRect.top + MIN_VERTICAL_TOOLTIP_OFFSET + MOUSE_OFFSET,
+ "tooltip top position for close button"
+ );
+ ok(
+ !tooltip.hasAttribute("position"),
+ "tooltip position attribute for close button"
+ );
+
+ await closeTooltip(tab, tooltip);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test verifies that a mouse wheel closes the tooltip.
+add_task(async function () {
+ const tabUrl =
+ "data:text/html,<html><head><title>A Tab</title></head><body>Hello</body></html>";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl);
+
+ let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+ await openTooltip(tab, tooltip);
+
+ EventUtils.synthesizeWheel(tab, 4, 4, {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaY: 1.0,
+ });
+
+ is(tooltip.state, "closed", "wheel event closed the tooltip");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_tabswitch_contextmenu.js b/browser/base/content/test/tabs/browser_tabswitch_contextmenu.js
new file mode 100644
index 0000000000..bd671a86c6
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabswitch_contextmenu.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Don't switch tabs via the keyboard while the contextmenu is open.
+ */
+add_task(async function cant_tabswitch_mid_contextmenu() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/idontexist"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org/idontexist"
+ );
+
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+ let promisePopupShown = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "shown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "body",
+ 0,
+ 0,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ tab2.linkedBrowser
+ );
+ await promisePopupShown;
+ EventUtils.synthesizeKey("VK_TAB", { accelKey: true });
+ ok(tab2.selected, "tab2 should stay selected");
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ let promisePopupHidden = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "hidden"
+ );
+ contextMenu.hidePopup();
+ await promisePopupHidden;
+});
diff --git a/browser/base/content/test/tabs/browser_tabswitch_select.js b/browser/base/content/test/tabs/browser_tabswitch_select.js
new file mode 100644
index 0000000000..3868764bed
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabswitch_select.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/. */
+
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:support"
+ );
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,Goodbye"
+ );
+
+ gURLBar.select();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ let focusPromise = BrowserTestUtils.waitForEvent(
+ gURLBar.inputField,
+ "select",
+ true
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await focusPromise;
+
+ is(gURLBar.selectionStart, 0, "url is selected");
+ is(gURLBar.selectionEnd, 22, "url is selected");
+
+ // Now check that the url bar is focused when a new tab is opened while in fullscreen.
+
+ let fullScreenEntered = TestUtils.waitForCondition(
+ () => document.documentElement.getAttribute("sizemode") == "fullscreen"
+ );
+ BrowserFullScreen();
+ await fullScreenEntered;
+
+ tab2.linkedBrowser.focus();
+
+ // Open a new tab
+ focusPromise = BrowserTestUtils.waitForEvent(
+ gURLBar.inputField,
+ "focus",
+ true
+ );
+ EventUtils.synthesizeKey("T", { accelKey: true });
+ await focusPromise;
+
+ is(document.activeElement, gURLBar.inputField, "urlbar is focused");
+
+ let fullScreenExited = TestUtils.waitForCondition(
+ () => document.documentElement.getAttribute("sizemode") != "fullscreen"
+ );
+ BrowserFullScreen();
+ await fullScreenExited;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js b/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js
new file mode 100644
index 0000000000..b5d2762eec
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js
@@ -0,0 +1,28 @@
+// This test ensures that only one command update happens when switching tabs.
+
+"use strict";
+
+add_task(async function () {
+ const uri = "data:text/html,<body><input>";
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri);
+
+ let updates = [];
+ function countUpdates(event) {
+ updates.push(new Error().stack);
+ }
+ let updater = document.getElementById("editMenuCommandSetAll");
+ updater.addEventListener("commandupdate", countUpdates, true);
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ is(updates.length, 1, "only one command update per tab switch");
+ if (updates.length > 1) {
+ for (let stack of updates) {
+ info("Update stack:\n" + stack);
+ }
+ }
+
+ updater.removeEventListener("commandupdate", countUpdates, true);
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/base/content/test/tabs/browser_tabswitch_window_focus.js b/browser/base/content/test/tabs/browser_tabswitch_window_focus.js
new file mode 100644
index 0000000000..a808ab4f09
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabswitch_window_focus.js
@@ -0,0 +1,78 @@
+"use strict";
+
+// Allow to open popups without any kind of interaction.
+SpecialPowers.pushPrefEnv({ set: [["dom.disable_window_flip", false]] });
+
+const FILE = getRootDirectory(gTestPath) + "open_window_in_new_tab.html";
+
+add_task(async function () {
+ info("Opening first tab: " + FILE);
+ let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE);
+
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ FILE + "?open-click",
+ true
+ );
+ info("Opening second tab using a click");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open-click",
+ {},
+ firstTab.linkedBrowser
+ );
+
+ info("Waiting for the second tab to be opened");
+ let secondTab = await promiseTabOpened;
+
+ info("Going back to the first tab");
+ await BrowserTestUtils.switchTab(gBrowser, firstTab);
+
+ info("Focusing second tab by clicking on the first tab");
+ await BrowserTestUtils.switchTab(gBrowser, async function () {
+ await SpecialPowers.spawn(firstTab.linkedBrowser, [""], async function () {
+ content.document.querySelector("#focus").click();
+ });
+ });
+
+ is(gBrowser.selectedTab, secondTab, "Should've switched tabs");
+
+ await BrowserTestUtils.removeTab(firstTab);
+ await BrowserTestUtils.removeTab(secondTab);
+});
+
+add_task(async function () {
+ info("Opening first tab: " + FILE);
+ let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE);
+
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ FILE + "?open-mousedown",
+ true
+ );
+ info("Opening second tab using a click");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open-mousedown",
+ { type: "mousedown" },
+ firstTab.linkedBrowser
+ );
+
+ info("Waiting for the second tab to be opened");
+ let secondTab = await promiseTabOpened;
+
+ is(gBrowser.selectedTab, secondTab, "Should've switched tabs");
+
+ info("Ensuring we don't switch back");
+ await new Promise(resolve => {
+ // We need to wait for something _not_ happening, so we need to use an arbitrary setTimeout.
+ //
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(function () {
+ is(gBrowser.selectedTab, secondTab, "Should've remained in original tab");
+ resolve();
+ }, 500);
+ });
+
+ info("cleanup");
+ await BrowserTestUtils.removeTab(firstTab);
+ await BrowserTestUtils.removeTab(secondTab);
+});
diff --git a/browser/base/content/test/tabs/browser_undo_close_tabs.js b/browser/base/content/test/tabs/browser_undo_close_tabs.js
new file mode 100644
index 0000000000..e4e9da5d5d
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_undo_close_tabs.js
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_WARN_ON_CLOSE, false]],
+ });
+});
+
+add_task(async function withMultiSelectedTabs() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab("https://example.com/1");
+ let tab2 = await addTab("https://example.com/2");
+ let tab3 = await addTab("https://example.com/3");
+ let tab4 = await addTab("https://example.com/4");
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ gBrowser.selectedTab = tab2;
+ await triggerClickOn(tab4, { shiftKey: true });
+
+ ok(!initialTab.multiselected, "InitialTab is not multiselected");
+ ok(!tab1.multiselected, "Tab1 is not multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ ok(tab4.multiselected, "Tab4 is multiselected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+ gBrowser.removeMultiSelectedTabs();
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window) == 3,
+ "wait for the multi selected tabs to close in SessionStore"
+ );
+ is(
+ SessionStore.getLastClosedTabCount(window),
+ 3,
+ "SessionStore should know how many tabs were just closed"
+ );
+
+ undoCloseTab();
+ await TestUtils.waitForCondition(
+ () => gBrowser.tabs.length == 5,
+ "wait for the tabs to reopen"
+ );
+
+ is(
+ SessionStore.getLastClosedTabCount(window),
+ SessionStore.getClosedTabCountForWindow(window) ? 1 : 0,
+ "LastClosedTabCount should be reset"
+ );
+
+ info("waiting for the browsers to finish loading");
+ // Check that the tabs are restored in the correct order
+ for (let tabId of [2, 3, 4]) {
+ let browser = gBrowser.tabs[tabId].linkedBrowser;
+ await ContentTask.spawn(browser, tabId, async aTabId => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ content?.document?.readyState == "complete" &&
+ content?.document?.location.href == "https://example.com/" + aTabId
+ );
+ }, "waiting for tab " + aTabId + " to load");
+ });
+ }
+
+ gBrowser.removeAllTabsBut(initialTab);
+});
+
+add_task(async function withBothGroupsAndTab() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab("https://example.com/1");
+ let tab2 = await addTab("https://example.com/2");
+ let tab3 = await addTab("https://example.com/3");
+
+ gBrowser.selectedTab = tab2;
+ await triggerClickOn(tab3, { shiftKey: true });
+
+ ok(!initialTab.multiselected, "InitialTab is not multiselected");
+ ok(!tab1.multiselected, "Tab1 is not multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(tab3.multiselected, "Tab3 is multiselected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+ gBrowser.removeMultiSelectedTabs();
+ await TestUtils.waitForCondition(
+ () => gBrowser.tabs.length == 2,
+ "wait for the multiselected tabs to close"
+ );
+
+ is(
+ SessionStore.getLastClosedTabCount(window),
+ 2,
+ "SessionStore should know how many tabs were just closed"
+ );
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab4 = await addTab("http://example.com/4");
+
+ is(
+ SessionStore.getLastClosedTabCount(window),
+ 2,
+ "LastClosedTabCount should be the same"
+ );
+
+ gBrowser.removeTab(tab4);
+
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window) == 1,
+ "wait for the tab to close in SessionStore"
+ );
+
+ let count = 3;
+ for (let i = 0; i < 3; i++) {
+ is(
+ SessionStore.getLastClosedTabCount(window),
+ 1,
+ "LastClosedTabCount should be one"
+ );
+ undoCloseTab();
+
+ await TestUtils.waitForCondition(
+ () => gBrowser.tabs.length == count,
+ "wait for the tabs to reopen"
+ );
+ count++;
+ }
+
+ gBrowser.removeAllTabsBut(initialTab);
+});
+
+add_task(async function withCloseTabsToTheRight() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab("https://example.com/1");
+ await addTab("https://example.com/2");
+ await addTab("https://example.com/3");
+ await addTab("https://example.com/4");
+
+ gBrowser.removeTabsToTheEndFrom(tab1);
+ await TestUtils.waitForCondition(
+ () => gBrowser.tabs.length == 2,
+ "wait for the multiselected tabs to close"
+ );
+ is(
+ SessionStore.getLastClosedTabCount(window),
+ 3,
+ "SessionStore should know how many tabs were just closed"
+ );
+
+ undoCloseTab();
+ await TestUtils.waitForCondition(
+ () => gBrowser.tabs.length == 5,
+ "wait for the tabs to reopen"
+ );
+ info("waiting for the browsers to finish loading");
+ // Check that the tabs are restored in the correct order
+ for (let tabId of [2, 3, 4]) {
+ let browser = gBrowser.tabs[tabId].linkedBrowser;
+ ContentTask.spawn(browser, tabId, async aTabId => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ content?.document?.readyState == "complete" &&
+ content?.document?.location.href == "https://example.com/" + aTabId
+ );
+ }, "waiting for tab " + aTabId + " to load");
+ });
+ }
+
+ gBrowser.removeAllTabsBut(initialTab);
+});
diff --git a/browser/base/content/test/tabs/browser_undo_close_tabs_at_start.js b/browser/base/content/test/tabs/browser_undo_close_tabs_at_start.js
new file mode 100644
index 0000000000..9ad79ea1c8
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_undo_close_tabs_at_start.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_WARN_ON_CLOSE, false]],
+ });
+});
+
+add_task(async function replaceEmptyTabs() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const tabbrowser = win.gBrowser;
+ ok(
+ tabbrowser.tabs.length == 1 && tabbrowser.tabs[0].isEmpty,
+ "One blank tab should be opened."
+ );
+
+ let blankTab = tabbrowser.tabs[0];
+ await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ "https://example.com/1"
+ );
+ await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ "https://example.com/2"
+ );
+ await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ "https://example.com/3"
+ );
+
+ is(tabbrowser.tabs.length, 4, "There should be 4 tabs opened.");
+
+ tabbrowser.removeAllTabsBut(blankTab);
+
+ await TestUtils.waitForCondition(
+ () =>
+ SessionStore.getLastClosedTabCount(win) == 3 &&
+ tabbrowser.tabs.length == 1,
+ "wait for the tabs to close in SessionStore"
+ );
+ is(
+ SessionStore.getLastClosedTabCount(win),
+ 3,
+ "SessionStore should know how many tabs were just closed"
+ );
+
+ is(tabbrowser.selectedTab, blankTab, "The blank tab should be selected.");
+
+ win.undoCloseTab();
+
+ await TestUtils.waitForCondition(
+ () => tabbrowser.tabs.length == 3,
+ "wait for the tabs to reopen"
+ );
+
+ is(
+ SessionStore.getLastClosedTabCount(win),
+ SessionStore.getClosedTabCountForWindow(win) ? 1 : 0,
+ "LastClosedTabCount should be reset"
+ );
+
+ ok(
+ !tabbrowser.tabs.includes(blankTab),
+ "The blank tab should have been replaced."
+ );
+
+ // We can't (at the time of writing) check tab order.
+
+ // Cleanup
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js b/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js
new file mode 100644
index 0000000000..aca9166afe
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const DUMMY_FILE = "dummy_page.html";
+const DATA_URI = "data:text/html,Hi";
+const DATA_URI_SOURCE = "view-source:" + DATA_URI;
+
+// Test for bug 1345807.
+add_task(async function () {
+ // Open file:// page.
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append(DUMMY_FILE);
+ const uriString = Services.io.newFileURI(dir).spec;
+
+ await BrowserTestUtils.withNewTab(uriString, async function (fileBrowser) {
+ let filePid = await SpecialPowers.spawn(fileBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // Navigate to data URI.
+ let promiseLoad = BrowserTestUtils.browserLoaded(
+ fileBrowser,
+ false,
+ DATA_URI
+ );
+ BrowserTestUtils.loadURIString(fileBrowser, DATA_URI);
+ let href = await promiseLoad;
+ is(href, DATA_URI, "Check data URI loaded.");
+ let dataPid = await SpecialPowers.spawn(fileBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+ is(dataPid, filePid, "Check that data URI loaded in file content process.");
+
+ // Make sure we can view-source on the data URI page.
+ let promiseTab = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI_SOURCE);
+ BrowserViewSource(fileBrowser);
+ let viewSourceTab = await promiseTab;
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(viewSourceTab);
+ });
+ await SpecialPowers.spawn(
+ viewSourceTab.linkedBrowser,
+ [DATA_URI_SOURCE],
+ uri => {
+ is(
+ content.document.documentURI,
+ uri,
+ "Check that a view-source page was loaded."
+ );
+ }
+ );
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_visibleTabs_bookmarkAllTabs.js b/browser/base/content/test/tabs/browser_visibleTabs_bookmarkAllTabs.js
new file mode 100644
index 0000000000..d3b439ce8f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_visibleTabs_bookmarkAllTabs.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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();
+
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+ is(gBrowser.visibleTabs.length, 1, "1 tab should be open");
+
+ // Add a tab
+ let testTab1 = BrowserTestUtils.addTab(gBrowser);
+ is(gBrowser.visibleTabs.length, 2, "2 tabs should be open");
+
+ let testTab2 = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+ is(gBrowser.visibleTabs.length, 3, "3 tabs should be open");
+ // Wait for tab load, the code checks for currentURI.
+ testTab2.linkedBrowser.addEventListener(
+ "load",
+ function () {
+ // Hide the original tab
+ gBrowser.selectedTab = testTab2;
+ gBrowser.showOnlyTheseTabs([testTab2]);
+ is(gBrowser.visibleTabs.length, 1, "1 tab should be visible");
+
+ // Add a tab that will get pinned
+ let pinned = BrowserTestUtils.addTab(gBrowser);
+ is(gBrowser.visibleTabs.length, 2, "2 tabs should be visible now");
+ gBrowser.pinTab(pinned);
+ is(
+ BookmarkTabHidden(),
+ false,
+ "Bookmark Tab should be visible on a normal tab"
+ );
+ gBrowser.selectedTab = pinned;
+ is(
+ BookmarkTabHidden(),
+ false,
+ "Bookmark Tab should be visible on a pinned tab"
+ );
+
+ // Show all tabs
+ let allTabs = Array.from(gBrowser.tabs);
+ gBrowser.showOnlyTheseTabs(allTabs);
+
+ // reset the environment
+ gBrowser.removeTab(testTab2);
+ gBrowser.removeTab(testTab1);
+ gBrowser.removeTab(pinned);
+ is(gBrowser.visibleTabs.length, 1, "only orig is left and visible");
+ is(gBrowser.tabs.length, 1, "sanity check that it matches");
+ is(gBrowser.selectedTab, origTab, "got the orig tab");
+ is(origTab.hidden, false, "and it's not hidden -- visible!");
+ finish();
+ },
+ { capture: true, once: true }
+ );
+}
+
+function BookmarkTabHidden() {
+ updateTabContextMenu();
+ return document.getElementById("context_bookmarkTab").hidden;
+}
diff --git a/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js b/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js
new file mode 100644
index 0000000000..202c43ce47
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.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/. */
+
+const remoteClientsFixture = [
+ { id: 1, name: "Foo" },
+ { id: 2, name: "Bar" },
+];
+
+add_task(async function test() {
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+ is(gBrowser.visibleTabs.length, 1, "there is one visible tab");
+ let testTab = BrowserTestUtils.addTab(gBrowser);
+ is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs");
+
+ // Check the context menu with two tabs
+ updateTabContextMenu(origTab);
+ is(
+ document.getElementById("context_closeTab").disabled,
+ false,
+ "Close Tab is enabled"
+ );
+
+ // Hide the original tab.
+ gBrowser.selectedTab = testTab;
+ gBrowser.showOnlyTheseTabs([testTab]);
+ is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab");
+
+ // Check the context menu with one tab.
+ updateTabContextMenu(testTab);
+ is(
+ document.getElementById("context_closeTab").disabled,
+ false,
+ "Close Tab is enabled when more than one tab exists"
+ );
+
+ // Add a tab that will get pinned
+ // So now there's one pinned tab, one visible unpinned tab, and one hidden tab
+ let pinned = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.pinTab(pinned);
+ is(gBrowser.visibleTabs.length, 2, "now there are two visible tabs");
+
+ // Check the context menu on the pinned tab
+ updateTabContextMenu(pinned);
+ ok(
+ !document.getElementById("context_closeTabOptions").disabled,
+ "Close Multiple Tabs is enabled on pinned tab"
+ );
+ ok(
+ !document.getElementById("context_closeOtherTabs").disabled,
+ "Close Other Tabs is enabled on pinned tab"
+ );
+ ok(
+ document.getElementById("context_closeTabsToTheStart").disabled,
+ "Close Tabs To The Start is disabled on pinned tab"
+ );
+ ok(
+ !document.getElementById("context_closeTabsToTheEnd").disabled,
+ "Close Tabs To The End is enabled on pinned tab"
+ );
+
+ // Check the context menu on the unpinned visible tab
+ updateTabContextMenu(testTab);
+ ok(
+ document.getElementById("context_closeTabOptions").disabled,
+ "Close Multiple Tabs is disabled on single unpinned tab"
+ );
+ ok(
+ document.getElementById("context_closeOtherTabs").disabled,
+ "Close Other Tabs is disabled on single unpinned tab"
+ );
+ ok(
+ document.getElementById("context_closeTabsToTheStart").disabled,
+ "Close Tabs To The Start is disabled on single unpinned tab"
+ );
+ ok(
+ document.getElementById("context_closeTabsToTheEnd").disabled,
+ "Close Tabs To The End is disabled on single unpinned tab"
+ );
+
+ // Show all tabs
+ let allTabs = Array.from(gBrowser.tabs);
+ gBrowser.showOnlyTheseTabs(allTabs);
+
+ // Check the context menu now
+ updateTabContextMenu(testTab);
+ ok(
+ !document.getElementById("context_closeTabOptions").disabled,
+ "Close Multiple Tabs is enabled on unpinned tab when there's another unpinned tab"
+ );
+ ok(
+ !document.getElementById("context_closeOtherTabs").disabled,
+ "Close Other Tabs is enabled on unpinned tab when there's another unpinned tab"
+ );
+ ok(
+ !document.getElementById("context_closeTabsToTheStart").disabled,
+ "Close Tabs To The Start is enabled on last unpinned tab when there's another unpinned tab"
+ );
+ ok(
+ document.getElementById("context_closeTabsToTheEnd").disabled,
+ "Close Tabs To The End is disabled on last unpinned tab"
+ );
+
+ // Check the context menu of the original tab
+ // Close Tabs To The End should now be enabled
+ updateTabContextMenu(origTab);
+ ok(
+ !document.getElementById("context_closeTabsToTheEnd").disabled,
+ "Close Tabs To The End is enabled on unpinned tab when followed by another"
+ );
+
+ gBrowser.removeTab(testTab);
+ gBrowser.removeTab(pinned);
+});
diff --git a/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js b/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js
new file mode 100644
index 0000000000..dfba084995
--- /dev/null
+++ b/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const HOME_URL = `${TEST_ROOT}link_in_tab_title_and_url_prefilled.html`;
+const HOME_TITLE = HOME_URL.substring("https://".length);
+const WAIT_A_BIT_URL = `${TEST_ROOT}wait-a-bit.sjs`;
+const WAIT_A_BIT_LOADING_TITLE = WAIT_A_BIT_URL.substring("https://".length);
+const WAIT_A_BIT_PAGE_TITLE = "wait a bit";
+const REQUEST_TIMEOUT_URL = `${TEST_ROOT}request-timeout.sjs`;
+const REQUEST_TIMEOUT_LOADING_TITLE = REQUEST_TIMEOUT_URL.substring(
+ "https://".length
+);
+const BLANK_URL = "about:blank";
+const BLANK_TITLE = "New Tab";
+
+const OPEN_BY = {
+ CLICK: "click",
+ CONTEXT_MENU: "context_menu",
+};
+
+const OPEN_AS = {
+ FOREGROUND: "foreground",
+ BACKGROUND: "background",
+};
+
+async function doTestInSameWindow({
+ link,
+ openBy,
+ openAs,
+ loadingState,
+ actionWhileLoading,
+ finalState,
+}) {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ // NOTE: The behavior after the click <a href="about:blank">link</a>
+ // (no target) is different when the URL is opened directly with
+ // BrowserTestUtils.withNewTab() and when it is loaded later.
+ // Specifically, if we load `about:blank`, expect to see `New Tab` as the
+ // title of the tab, but the former will continue to display the URL that
+ // was previously displayed. Therefore, use the latter way.
+ BrowserTestUtils.loadURIString(browser, HOME_URL);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ HOME_URL
+ );
+
+ info(`Open link for ${link} by ${openBy} as ${openAs}`);
+ const onNewTabCreated = waitForNewTabWithLoadRequest();
+ const href = await openLink(browser, link, openBy, openAs);
+
+ info("Wait until starting to load in the target tab");
+ const target = await onNewTabCreated;
+ Assert.equal(target.selected, openAs === OPEN_AS.FOREGROUND);
+ Assert.equal(gURLBar.value, loadingState.urlbar);
+ Assert.equal(target.textLabel.textContent, loadingState.tab);
+
+ await actionWhileLoading(
+ BrowserTestUtils.browserLoaded(target.linkedBrowser, false, href)
+ );
+
+ info("Check the final result");
+ Assert.equal(gURLBar.value, finalState.urlbar);
+ Assert.equal(target.textLabel.textContent, finalState.tab);
+ const sessionHistory = await new Promise(r =>
+ SessionStore.getSessionHistory(target, r)
+ );
+ Assert.deepEqual(
+ sessionHistory.entries.map(e => e.url),
+ finalState.history
+ );
+
+ BrowserTestUtils.removeTab(target);
+ });
+}
+
+async function doTestWithNewWindow({ link, expectedSetURICalled }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.link.open_newwindow", 2]],
+ });
+
+ await BrowserTestUtils.withNewTab(HOME_URL, async browser => {
+ const onNewWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded();
+
+ info(`Open link for ${link}`);
+ const href = await openLink(
+ browser,
+ link,
+ OPEN_BY.CLICK,
+ OPEN_AS.FOREGROUND
+ );
+
+ info("Wait until opening a new window");
+ const win = await onNewWindowOpened;
+
+ info("Check whether gURLBar.setURI is called while loading the page");
+ const sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+ let isSetURIWhileLoading = false;
+ sandbox.stub(win.gURLBar, "setURI").callsFake(uri => {
+ if (
+ !uri &&
+ win.gBrowser.selectedBrowser.browsingContext.nonWebControlledBlankURI
+ ) {
+ isSetURIWhileLoading = true;
+ }
+ });
+ await BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ href
+ );
+ sandbox.restore();
+
+ Assert.equal(isSetURIWhileLoading, expectedSetURICalled);
+ Assert.equal(
+ !!win.gBrowser.selectedBrowser.browsingContext.nonWebControlledBlankURI,
+ expectedSetURICalled
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doSessionRestoreTest({
+ link,
+ openBy,
+ openAs,
+ expectedSessionHistory,
+ expectedSessionRestored,
+}) {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ BrowserTestUtils.loadURIString(browser, HOME_URL);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ HOME_URL
+ );
+
+ info(`Open link for ${link} by ${openBy} as ${openAs}`);
+ const onNewTabCreated = waitForNewTabWithLoadRequest();
+ const href = await openLink(browser, link, openBy, openAs);
+ const target = await onNewTabCreated;
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ target.linkedBrowser.browsingContext
+ .mostRecentLoadingSessionHistoryEntry
+ );
+
+ info("Close the session");
+ const sessionPromise = BrowserTestUtils.waitForSessionStoreUpdate(target);
+ BrowserTestUtils.removeTab(target);
+ await sessionPromise;
+
+ info("Restore the session");
+ const restoredTab = SessionStore.undoCloseTab(window, 0);
+ await BrowserTestUtils.browserLoaded(restoredTab.linkedBrowser);
+
+ info("Check the loaded URL of restored tab");
+ Assert.equal(
+ restoredTab.linkedBrowser.currentURI.spec === href,
+ expectedSessionRestored
+ );
+
+ if (expectedSessionRestored) {
+ info("Check the session history of restored tab");
+ const sessionHistory = await new Promise(r =>
+ SessionStore.getSessionHistory(restoredTab, r)
+ );
+ Assert.deepEqual(
+ sessionHistory.entries.map(e => e.url),
+ expectedSessionHistory
+ );
+ }
+
+ BrowserTestUtils.removeTab(restoredTab);
+ });
+}
+
+async function openLink(browser, link, openBy, openAs) {
+ let href;
+ const openAsBackground = openAs === OPEN_AS.BACKGROUND;
+ if (openBy === OPEN_BY.CLICK) {
+ href = await synthesizeMouse(browser, link, {
+ ctrlKey: openAsBackground,
+ metaKey: openAsBackground,
+ });
+ } else if (openBy === OPEN_BY.CONTEXT_MENU) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.loadInBackground", openAsBackground]],
+ });
+
+ const contextMenu = document.getElementById("contentAreaContextMenu");
+ const onPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ href = await synthesizeMouse(browser, link, {
+ type: "contextmenu",
+ button: 2,
+ });
+
+ await onPopupShown;
+
+ const openLinkMenuItem = contextMenu.querySelector(
+ "#context-openlinkintab"
+ );
+ contextMenu.activateItem(openLinkMenuItem);
+
+ await SpecialPowers.popPrefEnv();
+ } else {
+ throw new Error("Invalid openBy");
+ }
+
+ return href;
+}
+
+async function synthesizeMouse(browser, link, event) {
+ return SpecialPowers.spawn(
+ browser,
+ [link, event],
+ (linkInContent, eventInContent) => {
+ const target = content.document.getElementById(linkInContent);
+ EventUtils.synthesizeMouseAtCenter(target, eventInContent, content);
+ return target.href;
+ }
+ );
+}
+
+async function waitForNewTabWithLoadRequest() {
+ return new Promise(resolve =>
+ gBrowser.addTabsProgressListener({
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ gBrowser.removeTabsProgressListener(this);
+ resolve(gBrowser.getTabForBrowser(aBrowser));
+ }
+ },
+ })
+ );
+}
diff --git a/browser/base/content/test/tabs/dummy_page.html b/browser/base/content/test/tabs/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/base/content/test/tabs/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/base/content/test/tabs/file_about_child.html b/browser/base/content/test/tabs/file_about_child.html
new file mode 100644
index 0000000000..41fb745451
--- /dev/null
+++ b/browser/base/content/test/tabs/file_about_child.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1329032</title>
+</head>
+<body>
+ Just an about page that only loads in the child!
+</body>
+</html>
diff --git a/browser/base/content/test/tabs/file_about_parent.html b/browser/base/content/test/tabs/file_about_parent.html
new file mode 100644
index 0000000000..0d910f860b
--- /dev/null
+++ b/browser/base/content/test/tabs/file_about_parent.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1329032</title>
+</head>
+<body>
+ <a href="about:test-about-principal-child" id="aboutchildprincipal">about:test-about-principal-child</a>
+</body>
+</html>
diff --git a/browser/base/content/test/tabs/file_about_srcdoc.html b/browser/base/content/test/tabs/file_about_srcdoc.html
new file mode 100644
index 0000000000..0a8d0d74bf
--- /dev/null
+++ b/browser/base/content/test/tabs/file_about_srcdoc.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe srcdoc="hello world!"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/file_anchor_elements.html b/browser/base/content/test/tabs/file_anchor_elements.html
new file mode 100644
index 0000000000..598a3bd825
--- /dev/null
+++ b/browser/base/content/test/tabs/file_anchor_elements.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+ <title>Testing whether paste event is fired at middle click on anchor elements</title>
+</head>
+<body>
+ <p>Here is an <a id="a_with_href" href="https://example.com/#a_with_href">anchor element</a></p>
+ <p contenteditable>Here is an <a id="editable_a_with_href" href="https://example.com/#editable_a_with_href">editable anchor element</a></p>
+ <p contenteditable>Here is <span contenteditable="false"><a id="non-editable_a_with_href" href="https://example.com/#non-editable_a_with_href">non-editable anchor element</a></span>
+ <p>Here is an <a id="a_with_name" name="a_with_name">anchor element without href</a></p>
+</body>
+</html>
diff --git a/browser/base/content/test/tabs/file_mediaPlayback.html b/browser/base/content/test/tabs/file_mediaPlayback.html
new file mode 100644
index 0000000000..a6979287e2
--- /dev/null
+++ b/browser/base/content/test/tabs/file_mediaPlayback.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<audio src="audio.ogg" controls loop>
diff --git a/browser/base/content/test/tabs/file_new_tab_page.html b/browser/base/content/test/tabs/file_new_tab_page.html
new file mode 100644
index 0000000000..4ef22a8c7c
--- /dev/null
+++ b/browser/base/content/test/tabs/file_new_tab_page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <a href="http://example.com/#linkclick" id="link_to_example_com">go to example.com</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/file_rel_opener_noopener.html b/browser/base/content/test/tabs/file_rel_opener_noopener.html
new file mode 100644
index 0000000000..78e872005c
--- /dev/null
+++ b/browser/base/content/test/tabs/file_rel_opener_noopener.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <a target="_blank" rel="noopener" href="https://example.com/browser/browser/base/content/test/tabs/blank.html" id="link_noopener_examplecom">Go to example.com</a>
+ <a target="_blank" rel="opener" href="https://example.com/browser/browser/base/content/test/tabs/blank.html" id="link_opener_examplecom">Go to example.com</a>
+ <a target="_blank" rel="noopener" href="https://example.org/browser/browser/base/content/test/tabs/blank.html" id="link_noopener_exampleorg">Go to example.org</a>
+ <a target="_blank" rel="opener" href="https://example.org/browser/browser/base/content/test/tabs/blank.html" id="link_opener_exampleorg">Go to example.org</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/head.js b/browser/base/content/test/tabs/head.js
new file mode 100644
index 0000000000..abd6c060f7
--- /dev/null
+++ b/browser/base/content/test/tabs/head.js
@@ -0,0 +1,564 @@
+function updateTabContextMenu(tab) {
+ let menu = document.getElementById("tabContextMenu");
+ if (!tab) {
+ tab = gBrowser.selectedTab;
+ }
+ var evt = new Event("");
+ tab.dispatchEvent(evt);
+ menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
+ is(
+ window.TabContextMenu.contextTab,
+ tab,
+ "TabContextMenu context is the expected tab"
+ );
+ menu.hidePopup();
+}
+
+function triggerClickOn(target, options) {
+ let promise = BrowserTestUtils.waitForEvent(target, "click");
+ if (AppConstants.platform == "macosx") {
+ options = {
+ metaKey: options.ctrlKey,
+ shiftKey: options.shiftKey,
+ };
+ }
+ EventUtils.synthesizeMouseAtCenter(target, options);
+ return promise;
+}
+
+function triggerMiddleClickOn(target) {
+ let promise = BrowserTestUtils.waitForEvent(target, "click");
+ EventUtils.synthesizeMouseAtCenter(target, { button: 1 });
+ return promise;
+}
+
+async function addTab(url = "http://mochi.test:8888/", params) {
+ return addTabTo(gBrowser, url, params);
+}
+
+async function addTabTo(
+ targetBrowser,
+ url = "http://mochi.test:8888/",
+ params = {}
+) {
+ params.skipAnimation = true;
+ const tab = BrowserTestUtils.addTab(targetBrowser, url, params);
+ const browser = targetBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return tab;
+}
+
+async function addMediaTab() {
+ const PAGE =
+ "https://example.com/browser/browser/base/content/test/tabs/file_mediaPlayback.html";
+ const tab = BrowserTestUtils.addTab(gBrowser, PAGE, { skipAnimation: true });
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return tab;
+}
+
+function muted(tab) {
+ return tab.linkedBrowser.audioMuted;
+}
+
+function activeMediaBlocked(tab) {
+ return tab.activeMediaBlocked;
+}
+
+async function toggleMuteAudio(tab, expectMuted) {
+ let mutedPromise = get_wait_for_mute_promise(tab, expectMuted);
+ tab.toggleMuteAudio();
+ await mutedPromise;
+}
+
+async function pressIcon(icon) {
+ let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+ await hover_icon(icon, tooltip);
+ EventUtils.synthesizeMouseAtCenter(icon, { button: 0 });
+ leave_icon(icon);
+}
+
+async function wait_for_tab_playing_event(tab, expectPlaying) {
+ if (tab.soundPlaying == expectPlaying) {
+ ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+ return true;
+ }
+ return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => {
+ if (event.detail.changed.includes("soundplaying")) {
+ is(
+ tab.hasAttribute("soundplaying"),
+ expectPlaying,
+ "The tab should " + (expectPlaying ? "" : "not ") + "be playing"
+ );
+ is(
+ tab.soundPlaying,
+ expectPlaying,
+ "The tab should " + (expectPlaying ? "" : "not ") + "be playing"
+ );
+ return true;
+ }
+ return false;
+ });
+}
+
+async function wait_for_tab_media_blocked_event(tab, expectMediaBlocked) {
+ if (tab.activeMediaBlocked == expectMediaBlocked) {
+ ok(
+ true,
+ "The tab should " +
+ (expectMediaBlocked ? "" : "not ") +
+ "be activemedia-blocked"
+ );
+ return true;
+ }
+ return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => {
+ if (event.detail.changed.includes("activemedia-blocked")) {
+ is(
+ tab.hasAttribute("activemedia-blocked"),
+ expectMediaBlocked,
+ "The tab should " +
+ (expectMediaBlocked ? "" : "not ") +
+ "be activemedia-blocked"
+ );
+ is(
+ tab.activeMediaBlocked,
+ expectMediaBlocked,
+ "The tab should " +
+ (expectMediaBlocked ? "" : "not ") +
+ "be activemedia-blocked"
+ );
+ return true;
+ }
+ return false;
+ });
+}
+
+async function is_audio_playing(tab) {
+ let browser = tab.linkedBrowser;
+ let isPlaying = await SpecialPowers.spawn(browser, [], async function () {
+ let audio = content.document.querySelector("audio");
+ return !audio.paused;
+ });
+ return isPlaying;
+}
+
+async function play(tab, expectPlaying = true) {
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let audio = content.document.querySelector("audio");
+ audio.play();
+ });
+
+ // If the tab has already been muted, it means the tab won't get soundplaying,
+ // so we don't need to check this attribute.
+ if (browser.audioMuted) {
+ return;
+ }
+
+ if (expectPlaying) {
+ await wait_for_tab_playing_event(tab, true);
+ } else {
+ await wait_for_tab_media_blocked_event(tab, true);
+ }
+}
+
+function disable_non_test_mouse(disable) {
+ let utils = window.windowUtils;
+ utils.disableNonTestMouseEvents(disable);
+}
+
+function hover_icon(icon, tooltip) {
+ disable_non_test_mouse(true);
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+ EventUtils.synthesizeMouse(icon, 1, 1, { type: "mouseover" });
+ EventUtils.synthesizeMouse(icon, 2, 2, { type: "mousemove" });
+ EventUtils.synthesizeMouse(icon, 3, 3, { type: "mousemove" });
+ EventUtils.synthesizeMouse(icon, 4, 4, { type: "mousemove" });
+ return popupShownPromise;
+}
+
+function leave_icon(icon) {
+ EventUtils.synthesizeMouse(icon, 0, 0, { type: "mouseout" });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+
+ disable_non_test_mouse(false);
+}
+
+// The set of tabs which have ever had their mute state changed.
+// Used to determine whether the tab should have a muteReason value.
+let everMutedTabs = new WeakSet();
+
+function get_wait_for_mute_promise(tab, expectMuted) {
+ return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => {
+ if (
+ event.detail.changed.includes("muted") ||
+ event.detail.changed.includes("activemedia-blocked")
+ ) {
+ is(
+ tab.hasAttribute("muted"),
+ expectMuted,
+ "The tab should " + (expectMuted ? "" : "not ") + "be muted"
+ );
+ is(
+ tab.muted,
+ expectMuted,
+ "The tab muted property " + (expectMuted ? "" : "not ") + "be true"
+ );
+
+ if (expectMuted || everMutedTabs.has(tab)) {
+ everMutedTabs.add(tab);
+ is(tab.muteReason, null, "The tab should have a null muteReason value");
+ } else {
+ is(
+ tab.muteReason,
+ undefined,
+ "The tab should have an undefined muteReason value"
+ );
+ }
+ return true;
+ }
+ return false;
+ });
+}
+
+async function test_mute_tab(tab, icon, expectMuted) {
+ let mutedPromise = get_wait_for_mute_promise(tab, expectMuted);
+
+ let activeTab = gBrowser.selectedTab;
+
+ let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+
+ await hover_icon(icon, tooltip);
+ EventUtils.synthesizeMouseAtCenter(icon, { button: 0 });
+ leave_icon(icon);
+
+ is(
+ gBrowser.selectedTab,
+ activeTab,
+ "Clicking on mute should not change the currently selected tab"
+ );
+
+ // If the audio is playing, we should check whether clicking on icon affects
+ // the media element's playing state.
+ let isAudioPlaying = await is_audio_playing(tab);
+ if (isAudioPlaying) {
+ await wait_for_tab_playing_event(tab, !expectMuted);
+ }
+
+ return mutedPromise;
+}
+
+async function dragAndDrop(
+ tab1,
+ tab2,
+ copy,
+ destWindow = window,
+ afterTab = true
+) {
+ let rect = tab2.getBoundingClientRect();
+ let event = {
+ ctrlKey: copy,
+ altKey: copy,
+ clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1),
+ clientY: rect.top + rect.height / 2,
+ };
+
+ if (destWindow != window) {
+ // Make sure that both tab1 and tab2 are visible
+ window.focus();
+ window.moveTo(rect.left, rect.top + rect.height * 3);
+ }
+
+ let originalTPos = tab1._tPos;
+ EventUtils.synthesizeDrop(
+ tab1,
+ tab2,
+ null,
+ copy ? "copy" : "move",
+ window,
+ destWindow,
+ event
+ );
+ // Ensure dnd suppression is cleared.
+ EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, destWindow);
+ if (!copy && destWindow == window) {
+ await BrowserTestUtils.waitForCondition(
+ () => tab1._tPos != originalTPos,
+ "Waiting for tab position to be updated"
+ );
+ } else if (destWindow != window) {
+ await BrowserTestUtils.waitForCondition(
+ () => tab1.closing,
+ "Waiting for tab closing"
+ );
+ }
+}
+
+function getUrl(tab) {
+ return tab.linkedBrowser.currentURI.spec;
+}
+
+/**
+ * Takes a xul:browser and makes sure that the remoteTypes for the browser in
+ * both the parent and the child processes are the same.
+ *
+ * @param {xul:browser} browser
+ * A xul:browser.
+ * @param {string} expectedRemoteType
+ * The expected remoteType value for the browser in both the parent
+ * and child processes.
+ * @param {optional string} message
+ * If provided, shows this string as the message when remoteType values
+ * do not match. If not present, it uses the default message defined
+ * in the function parameters.
+ */
+function checkBrowserRemoteType(
+ browser,
+ expectedRemoteType,
+ message = `Ensures that tab runs in the ${expectedRemoteType} content process.`
+) {
+ // Check both parent and child to ensure that they have the correct remoteType.
+ if (expectedRemoteType == E10SUtils.WEB_REMOTE_TYPE) {
+ ok(E10SUtils.isWebRemoteType(browser.remoteType), message);
+ ok(
+ E10SUtils.isWebRemoteType(browser.messageManager.remoteType),
+ "Parent and child process should agree on the remote type."
+ );
+ } else {
+ is(browser.remoteType, expectedRemoteType, message);
+ is(
+ browser.messageManager.remoteType,
+ expectedRemoteType,
+ "Parent and child process should agree on the remote type."
+ );
+ }
+}
+
+function test_url_for_process_types({
+ url,
+ chromeResult,
+ webContentResult,
+ privilegedAboutContentResult,
+ privilegedMozillaContentResult,
+ extensionProcessResult,
+}) {
+ const CHROME_PROCESS = E10SUtils.NOT_REMOTE;
+ const WEB_CONTENT_PROCESS = E10SUtils.WEB_REMOTE_TYPE;
+ const PRIVILEGEDABOUT_CONTENT_PROCESS = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
+ const PRIVILEGEDMOZILLA_CONTENT_PROCESS =
+ E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE;
+ const EXTENSION_PROCESS = E10SUtils.EXTENSION_REMOTE_TYPE;
+
+ is(
+ E10SUtils.canLoadURIInRemoteType(url, /* fission */ false, CHROME_PROCESS),
+ chromeResult,
+ "Check URL in chrome process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url,
+ /* fission */ false,
+ WEB_CONTENT_PROCESS
+ ),
+ webContentResult,
+ "Check URL in web content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url,
+ /* fission */ false,
+ PRIVILEGEDABOUT_CONTENT_PROCESS
+ ),
+ privilegedAboutContentResult,
+ "Check URL in privileged about content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url,
+ /* fission */ false,
+ PRIVILEGEDMOZILLA_CONTENT_PROCESS
+ ),
+ privilegedMozillaContentResult,
+ "Check URL in privileged mozilla content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url,
+ /* fission */ false,
+ EXTENSION_PROCESS
+ ),
+ extensionProcessResult,
+ "Check URL in extension process."
+ );
+
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "#foo",
+ /* fission */ false,
+ CHROME_PROCESS
+ ),
+ chromeResult,
+ "Check URL with ref in chrome process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "#foo",
+ /* fission */ false,
+ WEB_CONTENT_PROCESS
+ ),
+ webContentResult,
+ "Check URL with ref in web content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "#foo",
+ /* fission */ false,
+ PRIVILEGEDABOUT_CONTENT_PROCESS
+ ),
+ privilegedAboutContentResult,
+ "Check URL with ref in privileged about content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "#foo",
+ /* fission */ false,
+ PRIVILEGEDMOZILLA_CONTENT_PROCESS
+ ),
+ privilegedMozillaContentResult,
+ "Check URL with ref in privileged mozilla content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "#foo",
+ /* fission */ false,
+ EXTENSION_PROCESS
+ ),
+ extensionProcessResult,
+ "Check URL with ref in extension process."
+ );
+
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo",
+ /* fission */ false,
+ CHROME_PROCESS
+ ),
+ chromeResult,
+ "Check URL with query in chrome process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo",
+ /* fission */ false,
+ WEB_CONTENT_PROCESS
+ ),
+ webContentResult,
+ "Check URL with query in web content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo",
+ /* fission */ false,
+ PRIVILEGEDABOUT_CONTENT_PROCESS
+ ),
+ privilegedAboutContentResult,
+ "Check URL with query in privileged about content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo",
+ /* fission */ false,
+ PRIVILEGEDMOZILLA_CONTENT_PROCESS
+ ),
+ privilegedMozillaContentResult,
+ "Check URL with query in privileged mozilla content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo",
+ /* fission */ false,
+ EXTENSION_PROCESS
+ ),
+ extensionProcessResult,
+ "Check URL with query in extension process."
+ );
+
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo#bar",
+ /* fission */ false,
+ CHROME_PROCESS
+ ),
+ chromeResult,
+ "Check URL with query and ref in chrome process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo#bar",
+ /* fission */ false,
+ WEB_CONTENT_PROCESS
+ ),
+ webContentResult,
+ "Check URL with query and ref in web content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo#bar",
+ /* fission */ false,
+ PRIVILEGEDABOUT_CONTENT_PROCESS
+ ),
+ privilegedAboutContentResult,
+ "Check URL with query and ref in privileged about content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo#bar",
+ /* fission */ false,
+ PRIVILEGEDMOZILLA_CONTENT_PROCESS
+ ),
+ privilegedMozillaContentResult,
+ "Check URL with query and ref in privileged mozilla content process."
+ );
+ is(
+ E10SUtils.canLoadURIInRemoteType(
+ url + "?foo#bar",
+ /* fission */ false,
+ EXTENSION_PROCESS
+ ),
+ extensionProcessResult,
+ "Check URL with query and ref in extension process."
+ );
+}
+
+/*
+ * Get a file URL for the local file name.
+ */
+function fileURL(filename) {
+ let ifile = getChromeDir(getResolvedURI(gTestPath));
+ ifile.append(filename);
+ return Services.io.newFileURI(ifile).spec;
+}
+
+/*
+ * Get a http URL for the local file name.
+ */
+function httpURL(filename, host = "https://example.com/") {
+ let root = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ host
+ );
+ return root + filename;
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
diff --git a/browser/base/content/test/tabs/helper_origin_attrs_testing.js b/browser/base/content/test/tabs/helper_origin_attrs_testing.js
new file mode 100644
index 0000000000..5c7938baca
--- /dev/null
+++ b/browser/base/content/test/tabs/helper_origin_attrs_testing.js
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 NUM_USER_CONTEXTS = 3;
+
+var xulFrameLoaderCreatedListenerInfo;
+
+function initXulFrameLoaderListenerInfo() {
+ xulFrameLoaderCreatedListenerInfo = {};
+ xulFrameLoaderCreatedListenerInfo.numCalledSoFar = 0;
+}
+
+function handleEvent(aEvent) {
+ if (aEvent.type != "XULFrameLoaderCreated") {
+ return;
+ }
+ // Ignore <browser> element in about:preferences and any other special pages
+ if ("gBrowser" in aEvent.target.ownerGlobal) {
+ xulFrameLoaderCreatedListenerInfo.numCalledSoFar++;
+ }
+}
+
+async function openURIInRegularTab(uri, win = window) {
+ info(`Opening url ${uri} in a regular tab`);
+
+ initXulFrameLoaderListenerInfo();
+ win.gBrowser.addEventListener("XULFrameLoaderCreated", handleEvent);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, uri);
+ info(
+ `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedListenerInfo.numCalledSoFar} time(s) for ${uri} in regular tab`
+ );
+
+ is(
+ xulFrameLoaderCreatedListenerInfo.numCalledSoFar,
+ 1,
+ "XULFrameLoaderCreated fired correct number of times"
+ );
+
+ win.gBrowser.removeEventListener("XULFrameLoaderCreated", handleEvent);
+ return { tab, uri };
+}
+
+async function openURIInContainer(uri, win, userContextId) {
+ info(`Opening url ${uri} in user context ${userContextId}`);
+ initXulFrameLoaderListenerInfo();
+ win.gBrowser.addEventListener("XULFrameLoaderCreated", handleEvent);
+
+ let tab = BrowserTestUtils.addTab(win.gBrowser, uri, {
+ userContextId,
+ });
+ is(
+ tab.getAttribute("usercontextid"),
+ userContextId.toString(),
+ "New tab has correct user context id"
+ );
+
+ let browser = tab.linkedBrowser;
+
+ await BrowserTestUtils.browserLoaded(browser, false, uri);
+ info(
+ `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedListenerInfo.numCalledSoFar}
+ time(s) for ${uri} in container tab ${userContextId}`
+ );
+
+ is(
+ xulFrameLoaderCreatedListenerInfo.numCalledSoFar,
+ 1,
+ "XULFrameLoaderCreated fired correct number of times"
+ );
+
+ win.gBrowser.removeEventListener("XULFrameLoaderCreated", handleEvent);
+
+ return { tab, user_context_id: userContextId, uri };
+}
+
+async function openURIInPrivateTab(uri) {
+ info(
+ `Opening url ${
+ uri ? uri : "about:privatebrowsing"
+ } in a private browsing tab`
+ );
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ waitForTabURL: "about:privatebrowsing",
+ });
+ if (!uri) {
+ return { tab: win.gBrowser.selectedTab, uri: "about:privatebrowsing" };
+ }
+ initXulFrameLoaderListenerInfo();
+ win.gBrowser.addEventListener("XULFrameLoaderCreated", handleEvent);
+
+ const browser = win.gBrowser.selectedTab.linkedBrowser;
+ let prevRemoteType = browser.remoteType;
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, uri);
+ BrowserTestUtils.loadURIString(browser, uri);
+ await loaded;
+ let currRemoteType = browser.remoteType;
+
+ info(
+ `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedListenerInfo.numCalledSoFar} time(s) for ${uri} in private tab`
+ );
+
+ if (
+ SpecialPowers.Services.appinfo.sessionHistoryInParent &&
+ currRemoteType == prevRemoteType &&
+ uri == "about:blank"
+ ) {
+ // about:blank page gets flagged for being eligible to go into bfcache
+ // and thus we create a new XULFrameLoader for these pages
+ is(
+ xulFrameLoaderCreatedListenerInfo.numCalledSoFar,
+ 1,
+ "XULFrameLoaderCreated fired correct number of times"
+ );
+ } else {
+ is(
+ xulFrameLoaderCreatedListenerInfo.numCalledSoFar,
+ currRemoteType == prevRemoteType ? 0 : 1,
+ "XULFrameLoaderCreated fired correct number of times"
+ );
+ }
+
+ win.gBrowser.removeEventListener("XULFrameLoaderCreated", handleEvent);
+ return { tab: win.gBrowser.selectedTab, uri };
+}
+
+function initXulFrameLoaderCreatedCounter(aXulFrameLoaderCreatedListenerInfo) {
+ aXulFrameLoaderCreatedListenerInfo.numCalledSoFar = 0;
+}
+
+// Expected remote types for the following tests:
+// browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js
+// browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js
+function getExpectedRemoteTypes(gFissionBrowser, numPagesOpen) {
+ var remoteTypes;
+ if (gFissionBrowser) {
+ remoteTypes = [
+ "webIsolated=https://example.com",
+ "webIsolated=https://example.com^userContextId=1",
+ "webIsolated=https://example.com^userContextId=2",
+ "webIsolated=https://example.com^userContextId=3",
+ "webIsolated=https://example.com^privateBrowsingId=1",
+ "webIsolated=https://example.org",
+ "webIsolated=https://example.org^userContextId=1",
+ "webIsolated=https://example.org^userContextId=2",
+ "webIsolated=https://example.org^userContextId=3",
+ "webIsolated=https://example.org^privateBrowsingId=1",
+ ];
+ } else {
+ remoteTypes = Array(numPagesOpen * 2).fill("web"); // example.com and example.org
+ }
+ remoteTypes = remoteTypes.concat(Array(numPagesOpen * 2).fill(null)); // about: pages
+ return remoteTypes;
+}
diff --git a/browser/base/content/test/tabs/link_in_tab_title_and_url_prefilled.html b/browser/base/content/test/tabs/link_in_tab_title_and_url_prefilled.html
new file mode 100644
index 0000000000..a7561f4099
--- /dev/null
+++ b/browser/base/content/test/tabs/link_in_tab_title_and_url_prefilled.html
@@ -0,0 +1,30 @@
+<style>
+a { display: block; }
+</style>
+
+<a id="wait-a-bit--blank-target" href="wait-a-bit.sjs" target="_blank">wait-a-bit - _blank target</a>
+<a id="wait-a-bit--other-target" href="wait-a-bit.sjs" target="other">wait-a-bit - other target</a>
+<a id="wait-a-bit--by-script">wait-a-bit - script</a>
+<a id="wait-a-bit--no-target" href="wait-a-bit.sjs">wait-a-bit - no target</a>
+
+<a id="request-timeout--blank-target" href="request-timeout.sjs" target="_blank">request-timeout - _blank target</a>
+<a id="request-timeout--other-target" href="request-timeout.sjs" target="other">request-timeout - other target</a>
+<a id="request-timeout--by-script">request-timeout - script</a>
+<a id="request-timeout--no-target" href="request-timeout.sjs">request-timeout - no target</a>
+
+<a id="blank-page--blank-target" href="about:blank" target="_blank">about:blank - _blank target</a>
+<a id="blank-page--other-target" href="about:blank" target="other">about:blank - other target</a>
+<a id="blank-page--by-script">blank - script</a>
+<a id="blank-page--no-target" href="about:blank">about:blank - no target</a>
+
+<script>
+document.getElementById("wait-a-bit--by-script").addEventListener("click", () => {
+ window.open("wait-a-bit.sjs", "_blank");
+})
+document.getElementById("request-timeout--by-script").addEventListener("click", () => {
+ window.open("request-timeout.sjs", "_blank");
+})
+document.getElementById("blank-page--by-script").addEventListener("click", () => {
+ window.open("about:blank", "_blank");
+})
+</script>
diff --git a/browser/base/content/test/tabs/open_window_in_new_tab.html b/browser/base/content/test/tabs/open_window_in_new_tab.html
new file mode 100644
index 0000000000..2bd7613d26
--- /dev/null
+++ b/browser/base/content/test/tabs/open_window_in_new_tab.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script>
+function openWindow(id) {
+ window.childWindow = window.open(location.href + "?" + id, "", "");
+}
+</script>
+<button id="open-click" onclick="openWindow('open-click')">Open window</button>
+<button id="focus" onclick="window.childWindow.focus()">Focus window</button>
+<button id="open-mousedown">Open window</button>
+<script>
+document.getElementById("open-mousedown").addEventListener("mousedown", function(e) {
+ openWindow(this.id);
+ e.preventDefault();
+});
+</script>
diff --git a/browser/base/content/test/tabs/page_with_iframe.html b/browser/base/content/test/tabs/page_with_iframe.html
new file mode 100644
index 0000000000..5d821cf980
--- /dev/null
+++ b/browser/base/content/test/tabs/page_with_iframe.html
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+ <head>
+ <title>This page has an iFrame</title>
+ </head>
+ <body>
+ <iframe id="hidden-iframe" style="visibility: hidden;" src="https://example.com/another/site"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/redirect_via_header.html b/browser/base/content/test/tabs/redirect_via_header.html
new file mode 100644
index 0000000000..5fedca6b4e
--- /dev/null
+++ b/browser/base/content/test/tabs/redirect_via_header.html
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+ <head>
+ <title>Redirect via the header associated with this file</title>
+ </head>
+</html>
diff --git a/browser/base/content/test/tabs/redirect_via_header.html^headers^ b/browser/base/content/test/tabs/redirect_via_header.html^headers^
new file mode 100644
index 0000000000..7543b06689
--- /dev/null
+++ b/browser/base/content/test/tabs/redirect_via_header.html^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Found
+Location: https://example.com/some/path
diff --git a/browser/base/content/test/tabs/redirect_via_meta_tag.html b/browser/base/content/test/tabs/redirect_via_meta_tag.html
new file mode 100644
index 0000000000..42b775055f
--- /dev/null
+++ b/browser/base/content/test/tabs/redirect_via_meta_tag.html
@@ -0,0 +1,13 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+ <head>
+ <title>Page that redirects</title>
+ <meta http-equiv="refresh" content="1;url=http://mochi.test:8888/" />
+ </head>
+ <body>
+ <p>This page has moved to http://mochi.test:8888/</p>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/request-timeout.sjs b/browser/base/content/test/tabs/request-timeout.sjs
new file mode 100644
index 0000000000..00e95ca4c0
--- /dev/null
+++ b/browser/base/content/test/tabs/request-timeout.sjs
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function handleRequest(request, response) {
+ response.setStatusLine("1.1", 408, "Request Timeout");
+}
diff --git a/browser/base/content/test/tabs/tab_that_closes.html b/browser/base/content/test/tabs/tab_that_closes.html
new file mode 100644
index 0000000000..795baec18b
--- /dev/null
+++ b/browser/base/content/test/tabs/tab_that_closes.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <h1>This tab will close</h2>
+ <script>
+ // We use half a second timeout because this can race in debug builds.
+ setTimeout( () => {
+ window.close();
+ }, 500);
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/tabs/test_bug1358314.html b/browser/base/content/test/tabs/test_bug1358314.html
new file mode 100644
index 0000000000..9aa2019752
--- /dev/null
+++ b/browser/base/content/test/tabs/test_bug1358314.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>Test page</p>
+ <a href="/">Link</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/test_process_flags_chrome.html b/browser/base/content/test/tabs/test_process_flags_chrome.html
new file mode 100644
index 0000000000..c447d7ffb0
--- /dev/null
+++ b/browser/base/content/test/tabs/test_process_flags_chrome.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p>chrome: test page</p>
+<p><a href="chrome://mochitests/content/browser/browser/base/content/test/tabs/test_process_flags_chrome.html">chrome</a></p>
+<p><a href="chrome://mochitests-any/content/browser/browser/base/content/test/tabs/test_process_flags_chrome.html">canremote</a></p>
+<p><a href="chrome://mochitests-content/content/browser/browser/base/content/test/tabs/test_process_flags_chrome.html">mustremote</a></p>
+</body>
+</html>
diff --git a/browser/base/content/test/tabs/wait-a-bit.sjs b/browser/base/content/test/tabs/wait-a-bit.sjs
new file mode 100644
index 0000000000..e90133d752
--- /dev/null
+++ b/browser/base/content/test/tabs/wait-a-bit.sjs
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+async function handleRequest(request, response) {
+ response.seizePower();
+
+ await new Promise(r => setTimeout(r, 2000));
+
+ response.write("HTTP/1.1 200 OK\r\n");
+ const body = "<title>wait a bit</title><body>ok</body>";
+ response.write("Content-Type: text/html\r\n");
+ response.write(`Content-Length: ${body.length}\r\n`);
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
diff --git a/browser/base/content/test/touch/browser.ini b/browser/base/content/test/touch/browser.ini
new file mode 100644
index 0000000000..7b14c74211
--- /dev/null
+++ b/browser/base/content/test/touch/browser.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[browser_menu_touch.js]
+skip-if = !(os == 'win' && os_version == '10.0')
diff --git a/browser/base/content/test/touch/browser_menu_touch.js b/browser/base/content/test/touch/browser_menu_touch.js
new file mode 100644
index 0000000000..0cb605675c
--- /dev/null
+++ b/browser/base/content/test/touch/browser_menu_touch.js
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 that toolbar menus are in touchmode
+ * when opened through a touch event. */
+
+async function openAndCheckMenu(menu, target) {
+ is(menu.state, "closed", `Menu panel (${menu.id}) is initally closed.`);
+
+ let popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeNativeTapAtCenter(target);
+ await popupshown;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ is(
+ menu.getAttribute("touchmode"),
+ "true",
+ `Menu panel (${menu.id}) is in touchmode.`
+ );
+
+ menu.hidePopup();
+
+ popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(target, {});
+ await popupshown;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ ok(
+ !menu.hasAttribute("touchmode"),
+ `Menu panel (${menu.id}) is not in touchmode.`
+ );
+
+ menu.hidePopup();
+}
+
+async function openAndCheckLazyMenu(id, target) {
+ let menu = document.getElementById(id);
+
+ EventUtils.synthesizeNativeTapAtCenter(target);
+ let ev = await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ e => e.target.id == id
+ );
+ menu = ev.target;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ is(
+ menu.getAttribute("touchmode"),
+ "true",
+ `Menu panel (${menu.id}) is in touchmode.`
+ );
+
+ menu.hidePopup();
+
+ let popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(target, {});
+ await popupshown;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ ok(
+ !menu.hasAttribute("touchmode"),
+ `Menu panel (${menu.id}) is not in touchmode.`
+ );
+
+ menu.hidePopup();
+}
+
+// The customization UI menu is not attached to the document when it is
+// closed and hence requires special attention.
+async function openAndCheckCustomizationUIMenu(target) {
+ EventUtils.synthesizeNativeTapAtCenter(target);
+
+ await BrowserTestUtils.waitForCondition(
+ () => document.getElementById("customizationui-widget-panel") != null
+ );
+ let menu = document.getElementById("customizationui-widget-panel");
+
+ if (menu.state != "open") {
+ await BrowserTestUtils.waitForEvent(menu, "popupshown");
+ is(menu.state, "open", `Menu for ${target.id} is open`);
+ }
+
+ is(
+ menu.getAttribute("touchmode"),
+ "true",
+ `Menu for ${target.id} is in touchmode.`
+ );
+
+ menu.hidePopup();
+
+ EventUtils.synthesizeMouseAtCenter(target, {});
+
+ await BrowserTestUtils.waitForCondition(
+ () => document.getElementById("customizationui-widget-panel") != null
+ );
+ menu = document.getElementById("customizationui-widget-panel");
+
+ if (menu.state != "open") {
+ await BrowserTestUtils.waitForEvent(menu, "popupshown");
+ is(menu.state, "open", `Menu for ${target.id} is open`);
+ }
+
+ ok(
+ !menu.hasAttribute("touchmode"),
+ `Menu for ${target.id} is not in touchmode.`
+ );
+
+ menu.hidePopup();
+}
+
+// Ensure that we can run touch events properly for windows [10]
+add_setup(async function () {
+ let isWindows = AppConstants.isPlatformAndVersionAtLeast("win", "10.0");
+ await SpecialPowers.pushPrefEnv({
+ set: [["apz.test.fails_with_native_injection", isWindows]],
+ });
+});
+
+// Test main ("hamburger") menu.
+add_task(async function test_main_menu_touch() {
+ let mainMenu = document.getElementById("appMenu-popup");
+ let target = document.getElementById("PanelUI-menu-button");
+ await openAndCheckMenu(mainMenu, target);
+});
+
+// Test the page action menu.
+add_task(async function test_page_action_panel_touch() {
+ // The page action menu only appears on a web page.
+ await BrowserTestUtils.withNewTab("https://example.com", async function () {
+ // The page actions button is not normally visible, so we must
+ // unhide it.
+ BrowserPageActions.mainButtonNode.style.visibility = "visible";
+ registerCleanupFunction(() => {
+ BrowserPageActions.mainButtonNode.style.removeProperty("visibility");
+ });
+ let target = document.getElementById("pageActionButton");
+ await openAndCheckLazyMenu("pageActionPanel", target);
+ });
+});
+
+// Test the customizationUI panel, which is used for various menus
+// such as library, history, sync, developer and encoding.
+add_task(async function test_customizationui_panel_touch() {
+ CustomizableUI.addWidgetToArea("library-button", CustomizableUI.AREA_NAVBAR);
+ CustomizableUI.addWidgetToArea(
+ "history-panelmenu",
+ CustomizableUI.AREA_NAVBAR
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ CustomizableUI.getPlacementOfWidget("library-button").area == "nav-bar"
+ );
+
+ let target = document.getElementById("library-button");
+ await openAndCheckCustomizationUIMenu(target);
+
+ target = document.getElementById("history-panelmenu");
+ await openAndCheckCustomizationUIMenu(target);
+
+ CustomizableUI.reset();
+});
+
+// Test the overflow menu panel.
+add_task(async function test_overflow_panel_touch() {
+ // Move something in the overflow menu to make the button appear.
+ CustomizableUI.addWidgetToArea(
+ "library-button",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ CustomizableUI.getPlacementOfWidget("library-button").area ==
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+
+ let overflowPanel = document.getElementById("widget-overflow");
+ let target = document.getElementById("nav-bar-overflow-button");
+ await openAndCheckMenu(overflowPanel, target);
+
+ CustomizableUI.reset();
+});
+
+// Test the list all tabs menu.
+add_task(async function test_list_all_tabs_touch() {
+ // Force the menu button to be shown.
+ let tabs = document.getElementById("tabbrowser-tabs");
+ if (!tabs.hasAttribute("overflow")) {
+ tabs.setAttribute("overflow", true);
+ registerCleanupFunction(() => tabs.removeAttribute("overflow"));
+ }
+
+ let target = document.getElementById("alltabs-button");
+ await openAndCheckCustomizationUIMenu(target);
+});
diff --git a/browser/base/content/test/utilityOverlay/browser.ini b/browser/base/content/test/utilityOverlay/browser.ini
new file mode 100644
index 0000000000..236f7a6f97
--- /dev/null
+++ b/browser/base/content/test/utilityOverlay/browser.ini
@@ -0,0 +1,2 @@
+[DEFAULT]
+[browser_openWebLinkIn.js]
diff --git a/browser/base/content/test/utilityOverlay/browser_openWebLinkIn.js b/browser/base/content/test/utilityOverlay/browser_openWebLinkIn.js
new file mode 100644
index 0000000000..53f11e49c0
--- /dev/null
+++ b/browser/base/content/test/utilityOverlay/browser_openWebLinkIn.js
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Stolen from https://searchfox.org/mozilla-central/source/browser/base/content/test/popups/browser_popup_close_main_window.js
+// When calling this function, the main window where the test runs will be
+// hidden from various APIs, so that they won't be able to find it. This makes
+// it possible to test some behaviors when only private windows are present.
+function concealMainWindow() {
+ info("Concealing main window.");
+ let oldWinType = document.documentElement.getAttribute("windowtype");
+ // Check if we've already done this to allow calling multiple times:
+ if (oldWinType != "navigator:testrunner") {
+ // Make the main test window not count as a browser window any longer
+ document.documentElement.setAttribute("windowtype", "navigator:testrunner");
+ BrowserWindowTracker.untrackForTestsOnly(window);
+
+ registerCleanupFunction(() => {
+ info("Unconcealing the main window in the cleanup phase.");
+ BrowserWindowTracker.track(window);
+ document.documentElement.setAttribute("windowtype", oldWinType);
+ });
+ }
+}
+
+const EXAMPLE_URL = "https://example.org/";
+add_task(async function test_open_tab() {
+ const waitForTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ EXAMPLE_URL
+ );
+ const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>
+ openWebLinkIn(EXAMPLE_URL, "tab", {
+ resolveOnContentBrowserCreated,
+ })
+ );
+
+ const tab = await waitForTabPromise;
+ is(
+ contentBrowser,
+ tab.linkedBrowser,
+ "We get a content browser that is the tab's linked browser as the result of opening a tab"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_open_window() {
+ const waitForWindowPromise = BrowserTestUtils.waitForNewWindow();
+ const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>
+ openWebLinkIn(EXAMPLE_URL, "window", {
+ resolveOnContentBrowserCreated,
+ })
+ );
+
+ const win = await waitForWindowPromise;
+ is(
+ contentBrowser,
+ win.gBrowser.selectedBrowser,
+ "We get the content browser for the newly opened window as a result of opening a window"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_open_private_window() {
+ const waitForWindowPromise = BrowserTestUtils.waitForNewWindow();
+ const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>
+ openWebLinkIn(EXAMPLE_URL, "window", {
+ resolveOnContentBrowserCreated,
+ private: true,
+ })
+ );
+
+ const win = await waitForWindowPromise;
+ ok(
+ PrivateBrowsingUtils.isBrowserPrivate(win),
+ "The new window is a private window."
+ );
+ is(
+ contentBrowser,
+ win.gBrowser.selectedBrowser,
+ "We get the content browser for the newly opened window as a result of opening a private window"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_open_private_tab_from_private_window() {
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ const waitForTabPromise = BrowserTestUtils.waitForNewTab(
+ privateWindow.gBrowser,
+ EXAMPLE_URL
+ );
+ const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>
+ privateWindow.openWebLinkIn(EXAMPLE_URL, "tab", {
+ resolveOnContentBrowserCreated,
+ })
+ );
+
+ const tab = await waitForTabPromise;
+ ok(
+ PrivateBrowsingUtils.isBrowserPrivate(tab),
+ "The new tab was opened in a private browser."
+ );
+ is(
+ contentBrowser,
+ tab.linkedBrowser,
+ "We get a content browser that is the tab's linked browser as the result of opening a private tab in a private window"
+ );
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
+
+add_task(async function test_open_non_private_tab_from_private_window() {
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Opening this tab from the private window should open it in the non-private window.
+ const waitForTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ EXAMPLE_URL
+ );
+
+ const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>
+ privateWindow.openWebLinkIn(EXAMPLE_URL, "tab", {
+ forceNonPrivate: true,
+ resolveOnContentBrowserCreated,
+ })
+ );
+
+ const nonPrivateTab = await waitForTabPromise;
+ ok(
+ !PrivateBrowsingUtils.isBrowserPrivate(nonPrivateTab),
+ "The new window isn't a private window."
+ );
+ is(
+ contentBrowser,
+ nonPrivateTab.linkedBrowser,
+ "We get a content browser that is the non private tab's linked browser as the result of opening a non private tab from a private window"
+ );
+
+ BrowserTestUtils.removeTab(nonPrivateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
+
+add_task(async function test_open_non_private_tab_from_only_private_window() {
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // In this test we'll hide the existing window from window trackers, because
+ // we want to test that we open a new window when there's only a private
+ // window.
+ concealMainWindow();
+
+ // Opening this tab from the private window should open it in a new non-private window.
+ const waitForWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: EXAMPLE_URL,
+ });
+ const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>
+ privateWindow.openWebLinkIn(EXAMPLE_URL, "tab", {
+ forceNonPrivate: true,
+ resolveOnContentBrowserCreated,
+ })
+ );
+
+ const nonPrivateWindow = await waitForWindowPromise;
+ ok(
+ !PrivateBrowsingUtils.isBrowserPrivate(nonPrivateWindow),
+ "The new window isn't a private window."
+ );
+ is(
+ contentBrowser,
+ nonPrivateWindow.gBrowser.selectedBrowser,
+ "We get the content browser for the newly opened non private window from a private window, as a result of opening a non private tab."
+ );
+
+ await BrowserTestUtils.closeWindow(nonPrivateWindow);
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
diff --git a/browser/base/content/test/webextensions/.eslintrc.js b/browser/base/content/test/webextensions/.eslintrc.js
new file mode 100644
index 0000000000..e57058ecb1
--- /dev/null
+++ b/browser/base/content/test/webextensions/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+};
diff --git a/browser/base/content/test/webextensions/browser.ini b/browser/base/content/test/webextensions/browser.ini
new file mode 100644
index 0000000000..d90da6c891
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files =
+ head.js
+ file_install_extensions.html
+ browser_legacy_webext.xpi
+ browser_webext_permissions.xpi
+ browser_webext_nopermissions.xpi
+ browser_webext_unsigned.xpi
+ browser_webext_update1.xpi
+ browser_webext_update2.xpi
+ browser_webext_update_icon1.xpi
+ browser_webext_update_icon2.xpi
+ browser_webext_update_perms1.xpi
+ browser_webext_update_perms2.xpi
+ browser_webext_update_origins1.xpi
+ browser_webext_update_origins2.xpi
+ browser_webext_update.json
+
+[browser_aboutaddons_blanktab.js]
+[browser_extension_sideloading.js]
+[browser_extension_update_background.js]
+[browser_extension_update_background_noprompt.js]
+[browser_permissions_dismiss.js]
+[browser_permissions_installTrigger.js]
+[browser_permissions_local_file.js]
+[browser_permissions_mozAddonManager.js]
+[browser_permissions_optional.js]
+[browser_permissions_pointerevent.js]
+[browser_permissions_unsigned.js]
+skip-if = require_signing
+[browser_update_checkForUpdates.js]
+skip-if = os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_update_interactive_noprompt.js]
diff --git a/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js b/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js
new file mode 100644
index 0000000000..228fe71815
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js
@@ -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/. */
+
+add_task(async function testBlankTabReusedAboutAddons() {
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ let tabCount = gBrowser.tabs.length;
+ is(browser, gBrowser.selectedBrowser, "New tab is selected");
+
+ // Opening about:addons shouldn't change the selected tab.
+ BrowserOpenAddonsMgr();
+
+ is(browser, gBrowser.selectedBrowser, "No new tab was opened");
+
+ // Wait for about:addons to load.
+ await BrowserTestUtils.browserLoaded(browser);
+
+ is(
+ browser.currentURI.spec,
+ "about:addons",
+ "about:addons should load into blank tab."
+ );
+
+ is(gBrowser.tabs.length, tabCount, "Still the same number of tabs");
+ });
+});
diff --git a/browser/base/content/test/webextensions/browser_extension_sideloading.js b/browser/base/content/test/webextensions/browser_extension_sideloading.js
new file mode 100644
index 0000000000..f1a7dca436
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js
@@ -0,0 +1,404 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const { AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+AddonTestUtils.hookAMTelemetryEvents();
+
+const kSideloaded = true;
+
+async function createWebExtension(details) {
+ let options = {
+ manifest: {
+ browser_specific_settings: { gecko: { id: details.id } },
+
+ name: details.name,
+
+ permissions: details.permissions,
+ },
+ };
+
+ if (details.iconURL) {
+ options.manifest.icons = { 64: details.iconURL };
+ }
+
+ let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+
+ await AddonTestUtils.manuallyInstall(xpi);
+}
+
+function promiseEvent(eventEmitter, event) {
+ return new Promise(resolve => {
+ eventEmitter.once(event, resolve);
+ });
+}
+
+function getAddonElement(managerWindow, addonId) {
+ return TestUtils.waitForCondition(
+ () =>
+ managerWindow.document.querySelector(`addon-card[addon-id="${addonId}"]`),
+ `Found entry for sideload extension addon "${addonId}" in HTML about:addons`
+ );
+}
+
+function assertSideloadedAddonElementState(addonElement, pressed) {
+ const enableBtn = addonElement.querySelector('[action="toggle-disabled"]');
+ is(
+ enableBtn.pressed,
+ pressed,
+ `The enable button is ${!pressed ? " not " : ""} pressed`
+ );
+ is(enableBtn.localName, "moz-toggle", "The enable button is a toggle");
+}
+
+function clickEnableExtension(addonElement) {
+ addonElement.querySelector('[action="toggle-disabled"]').click();
+}
+
+add_task(async function test_sideloading() {
+ const DEFAULT_ICON_URL =
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["xpinstall.signatures.required", false],
+ ["extensions.autoDisableScopes", 15],
+ ["extensions.ui.ignoreUnsigned", true],
+ ],
+ });
+
+ const ID1 = "addon1@tests.mozilla.org";
+ await createWebExtension({
+ id: ID1,
+ name: "Test 1",
+ userDisabled: true,
+ permissions: ["history", "https://*/*"],
+ iconURL: "foo-icon.png",
+ });
+
+ const ID2 = "addon2@tests.mozilla.org";
+ await createWebExtension({
+ id: ID2,
+ name: "Test 2",
+ permissions: ["<all_urls>"],
+ });
+
+ const ID3 = "addon3@tests.mozilla.org";
+ await createWebExtension({
+ id: ID3,
+ name: "Test 3",
+ permissions: ["<all_urls>"],
+ });
+
+ testCleanup = async function () {
+ // clear out ExtensionsUI state about sideloaded extensions so
+ // subsequent tests don't get confused.
+ ExtensionsUI.sideloaded.clear();
+ ExtensionsUI.emit("change");
+ };
+
+ // Navigate away from the starting page to force about:addons to load
+ // in a new tab during the tests below.
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function () {
+ // Return to about:blank when we're done
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ });
+
+ let changePromise = new Promise(resolve => {
+ ExtensionsUI.on("change", function listener() {
+ ExtensionsUI.off("change", listener);
+ resolve();
+ });
+ });
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // Check for the addons badge on the hamburger menu
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ is(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "Should have addon alert badge"
+ );
+
+ // Find the menu entries for sideloaded extensions
+ await gCUITestUtils.openMainMenu();
+
+ let addons = PanelUI.addonNotificationContainer;
+ is(
+ addons.children.length,
+ 3,
+ "Have 3 menu entries for sideloaded extensions"
+ );
+
+ info(
+ "Test disabling sideloaded addon 1 using the permission prompt secondary button"
+ );
+
+ // Click the first sideloaded extension
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addons.children[0].click();
+
+ // The click should hide the main menu. This is currently synchronous.
+ ok(PanelUI.panel.state != "open", "Main menu is closed or closing.");
+
+ // When we get the permissions prompt, we should be at the extensions
+ // list in about:addons
+ let panel = await popupPromise;
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "Foreground tab is at about:addons"
+ );
+
+ const VIEW = "addons://list/extension";
+ let win = gBrowser.selectedBrowser.contentWindow;
+
+ await TestUtils.waitForCondition(
+ () => !win.gViewController.isLoading,
+ "about:addons view is fully loaded"
+ );
+ is(
+ win.gViewController.currentViewId,
+ VIEW,
+ "about:addons is at extensions list"
+ );
+
+ // Check the contents of the notification, then choose "Cancel"
+ checkNotification(
+ panel,
+ /\/foo-icon\.png$/,
+ [
+ ["webext-perms-host-description-all-urls"],
+ ["webext-perms-description-history"],
+ ],
+ kSideloaded
+ );
+
+ panel.secondaryButton.click();
+
+ let [addon1, addon2, addon3] = await AddonManager.getAddonsByIDs([
+ ID1,
+ ID2,
+ ID3,
+ ]);
+ ok(addon1.seen, "Addon should be marked as seen");
+ is(addon1.userDisabled, true, "Addon 1 should still be disabled");
+ is(addon2.userDisabled, true, "Addon 2 should still be disabled");
+ is(addon3.userDisabled, true, "Addon 3 should still be disabled");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Should still have 2 entries in the hamburger menu
+ await gCUITestUtils.openMainMenu();
+
+ addons = PanelUI.addonNotificationContainer;
+ is(
+ addons.children.length,
+ 2,
+ "Have 2 menu entries for sideloaded extensions"
+ );
+
+ // Close the hamburger menu and go directly to the addons manager
+ await gCUITestUtils.hideMainMenu();
+
+ win = await BrowserOpenAddonsMgr(VIEW);
+ await waitAboutAddonsViewLoaded(win.document);
+
+ // about:addons addon entry element.
+ const addonElement = await getAddonElement(win, ID2);
+
+ assertSideloadedAddonElementState(addonElement, false);
+
+ info("Test enabling sideloaded addon 2 from about:addons enable button");
+
+ // When clicking enable we should see the permissions notification
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ clickEnableExtension(addonElement);
+ panel = await popupPromise;
+ checkNotification(
+ panel,
+ DEFAULT_ICON_URL,
+ [["webext-perms-host-description-all-urls"]],
+ kSideloaded
+ );
+
+ // Test incognito checkbox in post install notification
+ function setupPostInstallNotificationTest() {
+ let promiseNotificationShown =
+ promiseAppMenuNotificationShown("addon-installed");
+ return async function (addon) {
+ info(`Expect post install notification for "${addon.name}"`);
+ let postInstallPanel = await promiseNotificationShown;
+ let incognitoCheckbox = postInstallPanel.querySelector(
+ "#addon-incognito-checkbox"
+ );
+ is(
+ window.AppMenuNotifications.activeNotification.options.name,
+ addon.name,
+ "Got the expected addon name in the active notification"
+ );
+ ok(
+ incognitoCheckbox,
+ "Got an incognito checkbox in the post install notification panel"
+ );
+ ok(!incognitoCheckbox.hidden, "Incognito checkbox should not be hidden");
+ // Dismiss post install notification.
+ postInstallPanel.button.click();
+ };
+ }
+
+ // Setup async test for post install notification on addon 2
+ let testPostInstallIncognitoCheckbox = setupPostInstallNotificationTest();
+
+ // Accept the permissions
+ panel.button.click();
+ await promiseEvent(ExtensionsUI, "change");
+
+ addon2 = await AddonManager.getAddonByID(ID2);
+ is(addon2.userDisabled, false, "Addon 2 should be enabled");
+ assertSideloadedAddonElementState(addonElement, true);
+
+ // Test post install notification on addon 2.
+ await testPostInstallIncognitoCheckbox(addon2);
+
+ // Should still have 1 entry in the hamburger menu
+ await gCUITestUtils.openMainMenu();
+
+ addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions");
+
+ // Close the hamburger menu and go to the detail page for this addon
+ await gCUITestUtils.hideMainMenu();
+
+ win = await BrowserOpenAddonsMgr(
+ `addons://detail/${encodeURIComponent(ID3)}`
+ );
+
+ info("Test enabling sideloaded addon 3 from app menu");
+ // Trigger addon 3 install as triggered from the app menu, to be able to cover the
+ // post install notification that should be triggered when the permission
+ // dialog is accepted from that flow.
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ ExtensionsUI.showSideloaded(gBrowser, addon3);
+
+ panel = await popupPromise;
+ checkNotification(
+ panel,
+ DEFAULT_ICON_URL,
+ [["webext-perms-host-description-all-urls"]],
+ kSideloaded
+ );
+
+ // Setup async test for post install notification on addon 3
+ testPostInstallIncognitoCheckbox = setupPostInstallNotificationTest();
+
+ // Accept the permissions
+ panel.button.click();
+ await promiseEvent(ExtensionsUI, "change");
+
+ addon3 = await AddonManager.getAddonByID(ID3);
+ is(addon3.userDisabled, false, "Addon 3 should be enabled");
+
+ // Test post install notification on addon 3.
+ await testPostInstallIncognitoCheckbox(addon3);
+
+ isnot(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "Should no longer have addon alert badge"
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ for (let addon of [addon1, addon2, addon3]) {
+ await addon.uninstall();
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Assert that the expected AddonManager telemetry are being recorded.
+ const expectedExtra = { source: "app-profile", method: "sideload" };
+
+ const baseEvent = { object: "extension", extra: expectedExtra };
+ const createBaseEventAddon = n => ({
+ ...baseEvent,
+ value: `addon${n}@tests.mozilla.org`,
+ });
+ const getEventsForAddonId = (events, addonId) =>
+ events.filter(ev => ev.value === addonId);
+
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+
+ // Test telemetry events for addon1 (1 permission and 1 origin).
+ info("Test telemetry events collected for addon1");
+
+ const baseEventAddon1 = createBaseEventAddon(1);
+ const collectedEventsAddon1 = getEventsForAddonId(
+ amEvents,
+ baseEventAddon1.value
+ );
+ const expectedEventsAddon1 = [
+ {
+ ...baseEventAddon1,
+ method: "sideload_prompt",
+ extra: { ...expectedExtra, num_strings: "2" },
+ },
+ { ...baseEventAddon1, method: "uninstall" },
+ ];
+
+ let i = 0;
+ for (let event of collectedEventsAddon1) {
+ Assert.deepEqual(
+ event,
+ expectedEventsAddon1[i++],
+ "Got the expected telemetry event"
+ );
+ }
+
+ is(
+ collectedEventsAddon1.length,
+ expectedEventsAddon1.length,
+ "Got the expected number of telemetry events for addon1"
+ );
+
+ const baseEventAddon2 = createBaseEventAddon(2);
+ const collectedEventsAddon2 = getEventsForAddonId(
+ amEvents,
+ baseEventAddon2.value
+ );
+ const expectedEventsAddon2 = [
+ {
+ ...baseEventAddon2,
+ method: "sideload_prompt",
+ extra: { ...expectedExtra, num_strings: "1" },
+ },
+ { ...baseEventAddon2, method: "enable" },
+ { ...baseEventAddon2, method: "uninstall" },
+ ];
+
+ i = 0;
+ for (let event of collectedEventsAddon2) {
+ Assert.deepEqual(
+ event,
+ expectedEventsAddon2[i++],
+ "Got the expected telemetry event"
+ );
+ }
+
+ is(
+ collectedEventsAddon2.length,
+ expectedEventsAddon2.length,
+ "Got the expected number of telemetry events for addon2"
+ );
+});
diff --git a/browser/base/content/test/webextensions/browser_extension_update_background.js b/browser/base/content/test/webextensions/browser_extension_update_background.js
new file mode 100644
index 0000000000..b0a4a31439
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_update_background.js
@@ -0,0 +1,282 @@
+const { AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+AddonTestUtils.hookAMTelemetryEvents();
+
+const ID = "update2@tests.mozilla.org";
+const ID_ICON = "update_icon2@tests.mozilla.org";
+const ID_PERMS = "update_perms@tests.mozilla.org";
+const ID_LEGACY = "legacy_update@tests.mozilla.org";
+const FAKE_INSTALL_TELEMETRY_SOURCE = "fake-install-source";
+
+requestLongerTimeout(2);
+
+function promiseViewLoaded(tab, viewid) {
+ let win = tab.linkedBrowser.contentWindow;
+ if (
+ win.gViewController &&
+ !win.gViewController.isLoading &&
+ win.gViewController.currentViewId == viewid
+ ) {
+ return Promise.resolve();
+ }
+
+ return waitAboutAddonsViewLoaded(win.document);
+}
+
+function getBadgeStatus() {
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ return menuButton.getAttribute("badge-status");
+}
+
+// Set some prefs that apply to all the tests in this file
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+ ],
+ });
+
+ // Navigate away from the initial page so that about:addons always
+ // opens in a new tab during tests
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function () {
+ // Return to about:blank when we're done
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ });
+});
+
+// Helper function to test background updates.
+async function backgroundUpdateTest(url, id, checkIconFn) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Turn on background updates
+ ["extensions.update.enabled", true],
+
+ // Point updates to the local mochitest server
+ [
+ "extensions.update.background.url",
+ `${BASE}/browser_webext_update.json`,
+ ],
+ ],
+ });
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(url, {
+ source: FAKE_INSTALL_TELEMETRY_SOURCE,
+ });
+ let addonId = addon.id;
+
+ ok(addon, "Addon was installed");
+ is(getBadgeStatus(), "", "Should not start out with an addon alert badge");
+
+ // Trigger an update check and wait for the update for this addon
+ // to be downloaded.
+ let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+ await updatePromise;
+
+ is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
+
+ // Find the menu entry for the update
+ await gCUITestUtils.openMainMenu();
+
+ let addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 1, "Have a menu entry for the update");
+
+ // Click the menu item
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addons.children[0].click();
+
+ // The click should hide the main menu. This is currently synchronous.
+ ok(PanelUI.panel.state != "open", "Main menu is closed or closing.");
+
+ // about:addons should load and go to the list of extensions
+ let tab = await tabPromise;
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ "about:addons",
+ "Browser is at about:addons"
+ );
+
+ const VIEW = "addons://list/extension";
+ await promiseViewLoaded(tab, VIEW);
+ let win = tab.linkedBrowser.contentWindow;
+ ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
+ is(
+ win.gViewController.currentViewId,
+ VIEW,
+ "about:addons is at extensions list"
+ );
+
+ // Wait for the permission prompt, check the contents
+ let panel = await popupPromise;
+ checkIconFn(panel.getAttribute("icon"));
+
+ // The original extension has 1 promptable permission and the new one
+ // has 2 (history and <all_urls>) plus 1 non-promptable permission (cookies).
+ // So we should only see the 1 new promptable permission in the notification.
+ let singlePermissionEl = document.getElementById(
+ "addon-webext-perm-single-entry"
+ );
+ ok(!singlePermissionEl.hidden, "Single permission entry is not hidden");
+ ok(singlePermissionEl.textContent, "Single permission entry text is set");
+
+ // Cancel the update.
+ panel.secondaryButton.click();
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "1.0", "Should still be running the old version");
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Alert badge and hamburger menu items should be gone
+ is(getBadgeStatus(), "", "Addon alert badge should be gone");
+
+ await gCUITestUtils.openMainMenu();
+ addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 0, "Update menu entries should be gone");
+ await gCUITestUtils.hideMainMenu();
+
+ // Re-check for an update
+ updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+ await AddonManagerPrivate.backgroundUpdateCheck();
+ await updatePromise;
+
+ is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
+
+ // Find the menu entry for the update
+ await gCUITestUtils.openMainMenu();
+
+ addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 1, "Have a menu entry for the update");
+
+ // Click the menu item
+ tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons", true);
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+
+ addons.children[0].click();
+
+ // Wait for about:addons to load
+ tab = await tabPromise;
+ is(tab.linkedBrowser.currentURI.spec, "about:addons");
+
+ await promiseViewLoaded(tab, VIEW);
+ win = tab.linkedBrowser.contentWindow;
+ ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
+ is(
+ win.gViewController.currentViewId,
+ VIEW,
+ "about:addons is at extensions list"
+ );
+
+ // Wait for the permission prompt and accept it this time
+ updatePromise = waitForUpdate(addon);
+ panel = await popupPromise;
+ panel.button.click();
+
+ addon = await updatePromise;
+ is(addon.version, "2.0", "Should have upgraded to the new version");
+
+ BrowserTestUtils.removeTab(tab);
+
+ is(getBadgeStatus(), "", "Addon alert badge should be gone");
+
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ // Test that the expected telemetry events have been recorded (and that they include the
+ // permission_prompt event).
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+ const updateEvents = amEvents
+ .filter(evt => evt.method === "update")
+ .map(evt => {
+ delete evt.value;
+ return evt;
+ });
+
+ Assert.deepEqual(
+ updateEvents.map(evt => evt.extra && evt.extra.step),
+ [
+ // First update (cancelled).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "cancelled",
+ // Second update (completed).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "completed",
+ ],
+ "Got the steps from the collected telemetry events"
+ );
+
+ const method = "update";
+ const object = "extension";
+ const baseExtra = {
+ addon_id: addonId,
+ source: FAKE_INSTALL_TELEMETRY_SOURCE,
+ step: "permissions_prompt",
+ updated_from: "app",
+ };
+
+ // Expect the telemetry events to have num_strings set to 1, as only the origin permissions is going
+ // to be listed in the permission prompt.
+ Assert.deepEqual(
+ updateEvents.filter(
+ evt => evt.extra && evt.extra.step === "permissions_prompt"
+ ),
+ [
+ { method, object, extra: { ...baseExtra, num_strings: "1" } },
+ { method, object, extra: { ...baseExtra, num_strings: "1" } },
+ ],
+ "Got the expected permission_prompts events"
+ );
+}
+
+function checkDefaultIcon(icon) {
+ is(
+ icon,
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg",
+ "Popup has the default extension icon"
+ );
+}
+
+add_task(() =>
+ backgroundUpdateTest(
+ `${BASE}/browser_webext_update1.xpi`,
+ ID,
+ checkDefaultIcon
+ )
+);
+function checkNonDefaultIcon(icon) {
+ // The icon should come from the extension, don't bother with the precise
+ // path, just make sure we've got a jar url pointing to the right path
+ // inside the jar.
+ ok(icon.startsWith("jar:file://"), "Icon is a jar url");
+ ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
+}
+
+add_task(() =>
+ backgroundUpdateTest(
+ `${BASE}/browser_webext_update_icon1.xpi`,
+ ID_ICON,
+ checkNonDefaultIcon
+ )
+);
diff --git a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
new file mode 100644
index 0000000000..81f13302bf
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
@@ -0,0 +1,121 @@
+const { AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+AddonTestUtils.hookAMTelemetryEvents();
+
+const ID_PERMS = "update_perms@tests.mozilla.org";
+const ID_ORIGINS = "update_origins@tests.mozilla.org";
+
+function getBadgeStatus() {
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ return menuButton.getAttribute("badge-status");
+}
+
+// Set some prefs that apply to all the tests in this file
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+ // Don't require the extensions to be signed
+ ["xpinstall.signatures.required", false],
+ ],
+ });
+
+ // Navigate away from the initial page so that about:addons always
+ // opens in a new tab during tests
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function () {
+ // Return to about:blank when we're done
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ });
+});
+
+// Helper function to test an upgrade that should not show a prompt
+async function testNoPrompt(origUrl, id) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Turn on background updates
+ ["extensions.update.enabled", true],
+
+ // Point updates to the local mochitest server
+ [
+ "extensions.update.background.url",
+ `${BASE}/browser_webext_update.json`,
+ ],
+ ],
+ });
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(origUrl);
+
+ ok(addon, "Addon was installed");
+
+ let sawPopup = false;
+ PopupNotifications.panel.addEventListener(
+ "popupshown",
+ () => (sawPopup = true),
+ { once: true }
+ );
+
+ // Trigger an update check and wait for the update to be applied.
+ let updatePromise = waitForUpdate(addon);
+ AddonManagerPrivate.backgroundUpdateCheck();
+ await updatePromise;
+
+ // There should be no notifications about the update
+ is(getBadgeStatus(), "", "Should not have addon alert badge");
+
+ await gCUITestUtils.openMainMenu();
+ let addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 0, "Have 0 updates in the PanelUI menu");
+ await gCUITestUtils.hideMainMenu();
+
+ ok(!sawPopup, "Should not have seen permissions notification");
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "2.0", "Update should have applied");
+
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ // Test that the expected telemetry events have been recorded (and that they do not
+ // include the permission_prompt event).
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+ const updateEventsSteps = amEvents
+ .filter(evt => {
+ return evt.method === "update" && evt.extra && evt.extra.addon_id == id;
+ })
+ .map(evt => {
+ return evt.extra.step;
+ });
+
+ // Expect telemetry events related to a completed update with no permissions_prompt event.
+ Assert.deepEqual(
+ updateEventsSteps,
+ ["started", "download_started", "download_completed", "completed"],
+ "Got the steps from the collected telemetry events"
+ );
+}
+
+// Test that an update that adds new non-promptable permissions is just
+// applied without showing a notification dialog.
+add_task(() =>
+ testNoPrompt(`${BASE}/browser_webext_update_perms1.xpi`, ID_PERMS)
+);
+
+// Test that an update that narrows origin permissions is just applied without
+// showing a notification promt
+add_task(() =>
+ testNoPrompt(`${BASE}/browser_webext_update_origins1.xpi`, ID_ORIGINS)
+);
diff --git a/browser/base/content/test/webextensions/browser_legacy_webext.xpi b/browser/base/content/test/webextensions/browser_legacy_webext.xpi
new file mode 100644
index 0000000000..a3bdf6f832
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_legacy_webext.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_permissions_dismiss.js b/browser/base/content/test/webextensions/browser_permissions_dismiss.js
new file mode 100644
index 0000000000..11c12389cc
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_dismiss.js
@@ -0,0 +1,112 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+const INSTALL_XPI = `${BASE}/browser_webext_permissions.xpi`;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function test_tab_switch_dismiss() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, INSTALL_PAGE);
+
+ let installCanceled = new Promise(resolve => {
+ let listener = {
+ onInstallCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [INSTALL_XPI], function (url) {
+ content.wrappedJSObject.installMozAM(url);
+ });
+
+ await promisePopupNotificationShown("addon-webext-permissions");
+ let permsUL = document.getElementById("addon-webext-perm-list");
+ is(permsUL.childElementCount, 5, `Permissions list has 5 entries`);
+
+ let permsLearnMore = document.getElementById("addon-webext-perm-info");
+ ok(
+ BrowserTestUtils.is_visible(permsLearnMore),
+ "Learn more link is shown on Permission popup"
+ );
+ is(
+ permsLearnMore.href,
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "extension-permissions",
+ "Learn more link has desired URL"
+ );
+
+ // Switching tabs dismisses the notification and cancels the install.
+ let switchTo = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ BrowserTestUtils.removeTab(switchTo);
+ await installCanceled;
+
+ let addon = await AddonManager.getAddonByID("permissions@test.mozilla.org");
+ is(addon, null, "Extension is not installed");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_add_tab_by_user_and_switch() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, INSTALL_PAGE);
+
+ let listener = {
+ onInstallCancelled() {
+ this.canceledPromise = Promise.resolve();
+ },
+ };
+ AddonManager.addInstallListener(listener);
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [INSTALL_XPI], function (url) {
+ content.wrappedJSObject.installMozAM(url);
+ });
+
+ // Show addon permission notification.
+ await promisePopupNotificationShown("addon-webext-permissions");
+ is(
+ document.getElementById("addon-webext-perm-list").childElementCount,
+ 5,
+ "Permissions list has 5 entries"
+ );
+
+ // Open about:newtab page in a new tab.
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:newtab",
+ false
+ );
+
+ // Switch to tab that is opening addon permission notification.
+ gBrowser.selectedTab = tab;
+ is(
+ document.getElementById("addon-webext-perm-list").childElementCount,
+ 5,
+ "Permission notification is shown again"
+ );
+ ok(!listener.canceledPromise, "Extension installation is not canceled");
+
+ // Cancel installation.
+ document.querySelector(".popup-notification-secondary-button").click();
+ await listener.canceledPromise;
+ info("Extension installation is canceled");
+
+ let addon = await AddonManager.getAddonByID("permissions@test.mozilla.org");
+ is(addon, null, "Extension is not installed");
+
+ AddonManager.removeInstallListener(listener);
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(newTab);
+});
diff --git a/browser/base/content/test/webextensions/browser_permissions_installTrigger.js b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js
new file mode 100644
index 0000000000..6cd99d699b
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js
@@ -0,0 +1,26 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+
+async function installTrigger(filename) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.InstallTrigger.enabled", true],
+ ["extensions.InstallTriggerImpl.enabled", true],
+ // Relax the user input requirements while running this test.
+ ["xpinstall.userActivation.required", false],
+ ],
+ });
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, INSTALL_PAGE);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [`${BASE}/${filename}`],
+ async function (url) {
+ content.wrappedJSObject.installTrigger(url);
+ }
+ );
+}
+
+add_task(() => testInstallMethod(installTrigger, "installAmo"));
diff --git a/browser/base/content/test/webextensions/browser_permissions_local_file.js b/browser/base/content/test/webextensions/browser_permissions_local_file.js
new file mode 100644
index 0000000000..a2fdc34db3
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_local_file.js
@@ -0,0 +1,43 @@
+"use strict";
+
+async function installFile(filename) {
+ const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+ let chromeUrl = Services.io.newURI(gTestPath);
+ let fileUrl = ChromeRegistry.convertChromeURL(chromeUrl);
+ let file = fileUrl.QueryInterface(Ci.nsIFileURL).file;
+ file.leafName = filename;
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ MockFilePicker.setFiles([file]);
+ MockFilePicker.afterOpenCallback = MockFilePicker.cleanup;
+
+ let { document } = await BrowserOpenAddonsMgr("addons://list/extension");
+
+ // Do the install...
+ await waitAboutAddonsViewLoaded(document);
+ let installButton = document.querySelector('[action="install-from-file"]');
+ installButton.click();
+}
+
+add_task(async function test_install_extension_from_local_file() {
+ // Listen for the first installId so we can check it later.
+ let firstInstallId = null;
+ AddonManager.addInstallListener({
+ onNewInstall(install) {
+ firstInstallId = install.installId;
+ AddonManager.removeInstallListener(this);
+ },
+ });
+
+ // Install the add-ons.
+ await testInstallMethod(installFile, "installLocal");
+
+ // Check we got an installId.
+ ok(
+ firstInstallId != null && !isNaN(firstInstallId),
+ "There was an installId found"
+ );
+});
diff --git a/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js
new file mode 100644
index 0000000000..1370ff18f7
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js
@@ -0,0 +1,18 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+
+async function installMozAM(filename) {
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, INSTALL_PAGE);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [`${BASE}/${filename}`],
+ async function (url) {
+ await content.wrappedJSObject.installMozAM(url);
+ }
+ );
+}
+
+add_task(() => testInstallMethod(installMozAM, "installAmo"));
diff --git a/browser/base/content/test/webextensions/browser_permissions_optional.js b/browser/base/content/test/webextensions/browser_permissions_optional.js
new file mode 100644
index 0000000000..7c8a654cbc
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_optional.js
@@ -0,0 +1,52 @@
+"use strict";
+add_task(async function test_request_permissions_without_prompt() {
+ async function pageScript() {
+ const NO_PROMPT_PERM = "activeTab";
+ window.addEventListener(
+ "keypress",
+ async () => {
+ let permGranted = await browser.permissions.request({
+ permissions: [NO_PROMPT_PERM],
+ });
+ browser.test.assertTrue(
+ permGranted,
+ `${NO_PROMPT_PERM} permission was granted.`
+ );
+ let perms = await browser.permissions.getAll();
+ browser.test.assertTrue(
+ perms.permissions.includes(NO_PROMPT_PERM),
+ `${NO_PROMPT_PERM} permission exists.`
+ );
+ browser.test.sendMessage("permsGranted");
+ },
+ { once: true }
+ );
+ browser.test.sendMessage("pageReady");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+ files: {
+ "page.html": `<html><head><script src="page.js"></script></head></html>`,
+ "page.js": pageScript,
+ },
+ manifest: {
+ optional_permissions: ["activeTab"],
+ },
+ });
+ await extension.startup();
+
+ let url = await extension.awaitMessage("ready");
+
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => {
+ await extension.awaitMessage("pageReady");
+
+ await BrowserTestUtils.synthesizeKey("a", {}, browser);
+
+ await extension.awaitMessage("permsGranted");
+ });
+
+ await extension.unload();
+});
diff --git a/browser/base/content/test/webextensions/browser_permissions_pointerevent.js b/browser/base/content/test/webextensions/browser_permissions_pointerevent.js
new file mode 100644
index 0000000000..188aa8e3bf
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_pointerevent.js
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_pointerevent() {
+ async function contentScript() {
+ document.addEventListener("pointerdown", async e => {
+ browser.test.assertTrue(true, "Should receive pointerdown");
+ e.preventDefault();
+ });
+
+ document.addEventListener("mousedown", e => {
+ browser.test.assertTrue(true, "Should receive mousedown");
+ });
+
+ document.addEventListener("mouseup", e => {
+ browser.test.assertTrue(true, "Should receive mouseup");
+ });
+
+ document.addEventListener("pointerup", e => {
+ browser.test.assertTrue(true, "Should receive pointerup");
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("pageReady");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+ files: {
+ "page.html": `<html><head><script src="page.js"></script></head></html>`,
+ "page.js": contentScript,
+ },
+ });
+ await extension.startup();
+ let url = await extension.awaitMessage("ready");
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => {
+ await extension.awaitMessage("pageReady");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ { type: "mousedown", button: 0 },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ { type: "mouseup", button: 0 },
+ browser
+ );
+ await extension.awaitMessage("done");
+ });
+ await extension.unload();
+});
diff --git a/browser/base/content/test/webextensions/browser_permissions_unsigned.js b/browser/base/content/test/webextensions/browser_permissions_unsigned.js
new file mode 100644
index 0000000000..dff7ad872e
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_unsigned.js
@@ -0,0 +1,63 @@
+"use strict";
+
+const ID = "permissions@test.mozilla.org";
+const WARNING_ICON = "chrome://global/skin/icons/warning.svg";
+
+add_task(async function test_unsigned() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.InstallTrigger.enabled", true],
+ ["extensions.InstallTriggerImpl.enabled", true],
+ // Relax the user input requirements while running this test.
+ ["xpinstall.userActivation.required", false],
+ ],
+ });
+
+ let testURI = makeURI("https://example.com/");
+ PermissionTestUtils.add(testURI, "install", Services.perms.ALLOW_ACTION);
+ registerCleanupFunction(() => PermissionTestUtils.remove(testURI, "install"));
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ `${BASE}/file_install_extensions.html`
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [`${BASE}/browser_webext_unsigned.xpi`],
+ async function (url) {
+ content.wrappedJSObject.installTrigger(url);
+ }
+ );
+
+ let panel = await promisePopupNotificationShown("addon-webext-permissions");
+
+ is(panel.getAttribute("icon"), WARNING_ICON);
+ let description = panel.querySelector(
+ ".popup-notification-description"
+ ).textContent;
+ const expected = formatExtValue("webext-perms-header-unsigned-with-perms", {
+ extension: "<>",
+ });
+ for (let part of expected.split("<>")) {
+ ok(
+ description.includes(part),
+ "Install notification includes unsigned warning"
+ );
+ }
+
+ // cancel the install
+ let promise = promiseInstallEvent({ id: ID }, "onInstallCancelled");
+ panel.secondaryButton.click();
+ await promise;
+
+ let addon = await AddonManager.getAddonByID(ID);
+ is(addon, null, "Extension is not installed");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/webextensions/browser_update_checkForUpdates.js b/browser/base/content/test/webextensions/browser_update_checkForUpdates.js
new file mode 100644
index 0000000000..b902527cae
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_update_checkForUpdates.js
@@ -0,0 +1,17 @@
+// Invoke the "Check for Updates" menu item
+function checkAll(win) {
+ triggerPageOptionsAction(win, "check-for-updates");
+ return new Promise(resolve => {
+ let observer = {
+ observe(subject, topic, data) {
+ Services.obs.removeObserver(observer, "EM-update-check-finished");
+ resolve();
+ },
+ };
+ Services.obs.addObserver(observer, "EM-update-check-finished");
+ });
+}
+
+// Test "Check for Updates" with both auto-update settings
+add_task(() => interactiveUpdateTest(true, checkAll));
+add_task(() => interactiveUpdateTest(false, checkAll));
diff --git a/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
new file mode 100644
index 0000000000..016eb22667
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
@@ -0,0 +1,77 @@
+// Set some prefs that apply to all the tests in this file
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+
+ // Don't require the extensions to be signed
+ ["xpinstall.signatures.required", false],
+
+ // Point updates to the local mochitest server
+ ["extensions.update.url", `${BASE}/browser_webext_update.json`],
+ ],
+ });
+});
+
+// Helper to test that an update of a given extension does not
+// generate any permission prompts.
+async function testUpdateNoPrompt(
+ filename,
+ id,
+ initialVersion = "1.0",
+ updateVersion = "2.0"
+) {
+ // Navigate away to ensure that BrowserOpenAddonMgr() opens a new tab
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:mozilla");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ // Install initial version of the test extension
+ let addon = await promiseInstallAddon(`${BASE}/${filename}`);
+ ok(addon, "Addon was installed");
+ is(addon.version, initialVersion, "Version 1 of the addon is installed");
+
+ // Go to Extensions in about:addons
+ let win = await BrowserOpenAddonsMgr("addons://list/extension");
+
+ await waitAboutAddonsViewLoaded(win.document);
+
+ let sawPopup = false;
+ function popupListener() {
+ sawPopup = true;
+ }
+ PopupNotifications.panel.addEventListener("popupshown", popupListener);
+
+ // Trigger an update check, we should see the update get applied
+ let updatePromise = waitForUpdate(addon);
+ triggerPageOptionsAction(win, "check-for-updates");
+ await updatePromise;
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, updateVersion, "Should have upgraded");
+
+ ok(!sawPopup, "Should not have seen a permission notification");
+ PopupNotifications.panel.removeEventListener("popupshown", popupListener);
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await addon.uninstall();
+}
+
+// Test that we don't see a prompt when no new promptable permissions
+// are added.
+add_task(() =>
+ testUpdateNoPrompt(
+ "browser_webext_update_perms1.xpi",
+ "update_perms@tests.mozilla.org"
+ )
+);
+
+// Test that an update that narrows origin permissions is just applied without
+// showing a notification promt
+add_task(() =>
+ testUpdateNoPrompt(
+ "browser_webext_update_origins1.xpi",
+ "update_origins@tests.mozilla.org"
+ )
+);
diff --git a/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi
new file mode 100644
index 0000000000..ab97d96a11
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_permissions.xpi b/browser/base/content/test/webextensions/browser_webext_permissions.xpi
new file mode 100644
index 0000000000..a8c8c38ef8
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_permissions.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_unsigned.xpi b/browser/base/content/test/webextensions/browser_webext_unsigned.xpi
new file mode 100644
index 0000000000..55779530ce
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_unsigned.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update.json b/browser/base/content/test/webextensions/browser_webext_update.json
new file mode 100644
index 0000000000..ae18044e9c
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update.json
@@ -0,0 +1,70 @@
+{
+ "addons": {
+ "update2@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "update_icon2@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "update_perms@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "legacy_update@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_legacy_webext.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "advisory_max_version": "*"
+ }
+ }
+ }
+ ]
+ },
+ "update_origins@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/base/content/test/webextensions/browser_webext_update1.xpi b/browser/base/content/test/webextensions/browser_webext_update1.xpi
new file mode 100644
index 0000000000..086b3839b9
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update1.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update2.xpi b/browser/base/content/test/webextensions/browser_webext_update2.xpi
new file mode 100644
index 0000000000..19967c39c0
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update2.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi
new file mode 100644
index 0000000000..24cb7616d2
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi
new file mode 100644
index 0000000000..fd9cf7eb0e
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_origins1.xpi b/browser/base/content/test/webextensions/browser_webext_update_origins1.xpi
new file mode 100644
index 0000000000..2909f8e8fd
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update_origins1.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi b/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi
new file mode 100644
index 0000000000..b1051affb1
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi
new file mode 100644
index 0000000000..f4942f9082
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi
new file mode 100644
index 0000000000..2c023edc9d
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/file_install_extensions.html b/browser/base/content/test/webextensions/file_install_extensions.html
new file mode 100644
index 0000000000..9dd8ae830d
--- /dev/null
+++ b/browser/base/content/test/webextensions/file_install_extensions.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="text/javascript">
+function installMozAM(url) {
+ return navigator.mozAddonManager.createInstall({url})
+ .then(install => install.install());
+}
+
+function installTrigger(url) {
+ InstallTrigger.install({extension: url});
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webextensions/head.js b/browser/base/content/test/webextensions/head.js
new file mode 100644
index 0000000000..71d1e6d009
--- /dev/null
+++ b/browser/base/content/test/webextensions/head.js
@@ -0,0 +1,650 @@
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+});
+
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+ // eslint-disable-next-line no-shadow
+ const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+ );
+ return Management;
+});
+
+let { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+let extL10n = null;
+/**
+ * @param {string} id
+ * @param {object} [args]
+ * @returns {string}
+ */
+function formatExtValue(id, args) {
+ if (!extL10n) {
+ extL10n = new Localization(
+ [
+ "toolkit/global/extensions.ftl",
+ "toolkit/global/extensionPermissions.ftl",
+ "branding/brand.ftl",
+ ],
+ true
+ );
+ }
+ return extL10n.formatValueSync(id, args);
+}
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ * The name of the notification to wait for.
+ *
+ * @returns {Promise}
+ * Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name) {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = PopupNotifications.getNotification(name);
+ if (!notification) {
+ return;
+ }
+
+ ok(notification, `${name} notification shown`);
+ ok(PopupNotifications.isPanelOpen, "notification panel open");
+
+ PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+ resolve(PopupNotifications.panel.firstElementChild);
+ }
+
+ PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function promiseAppMenuNotificationShown(id) {
+ const { AppMenuNotifications } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppMenuNotifications.sys.mjs"
+ );
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = AppMenuNotifications.activeNotification;
+ if (!notification) {
+ return;
+ }
+
+ is(notification.id, id, `${id} notification shown`);
+ ok(PanelUI.isNotificationPanelOpen, "notification panel open");
+
+ PanelUI.notificationPanel.removeEventListener("popupshown", popupshown);
+
+ let popupnotificationID = PanelUI._getPopupId(notification);
+ let popupnotification = document.getElementById(popupnotificationID);
+
+ resolve(popupnotification);
+ }
+ PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
+ });
+}
+
+/**
+ * Wait for a specific install event to fire for a given addon
+ *
+ * @param {AddonWrapper} addon
+ * The addon to watch for an event on
+ * @param {string}
+ * The name of the event to watch for (e.g., onInstallEnded)
+ *
+ * @returns {Promise}
+ * Resolves when the event triggers with the first argument
+ * to the event handler as the resolution value.
+ */
+function promiseInstallEvent(addon, event) {
+ return new Promise(resolve => {
+ let listener = {};
+ listener[event] = (install, arg) => {
+ if (install.addon.id == addon.id) {
+ AddonManager.removeInstallListener(listener);
+ resolve(arg);
+ }
+ };
+ AddonManager.addInstallListener(listener);
+ });
+}
+
+/**
+ * Install an (xpi packaged) extension
+ *
+ * @param {string} url
+ * URL of the .xpi file to install
+ * @param {Object?} installTelemetryInfo
+ * an optional object that contains additional details used by the telemetry events.
+ *
+ * @returns {Promise}
+ * Resolves when the extension has been installed with the Addon
+ * object as the resolution value.
+ */
+async function promiseInstallAddon(url, telemetryInfo) {
+ let install = await AddonManager.getInstallForURL(url, { telemetryInfo });
+ install.install();
+
+ let addon = await new Promise(resolve => {
+ install.addListener({
+ onInstallEnded(_install, _addon) {
+ resolve(_addon);
+ },
+ });
+ });
+
+ if (addon.isWebExtension) {
+ await new Promise(resolve => {
+ function listener(event, extension) {
+ if (extension.id == addon.id) {
+ Management.off("ready", listener);
+ resolve();
+ }
+ }
+ Management.on("ready", listener);
+ });
+ }
+
+ return addon;
+}
+
+/**
+ * Wait for an update to the given webextension to complete.
+ * (This does not actually perform an update, it just watches for
+ * the events that occur as a result of an update.)
+ *
+ * @param {AddonWrapper} addon
+ * The addon to be updated.
+ *
+ * @returns {Promise}
+ * Resolves when the extension has ben updated.
+ */
+async function waitForUpdate(addon) {
+ let installPromise = promiseInstallEvent(addon, "onInstallEnded");
+ let readyPromise = new Promise(resolve => {
+ function listener(event, extension) {
+ if (extension.id == addon.id) {
+ Management.off("ready", listener);
+ resolve();
+ }
+ }
+ Management.on("ready", listener);
+ });
+
+ let [newAddon] = await Promise.all([installPromise, readyPromise]);
+ return newAddon;
+}
+
+function waitAboutAddonsViewLoaded(doc) {
+ return BrowserTestUtils.waitForEvent(doc, "view-loaded");
+}
+
+/**
+ * Trigger an action from the page options menu.
+ */
+function triggerPageOptionsAction(win, action) {
+ win.document.querySelector(`#page-options [action="${action}"]`).click();
+}
+
+function isDefaultIcon(icon) {
+ return icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+}
+
+/**
+ * Check the contents of a permission popup notification
+ *
+ * @param {Window} panel
+ * The popup window.
+ * @param {string|regexp|function} checkIcon
+ * The icon expected to appear in the notification. If this is a
+ * string, it must match the icon url exactly. If it is a
+ * regular expression it is tested against the icon url, and if
+ * it is a function, it is called with the icon url and returns
+ * true if the url is correct.
+ * @param {array} permissions
+ * The expected entries in the permissions list. Each element
+ * in this array is itself a 2-element array with the string key
+ * for the item (e.g., "webext-perms-description-foo") and an
+ * optional formatting parameter.
+ * @param {boolean} sideloaded
+ * Whether the notification is for a sideloaded extenion.
+ */
+function checkNotification(panel, checkIcon, permissions, sideloaded) {
+ let icon = panel.getAttribute("icon");
+ let ul = document.getElementById("addon-webext-perm-list");
+ let singleDataEl = document.getElementById("addon-webext-perm-single-entry");
+ let learnMoreLink = document.getElementById("addon-webext-perm-info");
+
+ if (checkIcon instanceof RegExp) {
+ ok(
+ checkIcon.test(icon),
+ `Notification icon is correct ${JSON.stringify(icon)} ~= ${checkIcon}`
+ );
+ } else if (typeof checkIcon == "function") {
+ ok(checkIcon(icon), "Notification icon is correct");
+ } else {
+ is(icon, checkIcon, "Notification icon is correct");
+ }
+
+ let description = panel.querySelector(
+ ".popup-notification-description"
+ ).textContent;
+ let descL10nId = "webext-perms-header";
+ if (permissions.length) {
+ descL10nId = "webext-perms-header-with-perms";
+ }
+ if (sideloaded) {
+ descL10nId = "webext-perms-sideload-header";
+ }
+ const exp = formatExtValue(descL10nId, { extension: "<>" }).split("<>");
+ ok(description.startsWith(exp.at(0)), "Description is the expected one");
+ ok(description.endsWith(exp.at(-1)), "Description is the expected one");
+
+ is(
+ learnMoreLink.hidden,
+ !permissions.length,
+ "Permissions learn more is hidden if there are no permissions"
+ );
+
+ if (!permissions.length) {
+ ok(ul.hidden, "Permissions list is hidden");
+ ok(singleDataEl.hidden, "Single permission data entry is hidden");
+ ok(
+ !(ul.childElementCount || singleDataEl.textContent),
+ "Permission list and single permission element have no entries"
+ );
+ } else if (permissions.length === 1) {
+ ok(ul.hidden, "Permissions list is hidden");
+ ok(!ul.childElementCount, "Permission list has no entries");
+ ok(singleDataEl.textContent, "Single permission data label has been set");
+ } else {
+ ok(singleDataEl.hidden, "Single permission data entry is hidden");
+ ok(
+ !singleDataEl.textContent,
+ "Single permission data label has not been set"
+ );
+ for (let i in permissions) {
+ let [key, param] = permissions[i];
+ const expected = formatExtValue(key, param);
+ is(
+ ul.children[i].textContent,
+ expected,
+ `Permission number ${i + 1} is correct`
+ );
+ }
+ }
+}
+
+/**
+ * Test that install-time permission prompts work for a given
+ * installation method.
+ *
+ * @param {Function} installFn
+ * Callable that takes the name of an xpi file to install and
+ * starts to install it. Should return a Promise that resolves
+ * when the install is finished or rejects if the install is canceled.
+ * @param {string} telemetryBase
+ * If supplied, the base type for telemetry events that should be
+ * recorded for this install method.
+ *
+ * @returns {Promise}
+ */
+async function testInstallMethod(installFn, telemetryBase) {
+ const PERMS_XPI = "browser_webext_permissions.xpi";
+ const NO_PERMS_XPI = "browser_webext_nopermissions.xpi";
+ const ID = "permissions@test.mozilla.org";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+
+ let testURI = makeURI("https://example.com/");
+ PermissionTestUtils.add(testURI, "install", Services.perms.ALLOW_ACTION);
+ registerCleanupFunction(() => PermissionTestUtils.remove(testURI, "install"));
+
+ async function runOnce(filename, cancel) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let installPromise = new Promise(resolve => {
+ let listener = {
+ onDownloadCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onDownloadFailed() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onInstallCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onInstallEnded() {
+ AddonManager.removeInstallListener(listener);
+ resolve(true);
+ },
+
+ onInstallFailed() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+
+ let installMethodPromise = installFn(filename);
+
+ let panel = await promisePopupNotificationShown("addon-webext-permissions");
+ if (filename == PERMS_XPI) {
+ // The icon should come from the extension, don't bother with the precise
+ // path, just make sure we've got a jar url pointing to the right path
+ // inside the jar.
+ checkNotification(panel, /^jar:file:\/\/.*\/icon\.png$/, [
+ [
+ "webext-perms-host-description-wildcard",
+ { domain: "wildcard.domain" },
+ ],
+ [
+ "webext-perms-host-description-one-site",
+ { domain: "singlehost.domain" },
+ ],
+ ["webext-perms-description-nativeMessaging"],
+ // The below permissions are deliberately in this order as permissions
+ // are sorted alphabetically by the permission string to match AMO.
+ ["webext-perms-description-history"],
+ ["webext-perms-description-tabs"],
+ ]);
+ } else if (filename == NO_PERMS_XPI) {
+ checkNotification(panel, isDefaultIcon, []);
+ }
+
+ if (cancel) {
+ panel.secondaryButton.click();
+ try {
+ await installMethodPromise;
+ } catch (err) {}
+ } else {
+ // Look for post-install notification
+ let postInstallPromise =
+ promiseAppMenuNotificationShown("addon-installed");
+ panel.button.click();
+
+ // Press OK on the post-install notification
+ panel = await postInstallPromise;
+ panel.button.click();
+
+ await installMethodPromise;
+ }
+
+ let result = await installPromise;
+ let addon = await AddonManager.getAddonByID(ID);
+ if (cancel) {
+ ok(!result, "Installation was cancelled");
+ is(addon, null, "Extension is not installed");
+ } else {
+ ok(result, "Installation completed");
+ isnot(addon, null, "Extension is installed");
+ await addon.uninstall();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ // A few different tests for each installation method:
+ // 1. Start installation of an extension that requests no permissions,
+ // verify the notification contents, then cancel the install
+ await runOnce(NO_PERMS_XPI, true);
+
+ // 2. Same as #1 but with an extension that requests some permissions.
+ await runOnce(PERMS_XPI, true);
+
+ // 3. Repeat with the same extension from step 2 but this time,
+ // accept the permissions to install the extension. (Then uninstall
+ // the extension to clean up.)
+ await runOnce(PERMS_XPI, false);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+// Helper function to test a specific scenario for interactive updates.
+// `checkFn` is a callable that triggers a check for updates.
+// `autoUpdate` specifies whether the test should be run with
+// updates applied automatically or not.
+async function interactiveUpdateTest(autoUpdate, checkFn) {
+ AddonTestUtils.initMochitest(this);
+
+ const ID = "update2@tests.mozilla.org";
+ const FAKE_INSTALL_SOURCE = "fake-install-source";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+
+ ["extensions.update.autoUpdateDefault", autoUpdate],
+
+ // Point updates to the local mochitest server
+ ["extensions.update.url", `${BASE}/browser_webext_update.json`],
+ ],
+ });
+
+ AddonTestUtils.hookAMTelemetryEvents();
+
+ // Trigger an update check, manually applying the update if we're testing
+ // without auto-update.
+ async function triggerUpdate(win, addon) {
+ let manualUpdatePromise;
+ if (!autoUpdate) {
+ manualUpdatePromise = new Promise(resolve => {
+ let listener = {
+ onNewInstall() {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+ }
+
+ let promise = checkFn(win, addon);
+
+ if (manualUpdatePromise) {
+ await manualUpdatePromise;
+
+ let doc = win.document;
+ if (win.gViewController.currentViewId !== "addons://updates/available") {
+ let showUpdatesBtn = doc.querySelector("addon-updates-message").button;
+ await TestUtils.waitForCondition(() => {
+ return !showUpdatesBtn.hidden;
+ }, "Wait for show updates button");
+ let viewChanged = waitAboutAddonsViewLoaded(doc);
+ showUpdatesBtn.click();
+ await viewChanged;
+ }
+ let card = await TestUtils.waitForCondition(() => {
+ return doc.querySelector(`addon-card[addon-id="${ID}"]`);
+ }, `Wait addon card for "${ID}"`);
+ let updateBtn = card.querySelector('panel-item[action="install-update"]');
+ ok(updateBtn, `Found update button for "${ID}"`);
+ updateBtn.click();
+ }
+
+ return { promise };
+ }
+
+ // Navigate away from the starting page to force about:addons to load
+ // in a new tab during the tests below.
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:mozilla");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(`${BASE}/browser_webext_update1.xpi`, {
+ source: FAKE_INSTALL_SOURCE,
+ });
+ ok(addon, "Addon was installed");
+ is(addon.version, "1.0", "Version 1 of the addon is installed");
+
+ let win = await BrowserOpenAddonsMgr("addons://list/extension");
+
+ await waitAboutAddonsViewLoaded(win.document);
+
+ // Trigger an update check
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ let { promise: checkPromise } = await triggerUpdate(win, addon);
+ let panel = await popupPromise;
+
+ // Click the cancel button, wait to see the cancel event
+ let cancelPromise = promiseInstallEvent(addon, "onInstallCancelled");
+ panel.secondaryButton.click();
+ await cancelPromise;
+
+ addon = await AddonManager.getAddonByID(ID);
+ is(addon.version, "1.0", "Should still be running the old version");
+
+ // Make sure the update check is completely finished.
+ await checkPromise;
+
+ // Trigger a new update check
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ checkPromise = (await triggerUpdate(win, addon)).promise;
+
+ // This time, accept the upgrade
+ let updatePromise = waitForUpdate(addon);
+ panel = await popupPromise;
+ panel.button.click();
+
+ addon = await updatePromise;
+ is(addon.version, "2.0", "Should have upgraded");
+
+ await checkPromise;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ const collectedUpdateEvents = AddonTestUtils.getAMTelemetryEvents().filter(
+ evt => {
+ return evt.method === "update";
+ }
+ );
+
+ Assert.deepEqual(
+ collectedUpdateEvents.map(evt => evt.extra.step),
+ [
+ // First update is cancelled on the permission prompt.
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "cancelled",
+ // Second update is expected to be completed.
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "completed",
+ ],
+ "Got the expected sequence on update telemetry events"
+ );
+
+ ok(
+ collectedUpdateEvents.every(evt => evt.extra.addon_id === ID),
+ "Every update telemetry event should have the expected addon_id extra var"
+ );
+
+ ok(
+ collectedUpdateEvents.every(
+ evt => evt.extra.source === FAKE_INSTALL_SOURCE
+ ),
+ "Every update telemetry event should have the expected source extra var"
+ );
+
+ ok(
+ collectedUpdateEvents.every(evt => evt.extra.updated_from === "user"),
+ "Every update telemetry event should have the update_from extra var 'user'"
+ );
+
+ let hasPermissionsExtras = collectedUpdateEvents
+ .filter(evt => {
+ return evt.extra.step === "permissions_prompt";
+ })
+ .every(evt => {
+ return Number.isInteger(parseInt(evt.extra.num_strings, 10));
+ });
+
+ ok(
+ hasPermissionsExtras,
+ "Every 'permissions_prompt' update telemetry event should have the permissions extra vars"
+ );
+
+ let hasDownloadTimeExtras = collectedUpdateEvents
+ .filter(evt => {
+ return evt.extra.step === "download_completed";
+ })
+ .every(evt => {
+ const download_time = parseInt(evt.extra.download_time, 10);
+ return !isNaN(download_time) && download_time > 0;
+ });
+
+ ok(
+ hasDownloadTimeExtras,
+ "Every 'download_completed' update telemetry event should have a download_time extra vars"
+ );
+}
+
+// The tests in this directory install a bunch of extensions but they
+// need to uninstall them before exiting, as a stray leftover extension
+// after one test can foul up subsequent tests.
+// So, add a task to run before any tests that grabs a list of all the
+// add-ons that are pre-installed in the test environment and then checks
+// the list of installed add-ons at the end of the test to make sure no
+// new add-ons have been added.
+// Individual tests can store a cleanup function in the testCleanup global
+// to ensure it gets called before the final check is performed.
+let testCleanup;
+add_setup(async function head_setup() {
+ let addons = await AddonManager.getAllAddons();
+ let existingAddons = new Set(addons.map(a => a.id));
+
+ registerCleanupFunction(async function () {
+ if (testCleanup) {
+ await testCleanup();
+ testCleanup = null;
+ }
+
+ for (let addon of await AddonManager.getAllAddons()) {
+ // Builtin search extensions may have been installed by SearchService
+ // during the test run, ignore those.
+ if (
+ !existingAddons.has(addon.id) &&
+ !(addon.isBuiltin && addon.id.endsWith("@search.mozilla.org"))
+ ) {
+ ok(
+ false,
+ `Addon ${addon.id} was left installed at the end of the test`
+ );
+ await addon.uninstall();
+ }
+ }
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser.ini b/browser/base/content/test/webrtc/browser.ini
new file mode 100644
index 0000000000..63ae7704c8
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser.ini
@@ -0,0 +1,118 @@
+[DEFAULT]
+support-files =
+ get_user_media.html
+ get_user_media2.html
+ get_user_media_in_frame.html
+ get_user_media_in_xorigin_frame.html
+ get_user_media_in_xorigin_frame_ancestor.html
+ head.js
+ peerconnection_connect.html
+ single_peerconnection.html
+
+prefs =
+ privacy.webrtc.allowSilencingNotifications=true
+ privacy.webrtc.legacyGlobalIndicator=false
+ privacy.webrtc.sharedTabWarning=false
+ privacy.webrtc.deviceGracePeriodTimeoutMs=0
+
+[browser_WebrtcGlobalInformation.js]
+[browser_device_controls_menus.js]
+skip-if =
+ debug # bug 1369731
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_devices_get_user_media.js]
+https_first_disabled = true
+skip-if =
+ os == "linux" && bits == 64 # linux: bug 976544, Bug 1616011
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_anim.js]
+https_first_disabled = true
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_by_device_id.js]
+https_first_disabled = true
+[browser_devices_get_user_media_default_permissions.js]
+https_first_disabled = true
+[browser_devices_get_user_media_in_frame.js]
+https_first_disabled = true
+skip-if = debug # bug 1369731
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_in_xorigin_frame.js]
+https_first_disabled = true
+skip-if =
+ debug # bug 1369731
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_devices_get_user_media_in_xorigin_frame_chain.js]
+https_first_disabled = true
+[browser_devices_get_user_media_multi_process.js]
+https_first_disabled = true
+skip-if =
+ (debug && os == "win") # bug 1393761
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_devices_get_user_media_paused.js]
+https_first_disabled = true
+skip-if =
+ (os == "win" && !debug) # Bug 1440900
+ (os =="linux" && !debug && bits == 64) # Bug 1440900
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_devices_get_user_media_queue_request.js]
+https_first_disabled = true
+skip-if =
+ os == "linux" # Bug 1775945
+ os == "win" && !debug # Bug 1775945
+[browser_devices_get_user_media_screen.js]
+https_first_disabled = true
+skip-if =
+ (os == 'linux') # Bug 1503991
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+ os == 'win' # high frequency intermittent, bug 1739107
+[browser_devices_get_user_media_screen_tab_close.js]
+skip-if =
+ apple_catalina # platform migration
+ apple_silicon # bug 1707735
+[browser_devices_get_user_media_tear_off_tab.js]
+https_first_disabled = true
+skip-if =
+ apple_catalina # platform migration
+ apple_silicon # bug 1707735
+ os == "linux" # Bug 1775945
+ os == "win" && !debug # Bug 1775945
+[browser_devices_get_user_media_unprompted_access.js]
+skip-if =
+ os == "linux" && bits == 64 && !debug # Bug 1712012
+https_first_disabled = true
+[browser_devices_get_user_media_unprompted_access_in_frame.js]
+https_first_disabled = true
+[browser_devices_get_user_media_unprompted_access_queue_request.js]
+https_first_disabled = true
+[browser_devices_get_user_media_unprompted_access_tear_off_tab.js]
+https_first_disabled = true
+skip-if = (os == "win" && bits == 64) # win8: bug 1334752
+[browser_devices_select_audio_output.js]
+[browser_global_mute_toggles.js]
+[browser_indicator_popuphiding.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_notification_silencing.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_stop_sharing_button.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_stop_streams_on_indicator_close.js]
+skip-if =
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[browser_tab_switch_warning.js]
+skip-if =
+ apple_catalina # platform migration
+[browser_webrtc_hooks.js]
diff --git a/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js b/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js
new file mode 100644
index 0000000000..d66aa00461
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js
@@ -0,0 +1,484 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+);
+
+let getStatsReports = async (filter = "") => {
+ let { reports } = await new Promise(r =>
+ WebrtcGlobalInformation.getAllStats(r, filter)
+ );
+
+ ok(Array.isArray(reports), "|reports| is an array");
+
+ let sanityCheckReport = report => {
+ isnot(report.pcid, "", "pcid is non-empty");
+ if (filter.length) {
+ is(report.pcid, filter, "pcid matches filter");
+ }
+
+ // Check for duplicates
+ const checkForDuplicateId = statsArray => {
+ ok(Array.isArray(statsArray), "|statsArray| is an array");
+ const ids = new Set();
+ statsArray.forEach(stat => {
+ is(typeof stat.id, "string", "|stat.id| is a string");
+ ok(
+ !ids.has(stat.id),
+ `Id ${stat.id} should appear only once. Stat was ${JSON.stringify(
+ stat
+ )}`
+ );
+ ids.add(stat.id);
+ });
+ };
+
+ checkForDuplicateId(report.inboundRtpStreamStats);
+ checkForDuplicateId(report.outboundRtpStreamStats);
+ checkForDuplicateId(report.remoteInboundRtpStreamStats);
+ checkForDuplicateId(report.remoteOutboundRtpStreamStats);
+ checkForDuplicateId(report.rtpContributingSourceStats);
+ checkForDuplicateId(report.iceCandidatePairStats);
+ checkForDuplicateId(report.iceCandidateStats);
+ checkForDuplicateId(report.trickledIceCandidateStats);
+ checkForDuplicateId(report.dataChannelStats);
+ checkForDuplicateId(report.codecStats);
+ };
+
+ reports.forEach(sanityCheckReport);
+ return reports;
+};
+
+const getStatsHistoryPcIds = async () => {
+ return new Promise(r => WebrtcGlobalInformation.getStatsHistoryPcIds(r));
+};
+
+const getStatsHistorySince = async (pcid, after, sdpAfter) => {
+ return new Promise(r =>
+ WebrtcGlobalInformation.getStatsHistorySince(r, pcid, after, sdpAfter)
+ );
+};
+
+let getLogging = async () => {
+ let logs = await new Promise(r => WebrtcGlobalInformation.getLogging("", r));
+ ok(Array.isArray(logs), "|logs| is an array");
+ return logs;
+};
+
+let checkStatsReportCount = async (count, filter = "") => {
+ let reports = await getStatsReports(filter);
+ is(reports.length, count, `|reports| should have length ${count}`);
+ if (reports.length != count) {
+ info(`reports = ${JSON.stringify(reports)}`);
+ }
+ return reports;
+};
+
+let checkLoggingEmpty = async () => {
+ let logs = await getLogging();
+ is(logs.length, 0, "Logging is empty");
+ if (logs.length) {
+ info(`logs = ${JSON.stringify(logs)}`);
+ }
+ return logs;
+};
+
+let checkLoggingNonEmpty = async () => {
+ let logs = await getLogging();
+ isnot(logs.length, 0, "Logging is not empty");
+ return logs;
+};
+
+let clearAndCheck = async () => {
+ WebrtcGlobalInformation.clearAllStats();
+ WebrtcGlobalInformation.clearLogging();
+ await checkStatsReportCount(0);
+ await checkLoggingEmpty();
+};
+
+let openTabInNewProcess = async file => {
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+ );
+ let absoluteURI = rootDir + file;
+
+ return BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: absoluteURI,
+ forceNewProcess: true,
+ });
+};
+
+let killTabProcess = async tab => {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ ChromeUtils.privateNoteIntentionalCrash();
+ });
+ ProcessTools.kill(tab.linkedBrowser.frameLoader.remoteTab.osPid);
+};
+
+add_task(async () => {
+ info("Test that clearAllStats is callable");
+ WebrtcGlobalInformation.clearAllStats();
+ ok(true, "clearAllStats returns");
+});
+
+add_task(async () => {
+ info("Test that clearLogging is callable");
+ WebrtcGlobalInformation.clearLogging();
+ ok(true, "clearLogging returns");
+});
+
+add_task(async () => {
+ info(
+ "Test that getAllStats is callable, and returns 0 results when no RTCPeerConnections have existed"
+ );
+ await checkStatsReportCount(0);
+});
+
+add_task(async () => {
+ info(
+ "Test that getLogging is callable, and returns 0 results when no RTCPeerConnections have existed"
+ );
+ await checkLoggingEmpty();
+});
+
+add_task(async () => {
+ info("Test that we can get stats/logging for a PC on the parent process");
+ await clearAndCheck();
+ let pc = new RTCPeerConnection();
+ await pc.setLocalDescription(
+ await pc.createOffer({ offerToReceiveAudio: true })
+ );
+ // Let ICE stack go quiescent
+ await new Promise(r => {
+ pc.onicegatheringstatechange = () => {
+ if (pc.iceGatheringState == "complete") {
+ r();
+ }
+ };
+ });
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ pc.close();
+ pc = null;
+ // Closing a PC should not do anything to the ICE logging
+ await checkLoggingNonEmpty();
+ // There's just no way to get a signal that the ICE stack has stopped logging
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test that we can get stats/logging for a PC on a content process");
+ await clearAndCheck();
+ let tab = await openTabInNewProcess("single_peerconnection.html");
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ await killTabProcess(tab);
+ BrowserTestUtils.removeTab(tab);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info(
+ "Test that we can get stats/logging for two connected PCs on a content process"
+ );
+ await clearAndCheck();
+ let tab = await openTabInNewProcess("peerconnection_connect.html");
+ await checkStatsReportCount(2);
+ await checkLoggingNonEmpty();
+ await killTabProcess(tab);
+ BrowserTestUtils.removeTab(tab);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test filtering for stats reports (parent process)");
+ await clearAndCheck();
+ let pc1 = new RTCPeerConnection();
+ let pc2 = new RTCPeerConnection();
+ let allReports = await checkStatsReportCount(2);
+ await checkStatsReportCount(1, allReports[0].pcid);
+ pc1.close();
+ pc2.close();
+ pc1 = null;
+ pc2 = null;
+ await checkStatsReportCount(1, allReports[0].pcid);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test filtering for stats reports (content process)");
+ await clearAndCheck();
+ let tab1 = await openTabInNewProcess("single_peerconnection.html");
+ let tab2 = await openTabInNewProcess("single_peerconnection.html");
+ let allReports = await checkStatsReportCount(2);
+ await checkStatsReportCount(1, allReports[0].pcid);
+ await killTabProcess(tab1);
+ BrowserTestUtils.removeTab(tab1);
+ await killTabProcess(tab2);
+ BrowserTestUtils.removeTab(tab2);
+ await checkStatsReportCount(1, allReports[0].pcid);
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test that stats/logging persists when PC is closed (parent process)");
+ await clearAndCheck();
+ let pc = new RTCPeerConnection();
+ // This stuff will generate logging
+ await pc.setLocalDescription(
+ await pc.createOffer({ offerToReceiveAudio: true })
+ );
+ // Once gathering is done, the ICE stack should go quiescent
+ await new Promise(r => {
+ pc.onicegatheringstatechange = () => {
+ if (pc.iceGatheringState == "complete") {
+ r();
+ }
+ };
+ });
+ let reports = await checkStatsReportCount(1);
+ isnot(
+ window.browsingContext.browserId,
+ undefined,
+ "browserId is defined for parent process"
+ );
+ is(
+ reports[0].browserId,
+ window.browsingContext.browserId,
+ "browserId for stats report matches parent process"
+ );
+ await checkLoggingNonEmpty();
+ pc.close();
+ pc = null;
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ info("Test that stats/logging persists when PC is closed (content process)");
+ await clearAndCheck();
+ let tab = await openTabInNewProcess("single_peerconnection.html");
+ let { browserId } = tab.linkedBrowser;
+ let reports = await checkStatsReportCount(1);
+ is(reports[0].browserId, browserId, "browserId for stats report matches tab");
+ isnot(
+ browserId,
+ window.browsingContext.browserId,
+ "tab browser id is not the same as parent process browser id"
+ );
+ await checkLoggingNonEmpty();
+ await killTabProcess(tab);
+ BrowserTestUtils.removeTab(tab);
+ await checkStatsReportCount(1);
+ await checkLoggingNonEmpty();
+ await clearAndCheck();
+});
+
+const set_int_pref_returning_unsetter = (pref, num) => {
+ const value = Services.prefs.getIntPref(pref);
+ Services.prefs.setIntPref(pref, num);
+ return () => Services.prefs.setIntPref(pref, value);
+};
+
+const stats_history_is_enabled = () => {
+ return Services.prefs.getBoolPref("media.aboutwebrtc.hist.enabled");
+};
+
+const set_max_histories_to_retain = num =>
+ set_int_pref_returning_unsetter(
+ "media.aboutwebrtc.hist.closed_stats_to_retain",
+ num
+ );
+
+const set_history_storage_window_s = num =>
+ set_int_pref_returning_unsetter(
+ "media.aboutwebrtc.hist.storage_window_s",
+ num
+ );
+
+add_task(async () => {
+ if (!stats_history_is_enabled()) {
+ return;
+ }
+ info(
+ "Test that stats history is available after close until clearLongTermStats is called"
+ );
+ await clearAndCheck();
+ const pc = new RTCPeerConnection();
+
+ const ids = await getStatsHistoryPcIds();
+ is(ids.length, 1, "There is a single PeerConnection Id for stats history.");
+
+ let firstLen = 0;
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ {
+ const history = await getStatsHistorySince(ids[0]);
+ firstLen = history.reports.length;
+ ok(
+ history.reports.length,
+ "There is at least a single PeerConnection stats history before close."
+ );
+ }
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ {
+ const history = await getStatsHistorySince(ids[0]);
+ const secondLen = history.reports.length;
+ ok(
+ secondLen > firstLen,
+ "After waiting there are more history entries available."
+ );
+ }
+ pc.close();
+ // After close for final stats and pc teardown to settle
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ {
+ const history = await getStatsHistorySince(ids[0]);
+ ok(
+ history.reports.length,
+ "There is at least a single PeerConnection stats history after close."
+ );
+ }
+ await clearAndCheck();
+ {
+ const history = await getStatsHistorySince(ids[0]);
+ is(
+ history.reports.length,
+ 0,
+ "After PC.close and clearing the stats there are no history reports"
+ );
+ }
+ {
+ const ids1 = await getStatsHistoryPcIds();
+ is(
+ ids1.length,
+ 0,
+ "After PC.close and clearing the stats there are no history pcids"
+ );
+ }
+ {
+ const pc2 = new RTCPeerConnection();
+ const pc3 = new RTCPeerConnection();
+ let idsN = await getStatsHistoryPcIds();
+ is(
+ idsN.length,
+ 2,
+ "There are two pcIds after creating two PeerConnections"
+ );
+ pc2.close();
+ // After close for final stats and pc teardown to settle
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ await WebrtcGlobalInformation.clearAllStats();
+ idsN = await getStatsHistoryPcIds();
+ is(
+ idsN.length,
+ 1,
+ "There is one pcIds after closing one of two PeerConnections and clearing stats"
+ );
+ pc3.close();
+ // After close for final stats and pc teardown to settle
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ }
+});
+
+add_task(async () => {
+ if (!stats_history_is_enabled()) {
+ return;
+ }
+ const restoreHistRetainPref = set_max_histories_to_retain(7);
+ info("Test that the proper number of pcIds are available");
+ await clearAndCheck();
+ const pc01 = new RTCPeerConnection();
+ const pc02 = new RTCPeerConnection();
+ const pc03 = new RTCPeerConnection();
+ const pc04 = new RTCPeerConnection();
+ const pc05 = new RTCPeerConnection();
+ const pc06 = new RTCPeerConnection();
+ const pc07 = new RTCPeerConnection();
+ const pc08 = new RTCPeerConnection();
+ const pc09 = new RTCPeerConnection();
+ const pc10 = new RTCPeerConnection();
+ const pc11 = new RTCPeerConnection();
+ const pc12 = new RTCPeerConnection();
+ const pc13 = new RTCPeerConnection();
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ {
+ const ids = await getStatsHistoryPcIds();
+ is(ids.length, 13, "There is are 13 PeerConnection Ids for stats history.");
+ }
+ pc01.close();
+ pc02.close();
+ pc03.close();
+ pc04.close();
+ pc05.close();
+ pc06.close();
+ pc07.close();
+ pc08.close();
+ pc09.close();
+ pc10.close();
+ pc11.close();
+ pc12.close();
+ pc13.close();
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 5000));
+ {
+ const ids = await getStatsHistoryPcIds();
+ is(
+ ids.length,
+ 7,
+ "After closing 13 PCs there are no more than the max closed (7) PeerConnection Ids for stats history."
+ );
+ }
+ restoreHistRetainPref();
+ await clearAndCheck();
+});
+
+add_task(async () => {
+ if (!stats_history_is_enabled()) {
+ return;
+ }
+ // If you change this, please check if the setTimeout should be updated.
+ // NOTE: the unit here is _integer_ seconds.
+ const STORAGE_WINDOW_S = 1;
+ const restoreStorageWindowPref =
+ set_history_storage_window_s(STORAGE_WINDOW_S);
+ info("Test that history items are being aged out");
+ await clearAndCheck();
+ const pc = new RTCPeerConnection();
+ // I "don't love" this but we don't have a anything we can await on ... yet.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, STORAGE_WINDOW_S * 2 * 1000));
+ const ids = await getStatsHistoryPcIds();
+ const { reports } = await getStatsHistorySince(ids[0]);
+ const first = reports[0];
+ const last = reports.at(-1);
+ ok(
+ last.timestamp - first.timestamp <= STORAGE_WINDOW_S * 1000,
+ "History reports should be aging out according to the storage window pref"
+ );
+ pc.close();
+ restoreStorageWindowPref();
+ await clearAndCheck();
+});
diff --git a/browser/base/content/test/webrtc/browser_device_controls_menus.js b/browser/base/content/test/webrtc/browser_device_controls_menus.js
new file mode 100644
index 0000000000..3d6602bc5e
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_device_controls_menus.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Regression test for bug 1669801, where sharing a window would
+ * result in a device control menu that showed the wrong count.
+ */
+add_task(async function test_bug_1669801() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ false /* camera */,
+ false /* microphone */,
+ SHARE_WINDOW
+ );
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let menupopup = doc.querySelector("menupopup[type='Screen']");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ menupopup,
+ "popupshown"
+ );
+ menupopup.openPopup(doc.body, {});
+ await popupShownPromise;
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ menupopup,
+ "popuphidden"
+ );
+ menupopup.hidePopup();
+ await popupHiddenPromise;
+ await closeStream();
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media.js b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
new file mode 100644
index 0000000000..3ef88b976d
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -0,0 +1,949 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video",
+ run: async function checkAudioVideo() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio only",
+ run: async function checkAudioOnly() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareMicrophone-notification-icon",
+ "anchored to mic icon"
+ );
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia video only",
+ run: async function checkVideoOnly() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: 'getUserMedia audio+video, user clicks "Don\'t Share"',
+ run: async function checkDontShare() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkNotSharing();
+
+ // Verify that we set 'Temporarily blocked' permissions.
+ let browser = gBrowser.selectedBrowser;
+ let blockedPerms = document.getElementById(
+ "blocked-permissions-container"
+ );
+
+ let { state, scope } = SitePermissions.getForPrincipal(
+ null,
+ "camera",
+ browser
+ );
+ Assert.equal(state, SitePermissions.BLOCK);
+ Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
+ ok(
+ blockedPerms.querySelector(
+ ".blocked-permission-icon.camera-icon[showing=true]"
+ ),
+ "the blocked camera icon is shown"
+ );
+
+ ({ state, scope } = SitePermissions.getForPrincipal(
+ null,
+ "microphone",
+ browser
+ ));
+ Assert.equal(state, SitePermissions.BLOCK);
+ Assert.equal(scope, SitePermissions.SCOPE_TEMPORARY);
+ ok(
+ blockedPerms.querySelector(
+ ".blocked-permission-icon.microphone-icon[showing=true]"
+ ),
+ "the blocked microphone icon is shown"
+ );
+
+ info("requesting devices again to check temporarily blocked permissions");
+ promise = promiseMessage(permissionError);
+ observerPromise1 = expectObserverCalled("getUserMedia:request");
+ observerPromise2 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise3 = expectObserverCalled("recording-window-ended");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise1;
+ await observerPromise2;
+ await observerPromise3;
+ await checkNotSharing();
+
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: stop sharing",
+ run: async function checkStopSharing() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ await stopSharing();
+
+ // the stream is already closed, but this will do some cleanup anyway
+ await closeStream(true);
+
+ // After stop sharing, gUM(audio+camera) causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading the page removes all gUM UI",
+ run: async function checkReloading() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ await reloadAndAssertClosedStreams();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ // After the reload, gUM(audio+camera) causes a prompt.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia prompt: Always/Never Share",
+ run: async function checkRememberCheckbox() {
+ let elt = id => document.getElementById(id);
+
+ async function checkPerm(
+ aRequestAudio,
+ aRequestVideo,
+ aExpectedAudioPerm,
+ aExpectedVideoPerm,
+ aNever
+ ) {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await observerPromise;
+
+ is(
+ elt("webRTC-selectMicrophone").hidden,
+ !aRequestAudio,
+ "microphone selector expected to be " +
+ (aRequestAudio ? "visible" : "hidden")
+ );
+
+ is(
+ elt("webRTC-selectCamera").hidden,
+ !aRequestVideo,
+ "camera selector expected to be " +
+ (aRequestVideo ? "visible" : "hidden")
+ );
+
+ let expected = {};
+ let observerPromises = [];
+ let expectedMessage = aNever ? permissionError : "ok";
+ if (expectedMessage == "ok") {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:allow")
+ );
+ observerPromises.push(
+ expectObserverCalled("recording-device-events")
+ );
+ if (aRequestVideo) {
+ expected.video = true;
+ }
+ if (aRequestAudio) {
+ expected.audio = true;
+ }
+ } else {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:deny")
+ );
+ observerPromises.push(expectObserverCalled("recording-window-ended"));
+ }
+ await promiseMessage(expectedMessage, () => {
+ activateSecondaryAction(aNever ? kActionNever : kActionAlways);
+ });
+ await Promise.all(observerPromises);
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ function checkDevicePermissions(aDevice, aExpected) {
+ let uri = gBrowser.selectedBrowser.documentURI;
+ let devicePerms = PermissionTestUtils.testExactPermission(
+ uri,
+ aDevice
+ );
+ if (aExpected === undefined) {
+ is(
+ devicePerms,
+ Services.perms.UNKNOWN_ACTION,
+ "no " + aDevice + " persistent permissions"
+ );
+ } else {
+ is(
+ devicePerms,
+ aExpected
+ ? Services.perms.ALLOW_ACTION
+ : Services.perms.DENY_ACTION,
+ aDevice + " persistently " + (aExpected ? "allowed" : "denied")
+ );
+ }
+ PermissionTestUtils.remove(uri, aDevice);
+ }
+ checkDevicePermissions("microphone", aExpectedAudioPerm);
+ checkDevicePermissions("camera", aExpectedVideoPerm);
+
+ if (expectedMessage == "ok") {
+ await closeStream();
+ }
+ }
+
+ // 3 cases where the user accepts the device prompt.
+ info("audio+video, user grants, expect both Services.perms set to allow");
+ await checkPerm(true, true, true, true);
+ info(
+ "audio only, user grants, check audio perm set to allow, video perm not set"
+ );
+ await checkPerm(true, false, true, undefined);
+ info(
+ "video only, user grants, check video perm set to allow, audio perm not set"
+ );
+ await checkPerm(false, true, undefined, true);
+
+ // 3 cases where the user rejects the device request by using 'Never Share'.
+ info(
+ "audio only, user denies, expect audio perm set to deny, video not set"
+ );
+ await checkPerm(true, false, false, undefined, true);
+ info(
+ "video only, user denies, expect video perm set to deny, audio perm not set"
+ );
+ await checkPerm(false, true, undefined, false, true);
+ info("audio+video, user denies, expect both Services.perms set to deny");
+ await checkPerm(true, true, false, false, true);
+ },
+ },
+
+ {
+ desc: "getUserMedia without prompt: use persistent permissions",
+ run: async function checkUsePersistentPermissions() {
+ async function usePerm(
+ aAllowAudio,
+ aAllowVideo,
+ aRequestAudio,
+ aRequestVideo,
+ aExpectStream
+ ) {
+ let uri = gBrowser.selectedBrowser.documentURI;
+
+ if (aAllowAudio !== undefined) {
+ PermissionTestUtils.add(
+ uri,
+ "microphone",
+ aAllowAudio
+ ? Services.perms.ALLOW_ACTION
+ : Services.perms.DENY_ACTION
+ );
+ }
+ if (aAllowVideo !== undefined) {
+ PermissionTestUtils.add(
+ uri,
+ "camera",
+ aAllowVideo
+ ? Services.perms.ALLOW_ACTION
+ : Services.perms.DENY_ACTION
+ );
+ }
+
+ if (aExpectStream === undefined) {
+ // Check that we get a prompt.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await observerPromise;
+
+ // Deny the request to cleanup...
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:deny"
+ );
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, "camera", browser);
+ SitePermissions.removeFromPrincipal(null, "microphone", browser);
+ } else {
+ let expectedMessage = aExpectStream ? "ok" : permissionError;
+
+ let observerPromises = [expectObserverCalled("getUserMedia:request")];
+ if (expectedMessage == "ok") {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events")
+ );
+ } else {
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended")
+ );
+ }
+
+ let promise = promiseMessage(expectedMessage);
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await Promise.all(observerPromises);
+
+ if (expectedMessage == "ok") {
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ // Check what's actually shared.
+ let expected = {};
+ if (aAllowVideo && aRequestVideo) {
+ expected.video = true;
+ }
+ if (aAllowAudio && aRequestAudio) {
+ expected.audio = true;
+ }
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " +
+ Object.keys(expected).join(" and ") +
+ " to be shared"
+ );
+
+ await closeStream();
+ }
+ }
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ }
+
+ // Set both permissions identically
+ info("allow audio+video, request audio+video, expect ok (audio+video)");
+ await usePerm(true, true, true, true, true);
+ info("deny audio+video, request audio+video, expect denied");
+ await usePerm(false, false, true, true, false);
+
+ // Allow audio, deny video.
+ info("allow audio, deny video, request audio+video, expect denied");
+ await usePerm(true, false, true, true, false);
+ info("allow audio, deny video, request audio, expect ok (audio)");
+ await usePerm(true, false, true, false, true);
+ info("allow audio, deny video, request video, expect denied");
+ await usePerm(true, false, false, true, false);
+
+ // Deny audio, allow video.
+ info("deny audio, allow video, request audio+video, expect denied");
+ await usePerm(false, true, true, true, false);
+ info("deny audio, allow video, request audio, expect denied");
+ await usePerm(false, true, true, false, false);
+ info("deny audio, allow video, request video, expect ok (video)");
+ await usePerm(false, true, false, true, true);
+
+ // Allow audio, video not set.
+ info("allow audio, request audio+video, expect prompt");
+ await usePerm(true, undefined, true, true, undefined);
+ info("allow audio, request audio, expect ok (audio)");
+ await usePerm(true, undefined, true, false, true);
+ info("allow audio, request video, expect prompt");
+ await usePerm(true, undefined, false, true, undefined);
+
+ // Deny audio, video not set.
+ info("deny audio, request audio+video, expect denied");
+ await usePerm(false, undefined, true, true, false);
+ info("deny audio, request audio, expect denied");
+ await usePerm(false, undefined, true, false, false);
+ info("deny audio, request video, expect prompt");
+ await usePerm(false, undefined, false, true, undefined);
+
+ // Allow video, audio not set.
+ info("allow video, request audio+video, expect prompt");
+ await usePerm(undefined, true, true, true, undefined);
+ info("allow video, request audio, expect prompt");
+ await usePerm(undefined, true, true, false, undefined);
+ info("allow video, request video, expect ok (video)");
+ await usePerm(undefined, true, false, true, true);
+
+ // Deny video, audio not set.
+ info("deny video, request audio+video, expect denied");
+ await usePerm(undefined, false, true, true, false);
+ info("deny video, request audio, expect prompt");
+ await usePerm(undefined, false, true, false, undefined);
+ info("deny video, request video, expect denied");
+ await usePerm(undefined, false, false, true, false);
+ },
+ },
+
+ {
+ desc: "Stop Sharing removes permissions",
+ run: async function checkStopSharingRemovesPermissions() {
+ async function stopAndCheckPerm(
+ aRequestAudio,
+ aRequestVideo,
+ aStopAudio = aRequestAudio,
+ aStopVideo = aRequestVideo
+ ) {
+ let uri = gBrowser.selectedBrowser.documentURI;
+
+ // Initially set both permissions to 'allow'.
+ PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+ // Also set device-specific temporary allows.
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone^myDevice",
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ gBrowser.selectedBrowser,
+ 10000000
+ );
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ "camera^myDevice2",
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ gBrowser.selectedBrowser,
+ 10000000
+ );
+
+ if (aRequestAudio || aRequestVideo) {
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled("getUserMedia:request");
+ let observerPromise2 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise3 = expectObserverCalled(
+ "recording-device-events"
+ );
+ // Start sharing what's been requested.
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(aRequestAudio, aRequestVideo);
+ await promise;
+ await observerPromise1;
+ await observerPromise2;
+ await observerPromise3;
+
+ await indicator;
+ await checkSharingUI(
+ { video: aRequestVideo, audio: aRequestAudio },
+ undefined,
+ undefined,
+ {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ }
+ );
+ await stopSharing(aStopVideo ? "camera" : "microphone");
+ } else {
+ await revokePermission(aStopVideo ? "camera" : "microphone");
+ }
+
+ // Check that permissions have been removed as expected.
+ let audioPerm = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ let audioPermDevice = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone^myDevice",
+ gBrowser.selectedBrowser
+ );
+
+ if (
+ aRequestAudio ||
+ aRequestVideo ||
+ aStopAudio ||
+ (aStopVideo && aRequestAudio)
+ ) {
+ Assert.deepEqual(
+ audioPerm,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "microphone permissions removed"
+ );
+ Assert.deepEqual(
+ audioPermDevice,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "microphone device-specific permissions removed"
+ );
+ } else {
+ Assert.deepEqual(
+ audioPerm,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "microphone permissions untouched"
+ );
+ Assert.deepEqual(
+ audioPermDevice,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "microphone device-specific permissions untouched"
+ );
+ }
+
+ let videoPerm = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ let videoPermDevice = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "camera^myDevice2",
+ gBrowser.selectedBrowser
+ );
+ if (
+ aRequestAudio ||
+ aRequestVideo ||
+ aStopVideo ||
+ (aStopAudio && aRequestVideo)
+ ) {
+ Assert.deepEqual(
+ videoPerm,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "camera permissions removed"
+ );
+ Assert.deepEqual(
+ videoPermDevice,
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "camera device-specific permissions removed"
+ );
+ } else {
+ Assert.deepEqual(
+ videoPerm,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "camera permissions untouched"
+ );
+ Assert.deepEqual(
+ videoPermDevice,
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "camera device-specific permissions untouched"
+ );
+ }
+ await checkNotSharing();
+
+ // Cleanup.
+ await closeStream(true);
+
+ SitePermissions.removeFromPrincipal(
+ gBrowser.contentPrincipal,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ gBrowser.contentPrincipal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ }
+
+ info("request audio+video, stop sharing video resets both");
+ await stopAndCheckPerm(true, true);
+ info("request audio only, stop sharing audio resets both");
+ await stopAndCheckPerm(true, false);
+ info("request video only, stop sharing video resets both");
+ await stopAndCheckPerm(false, true);
+ info("request audio only, stop sharing video resets both");
+ await stopAndCheckPerm(true, false, false, true);
+ info("request video only, stop sharing audio resets both");
+ await stopAndCheckPerm(false, true, true, false);
+ info("request neither, stop audio affects audio only");
+ await stopAndCheckPerm(false, false, true, false);
+ info("request neither, stop video affects video only");
+ await stopAndCheckPerm(false, false, false, true);
+ },
+ },
+
+ {
+ desc: "test showPermissionPanel",
+ run: async function checkShowPermissionPanel() {
+ if (!USING_LEGACY_INDICATOR) {
+ // The indicator only links to the permission panel for the
+ // legacy indicator.
+ return;
+ }
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+
+ ok(permissionPopupHidden(), "permission panel should be hidden");
+ if (IS_MAC) {
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ } else {
+ let win = Services.wm.getMostRecentWindow(
+ "Browser:WebRTCGlobalIndicator"
+ );
+
+ let elt = win.document.getElementById("audioVideoButton");
+ EventUtils.synthesizeMouseAtCenter(elt, {}, win);
+ }
+
+ await TestUtils.waitForCondition(
+ () => !permissionPopupHidden(),
+ "wait for permission panel to open"
+ );
+ ok(!permissionPopupHidden(), "permission panel should be open");
+
+ gPermissionPanel._permissionPopup.hidePopup();
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "'Always Allow' disabled on http pages",
+ run: async function checkNoAlwaysOnHttp() {
+ // Load an http page instead of the https version.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.devices.insecure.enabled", true],
+ ["media.getusermedia.insecure.enabled", true],
+ // explicitly testing an http page, setting
+ // https-first to false.
+ ["dom.security.https_first", false],
+ ],
+ });
+
+ // Disable while loading a new page
+ await disableObserverVerification();
+
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURIString(
+ browser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ browser.documentURI.spec.replace("https://", "http://")
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await enableObserverVerification();
+
+ // Initially set both permissions to 'allow'.
+ let uri = browser.documentURI;
+ PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+
+ // Request devices and expect a prompt despite the saved 'Allow' permission,
+ // because the connection isn't secure.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ // Ensure that checking the 'Remember this decision' checkbox disables
+ // 'Allow'.
+ let notification = PopupNotifications.panel.firstElementChild;
+ let checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+ checkbox.click();
+ ok(checkbox.checked, "checkbox now checked");
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is shown"
+ );
+
+ // Cleanup.
+ await closeStream(true);
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
new file mode 100644
index 0000000000..dd20a672c3
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gTests = [
+ {
+ desc: "device sharing animation on background tabs",
+ run: async function checkAudioVideo() {
+ async function getStreamAndCheckBackgroundAnim(aAudio, aVideo, aSharing) {
+ // Get a stream
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let popupPromise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(aAudio, aVideo);
+ await popupPromise;
+ await observerPromise;
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ let expected = {};
+ if (aVideo) {
+ expected.video = true;
+ }
+ if (aAudio) {
+ expected.audio = true;
+ }
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ // Check the attribute on the tab, and check there's no visible
+ // sharing icon on the tab
+ let tab = gBrowser.selectedTab;
+ is(
+ tab.getAttribute("sharing"),
+ aSharing,
+ "the tab has the attribute to show the " + aSharing + " icon"
+ );
+ let icon = tab.sharingIcon;
+ is(
+ window.getComputedStyle(icon).display,
+ "none",
+ "the animated sharing icon of the tab is hidden"
+ );
+
+ // After selecting a new tab, check the attribute is still there,
+ // and the icon is now visible.
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ BrowserTestUtils.addTab(gBrowser)
+ );
+ is(
+ gBrowser.selectedTab.getAttribute("sharing"),
+ "",
+ "the new tab doesn't have the 'sharing' attribute"
+ );
+ is(
+ tab.getAttribute("sharing"),
+ aSharing,
+ "the tab still has the 'sharing' attribute"
+ );
+ isnot(
+ window.getComputedStyle(icon).display,
+ "none",
+ "the animated sharing icon of the tab is now visible"
+ );
+
+ // Ensure the icon disappears when selecting the tab.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ ok(tab.selected, "the tab with ongoing sharing is selected again");
+ is(
+ window.getComputedStyle(icon).display,
+ "none",
+ "the animated sharing icon is gone after selecting the tab again"
+ );
+
+ // And finally verify the attribute is removed when closing the stream.
+ await closeStream();
+
+ // TODO(Bug 1304997): Fix the race in closeStream() and remove this
+ // TestUtils.waitForCondition().
+ await TestUtils.waitForCondition(() => !tab.getAttribute("sharing"));
+ is(
+ tab.getAttribute("sharing"),
+ "",
+ "the tab no longer has the 'sharing' attribute after closing the stream"
+ );
+ }
+
+ await getStreamAndCheckBackgroundAnim(true, true, "camera");
+ await getStreamAndCheckBackgroundAnim(false, true, "camera");
+ await getStreamAndCheckBackgroundAnim(true, false, "microphone");
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js
new file mode 100644
index 0000000000..3e5ca0668a
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Utility function that should be called after a request for a device
+ * has been made. This function will allow sharing that device, and then
+ * immediately close the stream.
+ */
+async function allowStreamsThenClose() {
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await closeStream();
+}
+
+/**
+ * Tests that if a site requests a particular device by ID, that
+ * the Permission Panel menulist for that device shows only that
+ * device and is disabled.
+ */
+add_task(async function test_get_user_media_by_device_id() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ let audioId = devices
+ .filter(d => d.kind == "audioinput")
+ .map(d => d.deviceId)[0];
+ let videoId = devices
+ .filter(d => d.kind == "videoinput")
+ .map(d => d.deviceId)[0];
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice({ deviceId: { exact: audioId } });
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ await allowStreamsThenClose();
+
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(false, { deviceId: { exact: videoId } });
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ await allowStreamsThenClose();
+
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(
+ { deviceId: { exact: audioId } },
+ { deviceId: { exact: videoId } }
+ );
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+ await allowStreamsThenClose();
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
new file mode 100644
index 0000000000..e6464fd4aa
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const CAMERA_PREF = "permissions.default.camera";
+const MICROPHONE_PREF = "permissions.default.microphone";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video: globally blocking camera",
+ run: async function checkAudioVideo() {
+ Services.prefs.setIntPref(CAMERA_PREF, SitePermissions.BLOCK);
+
+ // Requesting audio+video shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(true, true),
+ ]);
+ await checkNotSharing();
+
+ // Requesting only video shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(false, true),
+ ]);
+ await checkNotSharing();
+
+ // Requesting audio should work.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareMicrophone-notification-icon",
+ "anchored to mic icon"
+ );
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true });
+ await closeStream();
+ Services.prefs.clearUserPref(CAMERA_PREF);
+ },
+ },
+
+ {
+ desc: "getUserMedia video: globally blocking camera + local exception",
+ run: async function checkAudioVideo() {
+ let browser = gBrowser.selectedBrowser;
+ Services.prefs.setIntPref(CAMERA_PREF, SitePermissions.BLOCK);
+ // Overwrite the permission for that URI, requesting video should work again.
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "camera",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Requesting video should work.
+ let indicator = promiseIndicatorWindow();
+ let promises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(false, true);
+ await promise;
+
+ await Promise.all(promises);
+ await indicator;
+ await checkSharingUI({ video: true }, undefined, undefined, {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream();
+
+ PermissionTestUtils.remove(browser.currentURI, "camera");
+ Services.prefs.clearUserPref(CAMERA_PREF);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: globally blocking microphone",
+ run: async function checkAudioVideo() {
+ Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
+
+ // Requesting audio+video shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(true, true),
+ ]);
+ await checkNotSharing();
+
+ // Requesting only audio shouldn't work.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(true),
+ ]);
+
+ // Requesting video should work.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+ await closeStream();
+ Services.prefs.clearUserPref(MICROPHONE_PREF);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio: globally blocking microphone + local exception",
+ run: async function checkAudioVideo() {
+ let browser = gBrowser.selectedBrowser;
+ Services.prefs.setIntPref(MICROPHONE_PREF, SitePermissions.BLOCK);
+ // Overwrite the permission for that URI, requesting video should work again.
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "microphone",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Requesting audio should work.
+ let indicator = promiseIndicatorWindow();
+ let promises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(true);
+ await promise;
+
+ await Promise.all(promises);
+ await indicator;
+ await checkSharingUI({ audio: true }, undefined, undefined, {
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream();
+
+ PermissionTestUtils.remove(browser.currentURI, "microphone");
+ Services.prefs.clearUserPref(MICROPHONE_PREF);
+ },
+ },
+];
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js
new file mode 100644
index 0000000000..0df69bb9da
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js
@@ -0,0 +1,388 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+requestLongerTimeout(2);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const SAME_ORIGIN = "https://example.com";
+const CROSS_ORIGIN = "https://example.org";
+
+const PATH = "/browser/browser/base/content/test/webrtc/get_user_media.html";
+const PATH2 = "/browser/browser/base/content/test/webrtc/get_user_media2.html";
+
+const GRACE_PERIOD_MS = 3000;
+const WAIT_PERIOD_MS = GRACE_PERIOD_MS + 500;
+
+// We're inherently testing timeouts (grace periods)
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
+const perms = SitePermissions;
+
+// These tests focus on camera and microphone, so we define some helpers.
+
+async function prompt(audio, video) {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(audio, video);
+ await promise;
+ await observerPromise;
+ const expectedDeviceSelectorTypes = [
+ audio && "microphone",
+ video && "camera",
+ ].filter(x => x);
+ checkDeviceSelectors(expectedDeviceSelectorTypes);
+}
+
+async function allow(audio, video) {
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ Object.assign({ audio: false, video: false }, await getMediaCaptureState()),
+ { audio, video },
+ `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared`
+ );
+ await indicator;
+ await checkSharingUI({ audio, video });
+}
+
+async function deny(action) {
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(action);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+}
+
+async function noPrompt(audio, video) {
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ let promise = promiseMessage("ok");
+ await promiseRequestDevice(audio, video);
+ await promise;
+ await Promise.all(observerPromises);
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ Assert.deepEqual(
+ Object.assign({ audio: false, video: false }, await getMediaCaptureState()),
+ { audio, video },
+ `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared`
+ );
+ await checkSharingUI({ audio, video });
+}
+
+async function navigate(browser, url) {
+ await disableObserverVerification();
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ await SpecialPowers.spawn(
+ browser,
+ [url],
+ u => (content.document.location = u)
+ );
+ await loaded;
+ await enableObserverVerification();
+}
+
+var gTests = [
+ {
+ desc: "getUserMedia camera+mic survives track.stop but not past grace",
+ run: async function checkAudioVideoGracePastStop() {
+ await prompt(true, true);
+ await allow(true, true);
+
+ info(
+ "After closing all streams, gUM(camera+mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await closeStream();
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+
+ info(
+ "After closing all streams, gUM(mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await closeStream();
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, false);
+
+ info(
+ "After closing all streams, gUM(camera) returns a stream " +
+ "without prompting within grace period."
+ );
+ await closeStream();
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(false, true);
+
+ info("gUM(screen) still causes a prompt.");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+ perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser);
+
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera) causes a prompt.");
+ await prompt(false, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+
+ info("After grace period expires, gUM(mic) causes a prompt.");
+ await prompt(true, false);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic survives page reload but not past grace",
+ run: async function checkAudioVideoGracePastReload(browser) {
+ await prompt(true, true);
+ await allow(true, true);
+ await closeStream();
+
+ await reloadFromContent();
+ info(
+ "After page reload, gUM(camera+mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+ await closeStream();
+
+ await reloadAsUser();
+ info(
+ "After user page reload, gUM(camera+mic) returns a stream " +
+ "without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+
+ info("gUM(screen) still causes a prompt.");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+ perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser);
+
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera) causes a prompt.");
+ await prompt(false, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+
+ info("After grace period expires, gUM(mic) causes a prompt.");
+ await prompt(true, false);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic grace period does not carry over to new tab",
+ run: async function checkAudioVideoGraceEndsNewTab() {
+ await prompt(true, true);
+ await allow(true, true);
+
+ info("Open same page in a new tab");
+ await disableObserverVerification();
+ await BrowserTestUtils.withNewTab(SAME_ORIGIN + PATH, async browser => {
+ info("In new tab, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ });
+ info("Closed tab");
+ await enableObserverVerification();
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic survives navigation but not past grace",
+ run: async function checkAudioVideoGracePastNavigation(browser) {
+ // Use longer grace period in this test to accommodate navigation
+ const LONG_GRACE_PERIOD_MS = 9000;
+ const LONG_WAIT_PERIOD_MS = LONG_GRACE_PERIOD_MS + 500;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.webrtc.deviceGracePeriodTimeoutMs", LONG_GRACE_PERIOD_MS],
+ ],
+ });
+ await prompt(true, true);
+ await allow(true, true);
+ await closeStream();
+
+ info("Navigate to a second same-origin page");
+ await navigate(browser, SAME_ORIGIN + PATH2);
+ info(
+ "After navigating to second same-origin page, gUM(camera+mic) " +
+ "returns a stream without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+ await closeStream();
+
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(LONG_WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ await allow(true, true);
+
+ info("Navigate to a different-origin page");
+ await navigate(browser, CROSS_ORIGIN + PATH2);
+ info(
+ "After navigating to a different-origin page, gUM(camera+mic) " +
+ "causes a prompt."
+ );
+ await prompt(true, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+
+ info("Navigate back to the first page");
+ await navigate(browser, SAME_ORIGIN + PATH);
+ info(
+ "After navigating back to the first page, gUM(camera+mic) " +
+ "returns a stream without prompting within grace period."
+ );
+ await checkNotSharingWithinGracePeriod();
+ await noPrompt(true, true);
+ await closeStream();
+ info("Closed stream. Waiting past grace period.");
+ await checkNotSharingWithinGracePeriod();
+ await wait(LONG_WAIT_PERIOD_MS);
+ await checkNotSharing();
+
+ info("After grace period expires, gUM(camera+mic) causes a prompt.");
+ await prompt(true, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+
+ {
+ desc: "getUserMedia camera+mic grace period cleared on permission block",
+ run: async function checkAudioVideoGraceEndsNewTab(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", 10000]],
+ });
+ info("Set up longer camera grace period.");
+ await prompt(false, true);
+ await allow(false, true);
+ await closeStream();
+ let principal = gBrowser.selectedBrowser.contentPrincipal;
+ info("Request both to get prompted so we can block both.");
+ await prompt(true, true);
+ // We need to remember this decision to set a block permission here and not just 'Not now' the request, see Bug:1609578
+ await deny(kActionNever);
+ // Clear the block so we can prompt again.
+ perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(
+ principal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ info("Revoking permission clears camera grace period.");
+ await prompt(false, true);
+ await deny(kActionDeny);
+ perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser);
+
+ info("Set up longer microphone grace period.");
+ await prompt(true, false);
+ await allow(true, false);
+ await closeStream();
+
+ info("Request both to get prompted so we can block both.");
+ await prompt(true, true);
+ // We need to remember this decision to be able to set a block permission here
+ await deny(kActionNever);
+ perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser);
+ perms.removeFromPrincipal(
+ principal,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ info("Revoking permission clears microphone grace period.");
+ await prompt(true, false);
+ // We need to remember this decision to be able to set a block permission here
+ await deny(kActionNever);
+ perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser);
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", GRACE_PERIOD_MS]],
+ });
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
new file mode 100644
index 0000000000..81e04cebce
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
@@ -0,0 +1,775 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+SpecialPowers.pushPrefEnv({
+ set: [["permissions.delegation.enabled", true]],
+});
+
+// This test has been seen timing out locally in non-opt debug builds.
+requestLongerTimeout(2);
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video",
+ run: async function checkAudioVideo(aBrowser, aSubFrames) {
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["microphone", "camera"]);
+ is(
+ PopupNotifications.panel.firstElementChild.getAttribute("popupid"),
+ "webRTC-shareDevices",
+ "panel using devices icon"
+ );
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+ await closeStream(false, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: stop sharing",
+ run: async function checkStopSharing(aBrowser, aSubFrames) {
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ activateSecondaryAction(kActionAlways);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true }, undefined, undefined, {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+
+ let uri = Services.io.newURI("https://example.com/");
+ is(
+ PermissionTestUtils.testExactPermission(uri, "microphone"),
+ Services.perms.ALLOW_ACTION,
+ "microphone persistently allowed"
+ );
+ is(
+ PermissionTestUtils.testExactPermission(uri, "camera"),
+ Services.perms.ALLOW_ACTION,
+ "camera persistently allowed"
+ );
+
+ await stopSharing("camera", false, frame1ObserveBC);
+
+ // The persistent permissions for the frame should have been removed.
+ is(
+ PermissionTestUtils.testExactPermission(uri, "microphone"),
+ Services.perms.UNKNOWN_ACTION,
+ "microphone not persistently allowed"
+ );
+ is(
+ PermissionTestUtils.testExactPermission(uri, "camera"),
+ Services.perms.UNKNOWN_ACTION,
+ "camera not persistently allowed"
+ );
+
+ // the stream is already closed, but this will do some cleanup anyway
+ await closeStream(true, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: Revoking active devices in frame does not add grace period.",
+ run: async function checkStopSharingGracePeriod(aBrowser, aSubFrames) {
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Stop sharing for camera and test that we stopped sharing.
+ await stopSharing("camera", false, frame1ObserveBC);
+
+ // There shouldn't be any grace period permissions at this point.
+ ok(
+ !SitePermissions.getAllForBrowser(aBrowser).length,
+ "Should not set any permissions."
+ );
+
+ // A new request should result in a prompt.
+ observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let notificationPromise = promisePopupNotificationShown(
+ "webRTC-shareDevices"
+ );
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await notificationPromise;
+ await observerPromise;
+
+ let denyPromise = expectObserverCalled(
+ "getUserMedia:response:deny",
+ 1,
+ frame1ObserveBC
+ );
+ let recordingEndedPromise = expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ frame1ObserveBC
+ );
+ const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await denyPromise;
+ await recordingEndedPromise;
+
+ // Clean up the temporary blocks from the prompt deny.
+ SitePermissions.clearTemporaryBlockPermissions(aBrowser);
+
+ // the stream is already closed, but this will do some cleanup anyway
+ await closeStream(true, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading the frame removes all sharing UI",
+ run: async function checkReloading(aBrowser, aSubFrames) {
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Disable while loading a new page
+ await disableObserverVerification();
+
+ info("reloading the frame");
+ let promises = [
+ expectObserverCalledOnClose(
+ "recording-device-stopped",
+ 1,
+ frame1ObserveBC
+ ),
+ expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ ),
+ expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame1ObserveBC
+ ),
+ ];
+ await promiseReloadFrame(frame1ID, frame1BC);
+ await Promise.all(promises);
+
+ await enableObserverVerification();
+
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading the frame removes prompts",
+ run: async function checkReloadingRemovesPrompts(aBrowser, aSubFrames) {
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ info("reloading the frame");
+ promise = expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseReloadFrame(frame1ID, frame1BC);
+ await promise;
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: with two frames sharing at the same time, sharing UI shows all shared devices",
+ run: async function checkFrameOverridingSharingUI(aBrowser, aSubFrames) {
+ // This tests an edge case discovered in bug 1440356 that works like this
+ // - Share audio and video in iframe 1.
+ // - Share only video in iframe 2.
+ // The WebRTC UI should still show both video and audio indicators.
+
+ let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ );
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = bcsAndFrameIds[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Check that requesting a new device from a different frame
+ // doesn't override sharing UI.
+ let {
+ bc: frame2BC,
+ id: frame2ID,
+ observeBC: frame2ObserveBC,
+ } = bcsAndFrameIds[1];
+
+ observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame2ObserveBC
+ );
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, frame2ID, undefined, frame2BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame2ObserveBC
+ );
+ observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await checkSharingUI({ video: true, audio: true });
+
+ // Check that ending the stream with the other frame
+ // doesn't override sharing UI.
+
+ observerPromise = expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame2ObserveBC
+ );
+ promise = expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+ await promiseReloadFrame(frame2ID, frame2BC);
+ await promise;
+
+ await observerPromise;
+ await checkSharingUI({ video: true, audio: true });
+
+ await closeStream(false, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading a frame updates the sharing UI",
+ run: async function checkUpdateWhenReloading(aBrowser, aSubFrames) {
+ // We'll share only the cam in the first frame, then share both in the
+ // second frame, then reload the second frame. After each step, we'll check
+ // the UI is in the correct state.
+ let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ );
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = bcsAndFrameIds[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: false });
+
+ let {
+ bc: frame2BC,
+ id: frame2ID,
+ observeBC: frame2ObserveBC,
+ } = bcsAndFrameIds[1];
+
+ observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame2ObserveBC
+ );
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame2ID, undefined, frame2BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame2ObserveBC
+ );
+ observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await checkSharingUI({ video: true, audio: true });
+
+ info("reloading the second frame");
+
+ observerPromise1 = expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ frame2ObserveBC
+ );
+ observerPromise2 = expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ frame2ObserveBC
+ );
+ await promiseReloadFrame(frame2ID, frame2BC);
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ video: true, audio: false });
+
+ await closeStream(false, frame1ID, undefined, frame1BC, frame1ObserveBC);
+ await checkNotSharing();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: reloading the top level page removes all sharing UI",
+ run: async function checkReloading(aBrowser, aSubFrames) {
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ await reloadAndAssertClosedStreams();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: closing a window with two frames sharing at the same time, closes the indicator",
+ skipObserverVerification: true,
+ run: async function checkFrameIndicatorClosedUI(aBrowser, aSubFrames) {
+ // This tests a case where the indicator didn't close when audio/video is
+ // shared in two subframes and then the tabs are closed.
+
+ let tabsToRemove = [gBrowser.selectedTab];
+
+ for (let t = 0; t < 2; t++) {
+ let {
+ bc: frame1BC,
+ id: frame1ID,
+ observeBC: frame1ObserveBC,
+ } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ gBrowser.selectedBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // During the second pass, the indicator is already open.
+ let indicator = t == 0 ? promiseIndicatorWindow() : Promise.resolve();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ frame1ObserveBC
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ frame1ObserveBC
+ );
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // The first time around, open another tab with the same uri.
+ // The second time, just open a normal test tab.
+ let uri = t == 0 ? gBrowser.selectedBrowser.currentURI.spec : undefined;
+ tabsToRemove.push(
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, uri)
+ );
+ }
+
+ BrowserTestUtils.removeTab(tabsToRemove[0]);
+ BrowserTestUtils.removeTab(tabsToRemove[1]);
+
+ await checkNotSharing();
+ },
+ },
+];
+
+add_task(async function test_inprocess() {
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_frame.html",
+ subFrames: { frame1: {}, frame2: {} },
+ });
+});
+
+add_task(async function test_outofprocess() {
+ const origin1 = encodeURI("https://test1.example.org");
+ const origin2 = encodeURI("https://www.mozilla.org:443");
+ const query = `origin=${origin1}&origin=${origin2}`;
+ const observe = SpecialPowers.useRemoteSubframes;
+ await runTests(gTests, {
+ relativeURI: `get_user_media_in_frame.html?${query}`,
+ subFrames: { frame1: { observe }, frame2: { observe } },
+ });
+});
+
+add_task(async function test_inprocess_in_outofprocess() {
+ const oopOrigin = encodeURI("https://www.mozilla.org");
+ const sameOrigin = encodeURI("https://example.com");
+ const query = `origin=${oopOrigin}&nested=${sameOrigin}&nested=${sameOrigin}`;
+ await runTests(gTests, {
+ relativeURI: `get_user_media_in_frame.html?${query}`,
+ subFrames: {
+ frame1: {
+ noTest: true,
+ children: { frame1: {}, frame2: {} },
+ },
+ },
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js
new file mode 100644
index 0000000000..8c0b0476f3
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js
@@ -0,0 +1,798 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const PromptResult = {
+ ALLOW: "allow",
+ DENY: "deny",
+ PROMPT: "prompt",
+};
+
+const Perms = Services.perms;
+
+async function promptNoDelegate(aThirdPartyOrgin, audio = true, video = true) {
+ // Persistent allowed first party origin
+ const uri = gBrowser.selectedBrowser.documentURI;
+ if (audio) {
+ PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
+ }
+ if (video) {
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+ }
+
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(audio, video, "frame4");
+ await promise;
+ await observerPromise;
+
+ // The 'Remember this decision' checkbox is hidden.
+ const notification = PopupNotifications.panel.firstElementChild;
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(checkbox.hidden, "checkbox is not visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options.name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ // Check the secondName of the notification should be the third party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .secondName,
+ aThirdPartyOrgin,
+ "Use third party's origin as secondName"
+ );
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ let state = await getMediaCaptureState();
+ is(
+ !!state.audio,
+ audio,
+ `expected microphone to be ${audio ? "" : "not"} shared`
+ );
+ is(
+ !!state.video,
+ video,
+ `expected camera to be ${video ? "" : "not"} shared`
+ );
+ await indicator;
+ await checkSharingUI({ audio, video }, undefined, undefined, {
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+
+ // Cleanup.
+ await closeStream(false, "frame4");
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+}
+
+async function promptNoDelegateScreenSharing(aThirdPartyOrgin) {
+ // Persistent allow screen sharing
+ const uri = gBrowser.selectedBrowser.documentURI;
+ PermissionTestUtils.add(uri, "screen", Services.perms.ALLOW_ACTION);
+
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, "frame4", "screen");
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["screen"]);
+ const notification = PopupNotifications.panel.firstElementChild;
+
+ // The 'Remember this decision' checkbox is hidden.
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ ok(!checkbox.hidden, "Notification silencing checkbox is visible");
+ } else {
+ ok(checkbox.hidden, "checkbox is not visible");
+ }
+
+ ok(!checkbox.checked, "checkbox not checked");
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options.name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ // Check the secondName of the notification should be the third party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .secondName,
+ aThirdPartyOrgin,
+ "Use third party's origin as secondName"
+ );
+
+ const menulist = document.getElementById("webRTC-selectWindow-menulist");
+ const count = menulist.itemCount;
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(!notification.button.disabled, "Allow button is enabled");
+
+ const indicator = promiseIndicatorWindow();
+ const observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ const observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" }, undefined, undefined, {
+ screen: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream(false, "frame4");
+
+ PermissionTestUtils.remove(uri, "screen");
+}
+
+var gTests = [
+ {
+ desc: "'Always Allow' enabled on third party pages, when origin is explicitly allowed",
+ run: async function checkNoAlwaysOnThirdParty() {
+ // Initially set both permissions to 'prompt'.
+ const uri = gBrowser.selectedBrowser.documentURI;
+ PermissionTestUtils.add(uri, "microphone", Services.perms.PROMPT_ACTION);
+ PermissionTestUtils.add(uri, "camera", Services.perms.PROMPT_ACTION);
+
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // The 'Remember this decision' checkbox is visible.
+ const notification = PopupNotifications.panel.firstElementChild;
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.hidden, "checkbox is visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options.name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ const indicator = promiseIndicatorWindow();
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Cleanup.
+ await closeStream(false, "frame1");
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ },
+ },
+ {
+ desc: "'Always Allow' disabled when sharing screen in third party iframes, when origin is explicitly allowed",
+ run: async function checkScreenSharing() {
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, "frame1", "screen");
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["screen"]);
+ const notification = PopupNotifications.panel.firstElementChild;
+
+ // The 'Remember this decision' checkbox is visible.
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.hidden, "checkbox is visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ const menulist = document.getElementById("webRTC-selectWindow-menulist");
+ const count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ const noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(!notification.button.disabled, "Allow button is enabled");
+
+ const indicator = promiseIndicatorWindow();
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+ await closeStream(false, "frame1");
+ },
+ },
+
+ {
+ desc: "getUserMedia use persistent permissions from first party",
+ run: async function checkUsePersistentPermissionsFirstParty() {
+ async function checkPersistentPermission(
+ aPermission,
+ aRequestType,
+ aIframeId,
+ aExpect
+ ) {
+ info(
+ `Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
+ );
+ const uri = gBrowser.selectedBrowser.documentURI;
+ // Persistent allow/deny for first party uri
+ PermissionTestUtils.add(uri, aRequestType, aPermission);
+
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+ if (aExpect == PromptResult.PROMPT) {
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:deny"
+ );
+ const observerPromise2 = expectObserverCalled(
+ "recording-window-ended"
+ );
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(audio, video, aIframeId, screen);
+ await promise;
+ await observerPromise;
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ // Deny the request to cleanup...
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ } else if (aExpect == PromptResult.ALLOW) {
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled(
+ "recording-device-events"
+ );
+ const promise = promiseMessage("ok");
+ await promiseRequestDevice(audio, video, aIframeId, screen);
+ await promise;
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ let expected = {};
+ if (audio) {
+ expected.audio = audio;
+ }
+ if (video) {
+ expected.video = video;
+ }
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ await closeStream(false, aIframeId);
+ } else if (aExpect == PromptResult.DENY) {
+ const promises = [];
+ // frame3 disallows by feature Permissions Policy before request.
+ if (aIframeId != "frame3") {
+ promises.push(
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny")
+ );
+ }
+ promises.push(
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(audio, video, aIframeId, screen)
+ );
+ await Promise.all(promises);
+ }
+
+ PermissionTestUtils.remove(uri, aRequestType);
+ }
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "camera",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "camera",
+ "frame1",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "camera",
+ "frame1",
+ PromptResult.ALLOW
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "microphone",
+ "frame1",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ "frame1",
+ PromptResult.ALLOW
+ );
+
+ // Wildcard attributes still get delegation when their src is unchanged.
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "camera",
+ "frame4",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "camera",
+ "frame4",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "camera",
+ "frame4",
+ PromptResult.ALLOW
+ );
+
+ // Wildcard attributes still get delegation when their src is unchanged.
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ "frame4",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "microphone",
+ "frame4",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ "frame4",
+ PromptResult.ALLOW
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "screen",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "screen",
+ "frame1",
+ PromptResult.DENY
+ );
+ // Always prompt screen sharing
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "screen",
+ "frame1",
+ PromptResult.PROMPT
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "screen",
+ "frame4",
+ PromptResult.PROMPT
+ );
+
+ // Denied by default if allow is not defined
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "camera",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "camera",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "camera",
+ "frame3",
+ PromptResult.DENY
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "microphone",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ "frame3",
+ PromptResult.DENY
+ );
+
+ await checkPersistentPermission(
+ Perms.PROMPT_ACTION,
+ "screen",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.DENY_ACTION,
+ "screen",
+ "frame3",
+ PromptResult.DENY
+ );
+ await checkPersistentPermission(
+ Perms.ALLOW_ACTION,
+ "screen",
+ "frame3",
+ PromptResult.DENY
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia use temporary blocked permissions from first party",
+ run: async function checkUseTempPermissionsBlockFirstParty() {
+ async function checkTempPermission(aRequestType) {
+ let browser = gBrowser.selectedBrowser;
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:deny"
+ );
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+
+ await promiseRequestDevice(audio, video, null, screen);
+ await promise;
+ await observerPromise;
+
+ // Temporarily grant/deny from top level
+ // Only need to check allow and deny temporary permissions
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ promise = promiseMessage(permissionError);
+ await promiseRequestDevice(audio, video, "frame1", screen);
+ await promise;
+
+ await observerPromise;
+ await observerPromise1;
+ await observerPromise2;
+
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ }
+
+ // At the moment we only save temporary deny
+ await checkTempPermission("camera");
+ await checkTempPermission("microphone");
+ await checkTempPermission("screen");
+ },
+ },
+ {
+ desc: "Don't reprompt while actively sharing in maybe unsafe permission delegation",
+ run: async function checkNoRepromptNoDelegate() {
+ // Change location to ensure that we're treated as potentially unsafe.
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+
+ // Check that we get a prompt.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame4");
+ await promise;
+ await observerPromise;
+
+ // Check the secondName of the notification should be the third party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .secondName,
+ "test2.example.com",
+ "Use third party's origin as secondName"
+ );
+
+ const notification = PopupNotifications.panel.firstElementChild;
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+
+ let state = await getMediaCaptureState();
+ is(!!state.audio, true, "expected microphone to be shared");
+ is(!!state.video, true, "expected camera to be shared");
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Check that we now don't get a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(true, true, "frame4");
+ await promise;
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ state = await getMediaCaptureState();
+ is(!!state.audio, true, "expected microphone to be shared");
+ is(!!state.video, true, "expected camera to be shared");
+ await checkSharingUI({ audio: true, video: true });
+
+ // Cleanup.
+ await closeStream(false, "frame4");
+ },
+ },
+ {
+ desc: "Change location, prompt and display both first party and third party origin in maybe unsafe permission delegation",
+ run: async function checkPromptNoDelegateChangeLoxation() {
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+ await promptNoDelegate("test2.example.com");
+ },
+ },
+ {
+ desc: "Change location, prompt and display both first party and third party origin when sharing screen in unsafe permission delegation",
+ run: async function checkPromptNoDelegateScreenSharingChangeLocation() {
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+ await promptNoDelegateScreenSharing("test2.example.com");
+ },
+ },
+ {
+ desc: "Prompt and display both first party and third party origin and temporary deny in frame does not change permission scope",
+ skipObserverVerification: true,
+ run: async function checkPromptBothOriginsTempDenyFrame() {
+ // Change location to ensure that we're treated as potentially unsafe.
+ await promiseChangeLocationFrame(
+ "frame4",
+ "https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
+ );
+
+ // Persistent allowed first party origin
+ let browser = gBrowser.selectedBrowser;
+ let uri = gBrowser.selectedBrowser.documentURI;
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ // Ensure that checking the 'Remember this decision'
+ let notification = PopupNotifications.panel.firstElementChild;
+ let checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+ checkbox.click();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true }, undefined, undefined, {
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+ await closeStream(true);
+
+ // Check that we get a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame4");
+ await promise;
+ await observerPromise;
+
+ // The 'Remember this decision' checkbox is hidden.
+ notification = PopupNotifications.panel.firstElementChild;
+ checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(checkbox.hidden, "checkbox is not visible");
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+
+ // Make sure we are not changing the scope and state of persistent
+ // permission
+ let { state, scope } = SitePermissions.getForPrincipal(
+ principal,
+ "camera",
+ browser
+ );
+ Assert.equal(state, SitePermissions.ALLOW);
+ Assert.equal(scope, SitePermissions.SCOPE_PERSISTENT);
+
+ ({ state, scope } = SitePermissions.getForPrincipal(
+ principal,
+ "microphone",
+ browser
+ ));
+ Assert.equal(state, SitePermissions.ALLOW);
+ Assert.equal(scope, SitePermissions.SCOPE_PERSISTENT);
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ });
+
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_xorigin_frame.html",
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js
new file mode 100644
index 0000000000..ad398994f0
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js
@@ -0,0 +1,251 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const PromptResult = {
+ ALLOW: "allow",
+ DENY: "deny",
+ PROMPT: "prompt",
+};
+
+const Perms = Services.perms;
+
+function expectObserverCalledAncestor(aTopic, browsingContext) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic);
+ }
+
+ return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic);
+}
+
+function enableObserverVerificationAncestor(browsingContext) {
+ // Skip these checks in single process mode as it isn't worth implementing it.
+ if (!gMultiProcessBrowser) {
+ return Promise.resolve();
+ }
+
+ return BrowserTestUtils.startObservingTopics(browsingContext, observerTopics);
+}
+
+function disableObserverVerificationAncestor(browsingContextt) {
+ if (!gMultiProcessBrowser) {
+ return Promise.resolve();
+ }
+
+ return BrowserTestUtils.stopObservingTopics(
+ browsingContextt,
+ observerTopics
+ ).catch(reason => {
+ ok(false, "Failed " + reason);
+ });
+}
+
+function promiseRequestDeviceAncestor(
+ aRequestAudio,
+ aRequestVideo,
+ aType,
+ aBrowser,
+ aBadDevice = false
+) {
+ info("requesting devices");
+ return SpecialPowers.spawn(
+ aBrowser,
+ [{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
+ async function (args) {
+ let global =
+ content.wrappedJSObject.document.getElementById("frame4").contentWindow;
+ global.requestDevice(
+ args.aRequestAudio,
+ args.aRequestVideo,
+ args.aType,
+ args.aBadDevice
+ );
+ }
+ );
+}
+
+async function closeStreamAncestor(browser) {
+ let observerPromises = [];
+ observerPromises.push(
+ expectObserverCalledAncestor("recording-device-events", browser)
+ );
+ observerPromises.push(
+ expectObserverCalledAncestor("recording-window-ended", browser)
+ );
+
+ info("closing the stream");
+ await SpecialPowers.spawn(browser, [], async () => {
+ let global =
+ content.wrappedJSObject.document.getElementById("frame4").contentWindow;
+ global.closeStream();
+ });
+
+ await Promise.all(observerPromises);
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+var gTests = [
+ {
+ desc: "getUserMedia use persistent permissions from first party if third party is explicitly trusted",
+ skipObserverVerification: true,
+ run: async function checkPermissionsAncestorChain() {
+ async function checkPermission(aPermission, aRequestType, aExpect) {
+ info(
+ `Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
+ );
+ const uri = gBrowser.selectedBrowser.documentURI;
+ // Persistent allow/deny for first party uri
+ PermissionTestUtils.add(uri, aRequestType, aPermission);
+
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+ const iframeAncestor = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.document.getElementById("frameAncestor")
+ .browsingContext;
+ }
+ );
+
+ if (aExpect == PromptResult.PROMPT) {
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalledAncestor(
+ "getUserMedia:request",
+ iframeAncestor
+ );
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await promise;
+ await observerPromise;
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .name,
+ uri.host,
+ "Use first party's origin"
+ );
+ const observerPromise1 = expectObserverCalledAncestor(
+ "getUserMedia:response:deny",
+ iframeAncestor
+ );
+ const observerPromise2 = expectObserverCalledAncestor(
+ "recording-window-ended",
+ iframeAncestor
+ );
+ // Deny the request to cleanup...
+ activateSecondaryAction(kActionDeny);
+ await observerPromise1;
+ await observerPromise2;
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ } else if (aExpect == PromptResult.ALLOW) {
+ const observerPromise = expectObserverCalledAncestor(
+ "getUserMedia:request",
+ iframeAncestor
+ );
+ const observerPromise1 = expectObserverCalledAncestor(
+ "getUserMedia:response:allow",
+ iframeAncestor
+ );
+ const observerPromise2 = expectObserverCalledAncestor(
+ "recording-device-events",
+ iframeAncestor
+ );
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ let expected = {};
+ if (audio) {
+ expected.audio = audio;
+ }
+ if (video) {
+ expected.video = video;
+ }
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ await closeStreamAncestor(iframeAncestor);
+ } else if (aExpect == PromptResult.DENY) {
+ const observerPromise = expectObserverCalledAncestor(
+ "recording-window-ended",
+ iframeAncestor
+ );
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await observerPromise;
+ }
+
+ PermissionTestUtils.remove(uri, aRequestType);
+ }
+
+ await checkPermission(Perms.PROMPT_ACTION, "camera", PromptResult.PROMPT);
+ await checkPermission(Perms.DENY_ACTION, "camera", PromptResult.DENY);
+ await checkPermission(Perms.ALLOW_ACTION, "camera", PromptResult.ALLOW);
+
+ await checkPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ PromptResult.PROMPT
+ );
+ await checkPermission(Perms.DENY_ACTION, "microphone", PromptResult.DENY);
+ await checkPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ PromptResult.ALLOW
+ );
+
+ await checkPermission(Perms.PROMPT_ACTION, "screen", PromptResult.PROMPT);
+ await checkPermission(Perms.DENY_ACTION, "screen", PromptResult.DENY);
+ // Always prompt screen sharing
+ await checkPermission(Perms.ALLOW_ACTION, "screen", PromptResult.PROMPT);
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ });
+
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_xorigin_frame_ancestor.html",
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js
new file mode 100644
index 0000000000..fa88b5d030
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js
@@ -0,0 +1,517 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gTests = [
+ {
+ desc: "getUserMedia audio in a first process + video in a second process",
+ // These tests call enableObserverVerification manually on a second tab, so
+ // don't add listeners to the first tab.
+ skipObserverVerification: true,
+ run: async function checkMultiProcess() {
+ // The main purpose of this test is to ensure webrtc sharing indicators
+ // work with multiple content processes, but it makes sense to run this
+ // test without e10s too to ensure using webrtc devices in two different
+ // tabs is handled correctly.
+
+ // Request audio in the first tab.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["microphone"]);
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator shown"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, true).length,
+ 1,
+ "1 active audio stream"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ // If we have reached the max process count already, increase it to ensure
+ // our new tab can have its own content process.
+ let childCount = Services.ppmm.childCount;
+ let maxContentProcess = Services.prefs.getIntPref("dom.ipc.processCount");
+ // The first check is because if we are on a branch where e10s-multi is
+ // disabled, we want to keep testing e10s with a single content process.
+ // The + 1 is because ppmm.childCount also counts the chrome process
+ // (which also runs process scripts).
+ if (maxContentProcess > 1 && childCount == maxContentProcess + 1) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", childCount]],
+ });
+ }
+
+ // Open a new tab with a different hostname.
+ let url = gBrowser.currentURI.spec.replace(
+ "https://example.com/",
+ "http://127.0.0.1:8888/"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await enableObserverVerification();
+
+ // Request video.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ video: true }, window, {
+ audio: true,
+ video: true,
+ });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator shown"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, true).length,
+ 1,
+ "1 active audio stream"
+ );
+ is(webrtcUI.getActiveStreams(true).length, 1, "1 active video stream");
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 2,
+ "2 active streams"
+ );
+
+ info("removing the second tab");
+
+ await disableObserverVerification();
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Check that we still show the sharing indicators for the first tab's stream.
+ await Promise.all([
+ TestUtils.waitForCondition(() => !webrtcUI.showCameraIndicator),
+ TestUtils.waitForCondition(
+ () => webrtcUI.getActiveStreams(true, true, true).length == 1
+ ),
+ ]);
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator shown"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, true).length,
+ 1,
+ "1 active audio stream"
+ );
+
+ await checkSharingUI({ audio: true });
+
+ // Close the first tab's stream and verify that all indicators are removed.
+ await closeStream(false, null, true);
+
+ ok(
+ !webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 0,
+ "0 active streams"
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia camera in a first process + camera in a second process",
+ skipObserverVerification: true,
+ run: async function checkMultiProcessCamera() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ // Request camera in the first tab.
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(webrtcUI.getActiveStreams(true).length, 1, "1 active camera stream");
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ // If we have reached the max process count already, increase it to ensure
+ // our new tab can have its own content process.
+ let childCount = Services.ppmm.childCount;
+ let maxContentProcess = Services.prefs.getIntPref("dom.ipc.processCount");
+ // The first check is because if we are on a branch where e10s-multi is
+ // disabled, we want to keep testing e10s with a single content process.
+ // The + 1 is because ppmm.childCount also counts the chrome process
+ // (which also runs process scripts).
+ if (maxContentProcess > 1 && childCount == maxContentProcess + 1) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", childCount]],
+ });
+ }
+
+ // Open a new tab with a different hostname.
+ let url = gBrowser.currentURI.spec.replace(
+ "https://example.com/",
+ "http://127.0.0.1:8888/"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await enableObserverVerification();
+
+ // Request camera in the second tab.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ video: true }, window, { video: true });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(webrtcUI.getActiveStreams(true).length, 2, "2 active camera streams");
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 2,
+ "2 active streams"
+ );
+
+ await disableObserverVerification();
+
+ info("removing the second tab");
+ BrowserTestUtils.removeTab(tab);
+
+ // Check that we still show the sharing indicators for the first tab's stream.
+ await Promise.all([
+ TestUtils.waitForCondition(() => webrtcUI.showCameraIndicator),
+ TestUtils.waitForCondition(
+ () => webrtcUI.getActiveStreams(true).length == 1
+ ),
+ ]);
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator shown"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ await checkSharingUI({ video: true });
+
+ // Close the first tab's stream and verify that all indicators are removed.
+ await closeStream(false, null, true);
+
+ ok(
+ !webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 0,
+ "0 active streams"
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia screen sharing in a first process + screen sharing in a second process",
+ skipObserverVerification: true,
+ run: async function checkMultiProcessScreen() {
+ // Request screen sharing in the first tab.
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ // Select the last screen so that we can have a stream.
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showScreenSharingIndicator,
+ "webrtcUI wants the screen sharing indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, false, true).length,
+ 1,
+ "1 active screen sharing stream"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 1,
+ "1 active stream"
+ );
+
+ // If we have reached the max process count already, increase it to ensure
+ // our new tab can have its own content process.
+ let childCount = Services.ppmm.childCount;
+ let maxContentProcess = Services.prefs.getIntPref("dom.ipc.processCount");
+ // The first check is because if we are on a branch where e10s-multi is
+ // disabled, we want to keep testing e10s with a single content process.
+ // The + 1 is because ppmm.childCount also counts the chrome process
+ // (which also runs process scripts).
+ if (maxContentProcess > 1 && childCount == maxContentProcess + 1) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", childCount]],
+ });
+ }
+
+ // Open a new tab with a different hostname.
+ let url = gBrowser.currentURI.spec.replace(
+ "https://example.com/",
+ "https://example.com/"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await enableObserverVerification();
+
+ // Request screen sharing in the second tab.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ // Select the last screen so that we can have a stream.
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkSharingUI({ screen: "Screen" }, window, { screen: "Screen" });
+
+ ok(
+ webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator shown"
+ );
+ ok(
+ webrtcUI.showScreenSharingIndicator,
+ "webrtcUI wants the screen sharing indicator shown"
+ );
+ ok(
+ !webrtcUI.showCameraIndicator,
+ "webrtcUI wants the camera indicator hidden"
+ );
+ ok(
+ !webrtcUI.showMicrophoneIndicator,
+ "webrtcUI wants the mic indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(false, false, true).length,
+ 2,
+ "2 active desktop sharing streams"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 2,
+ "2 active streams"
+ );
+
+ await disableObserverVerification();
+
+ info("removing the second tab");
+ BrowserTestUtils.removeTab(tab);
+
+ await TestUtils.waitForCondition(
+ () => webrtcUI.getActiveStreams(true, true, true).length == 1
+ );
+
+ // Close the first tab's stream and verify that all indicators are removed.
+ await closeStream(false, null, true);
+
+ ok(
+ !webrtcUI.showGlobalIndicator,
+ "webrtcUI wants the global indicator hidden"
+ );
+ is(
+ webrtcUI.getActiveStreams(true, true, true).length,
+ 0,
+ "0 active streams"
+ );
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js
new file mode 100644
index 0000000000..27271c2a45
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js
@@ -0,0 +1,999 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function setCameraMuted(mute) {
+ return sendObserverNotification(
+ mute ? "getUserMedia:muteVideo" : "getUserMedia:unmuteVideo"
+ );
+}
+
+function setMicrophoneMuted(mute) {
+ return sendObserverNotification(
+ mute ? "getUserMedia:muteAudio" : "getUserMedia:unmuteAudio"
+ );
+}
+
+function sendObserverNotification(topic) {
+ const windowId = gBrowser.selectedBrowser.innerWindowID;
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ topic, windowId }],
+ function (args) {
+ Services.obs.notifyObservers(
+ content.window,
+ args.topic,
+ JSON.stringify(args.windowId)
+ );
+ }
+ );
+}
+
+function setTrackEnabled(audio, video) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ audio, video }],
+ function (args) {
+ let stream = content.wrappedJSObject.gStreams[0];
+ if (args.audio != null) {
+ stream.getAudioTracks()[0].enabled = args.audio;
+ }
+ if (args.video != null) {
+ stream.getVideoTracks()[0].enabled = args.video;
+ }
+ }
+ );
+}
+
+async function getVideoTrackMuted() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gStreams[0].getVideoTracks()[0].muted
+ );
+}
+
+async function getVideoTrackEvents() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gVideoEvents
+ );
+}
+
+async function getAudioTrackMuted() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gStreams[0].getAudioTracks()[0].muted
+ );
+}
+
+async function getAudioTrackEvents() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.wrappedJSObject.gAudioEvents
+ );
+}
+
+function cloneTracks(audio, video) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ audio, video }],
+ function (args) {
+ if (!content.wrappedJSObject.gClones) {
+ content.wrappedJSObject.gClones = [];
+ }
+ let clones = content.wrappedJSObject.gClones;
+ let stream = content.wrappedJSObject.gStreams[0];
+ if (args.audio != null) {
+ clones.push(stream.getAudioTracks()[0].clone());
+ }
+ if (args.video != null) {
+ clones.push(stream.getVideoTracks()[0].clone());
+ }
+ }
+ );
+}
+
+function stopClonedTracks(audio, video) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ audio, video }],
+ function (args) {
+ let clones = content.wrappedJSObject.gClones || [];
+ if (args.audio != null) {
+ clones.filter(t => t.kind == "audio").forEach(t => t.stop());
+ }
+ if (args.video != null) {
+ clones.filter(t => t.kind == "video").forEach(t => t.stop());
+ }
+ let liveClones = clones.filter(t => t.readyState == "live");
+ if (!liveClones.length) {
+ delete content.wrappedJSObject.gClones;
+ } else {
+ content.wrappedJSObject.gClones = liveClones;
+ }
+ }
+ );
+}
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video: disabling the stream shows the paused indicator",
+ run: async function checkDisabled() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Disable both audio and video.
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setTrackEnabled(false, false);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+
+ // Enable only audio again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Enable video again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: disabling the original tracks and stopping enabled clones shows the paused indicator",
+ run: async function checkDisabledAfterCloneStop() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Clone audio and video, their state will be enabled
+ await cloneTracks(true, true);
+
+ // Disable both audio and video.
+ await setTrackEnabled(false, false);
+
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+
+ // Stop the clones. This should disable the sharing indicators.
+ await stopClonedTracks(true, true);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED &&
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "video and audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+
+ // Enable only audio again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // Enable video again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia screen: disabling the stream shows the paused indicator",
+ run: async function checkScreenDisabled() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, false);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.screen == "ScreenPaused",
+ "screen should be disabled"
+ );
+ await observerPromise;
+ await checkSharingUI({ screen: "ScreenPaused" }, window, {
+ screen: "Screen",
+ });
+
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () => window.gPermissionPanel._sharingState.webRTC.screen == "Screen",
+ "screen should be enabled"
+ );
+ await observerPromise;
+ await checkSharingUI({ screen: "Screen" });
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: muting the camera shows the muted indicator",
+ run: async function checkCameraMuted() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track starts unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ [],
+ "no video track events fired yet"
+ );
+
+ // Mute camera.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setCameraMuted(true);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be muted"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only camera as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is muted");
+ Assert.deepEqual(await getVideoTrackEvents(), ["mute"], "mute fired");
+
+ // Unmute video again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setCameraMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track is unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: muting the microphone shows the muted indicator",
+ run: async function checkMicrophoneMuted() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track starts unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ [],
+ "no audio track events fired yet"
+ );
+
+ // Mute microphone.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setMicrophoneMuted(true);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be muted"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only microphone as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is muted");
+ Assert.deepEqual(await getAudioTrackEvents(), ["mute"], "mute fired");
+
+ // Unmute audio again.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setMicrophoneMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track is unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: disabling & muting camera in combination",
+ // Test the following combinations of disabling and muting camera:
+ // 1. Disable video track only.
+ // 2. Mute camera & disable audio (to have a condition to wait for)
+ // 3. Enable both audio and video tracks (only audio should flow).
+ // 4. Unmute camera again (video should flow).
+ // 5. Mute camera & disable both tracks.
+ // 6. Unmute camera & enable audio (only audio should flow)
+ // 7. Enable video track again (video should flow).
+ run: async function checkDisabledMutedCombination() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // 1. Disable video track only.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, false);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track still unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ [],
+ "no video track events fired yet"
+ );
+
+ // 2. Mute camera & disable audio (to have a condition to wait for)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setCameraMuted(true);
+ await setTrackEnabled(false, null);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is muted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute"],
+ "mute is still fired even though track was disabled"
+ );
+
+ // 3. Enable both audio and video tracks (only audio should flow).
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setTrackEnabled(true, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only audio as enabled, as video is muted.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is still muted");
+ Assert.deepEqual(await getVideoTrackEvents(), ["mute"], "no new events");
+
+ // 4. Unmute camera again (video should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setCameraMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track is unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+
+ // 5. Mute camera & disable both tracks.
+ observerPromise = expectObserverCalled("recording-device-events", 3);
+ await setCameraMuted(true);
+ await setTrackEnabled(false, false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "video should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getVideoTrackMuted(), true, "video track is muted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute", "mute"],
+ "mute fired afain"
+ );
+
+ // 6. Unmute camera & enable audio (only audio should flow)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setCameraMuted(false);
+ await setTrackEnabled(true, null);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // Only audio should show as running, as video track is still disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track is unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "unmute fired even though track is disabled"
+ );
+
+ // 7. Enable video track again (video should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as running again.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getVideoTrackMuted(), false, "video track remains unmuted");
+ Assert.deepEqual(
+ await getVideoTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "no new events fired"
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+video: disabling & muting microphone in combination",
+ // Test the following combinations of disabling and muting microphone:
+ // 1. Disable audio track only.
+ // 2. Mute microphone & disable video (to have a condition to wait for)
+ // 3. Enable both audio and video tracks (only video should flow).
+ // 4. Unmute microphone again (audio should flow).
+ // 5. Mute microphone & disable both tracks.
+ // 6. Unmute microphone & enable video (only video should flow)
+ // 7. Enable audio track again (audio should flow).
+ run: async function checkDisabledMutedCombination() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+
+ // 1. Disable audio track only.
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(false, null);
+
+ // Wait for capture state to propagate to the UI asynchronously.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only audio as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track still unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ [],
+ "no audio track events fired yet"
+ );
+
+ // 2. Mute microphone & disable video (to have a condition to wait for)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setMicrophoneMuted(true);
+ await setTrackEnabled(null, false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED,
+ "camera should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is muted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute"],
+ "mute is still fired even though track was disabled"
+ );
+
+ // 3. Enable both audio and video tracks (only video should flow).
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setTrackEnabled(true, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show only video as enabled, as audio is muted.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is still muted");
+ Assert.deepEqual(await getAudioTrackEvents(), ["mute"], "no new events");
+
+ // 4. Unmute microphone again (audio should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setMicrophoneMuted(false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // Both streams should show as running.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track is unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute"],
+ "unmute fired"
+ );
+
+ // 5. Mute microphone & disable both tracks.
+ observerPromise = expectObserverCalled("recording-device-events", 3);
+ await setMicrophoneMuted(true);
+ await setTrackEnabled(false, false);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_DISABLED,
+ "audio should be disabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_DISABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), true, "audio track is muted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute", "mute"],
+ "mute fired again"
+ );
+
+ // 6. Unmute microphone & enable video (only video should flow)
+ observerPromise = expectObserverCalled("recording-device-events", 2);
+ await setMicrophoneMuted(false);
+ await setTrackEnabled(null, true);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.camera ==
+ STATE_CAPTURE_ENABLED,
+ "video should be enabled"
+ );
+
+ await observerPromise;
+
+ // Only video should show as running, as audio track is still disabled.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_DISABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track is unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "unmute fired even though track is disabled"
+ );
+
+ // 7. Enable audio track again (audio should flow).
+ observerPromise = expectObserverCalled("recording-device-events");
+ await setTrackEnabled(true, null);
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ window.gPermissionPanel._sharingState.webRTC.microphone ==
+ STATE_CAPTURE_ENABLED,
+ "audio should be enabled"
+ );
+
+ await observerPromise;
+
+ // The identity UI should show both as running again.
+ await checkSharingUI({
+ video: STATE_CAPTURE_ENABLED,
+ audio: STATE_CAPTURE_ENABLED,
+ });
+ is(await getAudioTrackMuted(), false, "audio track remains unmuted");
+ Assert.deepEqual(
+ await getAudioTrackEvents(),
+ ["mute", "unmute", "mute", "unmute"],
+ "no new events fired"
+ );
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.getusermedia.camera.off_while_disabled.delay_ms", 0],
+ ["media.getusermedia.microphone.off_while_disabled.delay_ms", 0],
+ ],
+ });
+
+ SimpleTest.requestCompleteLog();
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js
new file mode 100644
index 0000000000..1c90787640
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js
@@ -0,0 +1,383 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const badDeviceError =
+ "error: NotReadableError: Failed to allocate videosource";
+
+var gTests = [
+ {
+ desc: "test 'Not now' label queueing audio twice behind allow video",
+ run: async function testQueuingDenyAudioBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(true, false);
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ checkDeviceSelectors(["camera"]);
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Block",
+ "We offer Block because of no active camera/mic device"
+ );
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: false, video: true });
+
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await observerPromise;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(["microphone"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option because of an allowed camera/mic device"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option again because of an allowed camera/mic device"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+
+ // Clean up the active camera in the activePerms map
+ webrtcUI.activePerms.delete(gBrowser.selectedBrowser.outerWindowID);
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "test 'Not now'/'Block' label queueing microphone behind screen behind allow camera",
+ run: async function testQueuingAudioAndScreenBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promiseRequestDevice(false, true, null, "screen");
+ await promiseRequestDevice(true, false);
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ checkDeviceSelectors(["camera"]);
+ await observerPromise;
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Block",
+ "We offer Block because of no active camera/mic device"
+ );
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+ await checkSharingUI({ audio: false, video: true });
+
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await observerPromise;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(["screen"]);
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option because we are asking for screen"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ is(
+ PopupNotifications.panel.firstElementChild.secondaryButton.label,
+ "Not now",
+ "We offer Not now option because we are asking for mic and cam is already active"
+ );
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+
+ // Clean up
+ webrtcUI.activePerms.delete(gBrowser.selectedBrowser.outerWindowID);
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "test queueing allow video behind deny audio",
+ run: async function testQueuingAllowVideoBehindDenyAudio() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(false, true);
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ ];
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await Promise.all(observerPromises);
+ checkDeviceSelectors(["camera"]);
+
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: false, video: true });
+
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "test queueing allow audio behind allow video with error",
+ run: async function testQueuingAllowAudioBehindAllowVideoWithError() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(
+ false,
+ true,
+ null,
+ null,
+ gBrowser.selectedBrowser,
+ true
+ );
+ await promiseRequestDevice(true, false);
+ await observerPromise;
+ await promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+
+ checkDeviceSelectors(["camera"]);
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:request");
+ let observerPromise2 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ await promiseMessage(badDeviceError, () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: false });
+
+ // Clean up the active microphone in the activePerms map
+ webrtcUI.activePerms.delete(gBrowser.selectedBrowser.outerWindowID);
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "test queueing audio+video behind deny audio",
+ run: async function testQueuingAllowVideoBehindDenyAudio() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny", 2),
+ expectObserverCalled("recording-window-ended"),
+ ];
+
+ await promiseMessage(
+ permissionError,
+ () => {
+ activateSecondaryAction(kActionDeny);
+ },
+ 2
+ );
+ await Promise.all(observerPromises);
+
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "test queueing audio, video behind reload after pending audio, video",
+ run: async function testQueuingDenyAudioBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ await reloadAndAssertClosedStreams();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ // After the reload, gUM(audio) causes a prompt.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ // expect pending camera prompt to appear after ok'ing microphone one.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ video: false, audio: true });
+
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected microphone and camera to be shared"
+ );
+
+ // close all streams
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
new file mode 100644
index 0000000000..d09d7f2c5f
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
@@ -0,0 +1,949 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 rejection "The fetching process for the media resource was aborted by the
+// user agent at the user's request." is left unhandled in some cases. This bug
+// should be fixed, but for the moment this file allows a class of rejections.
+//
+// NOTE: Allowing a whole class of rejections should be avoided. Normally you
+// should use "expectUncaughtRejection" to flag individual failures.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/aborted by the user agent/);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const notFoundError = "error: NotFoundError: The object can not be found here.";
+
+const isHeadless = Services.env.get("MOZ_HEADLESS");
+
+function verifyTabSharingPopup(expectedItems) {
+ let event = new MouseEvent("popupshowing");
+ let sharingMenu = document.getElementById("tabSharingMenuPopup");
+ sharingMenu.dispatchEvent(event);
+
+ is(
+ sharingMenu.children.length,
+ expectedItems.length,
+ "correct number of items on tab sharing menu"
+ );
+ for (let i = 0; i < expectedItems.length; i++) {
+ is(
+ JSON.parse(sharingMenu.children[i].getAttribute("data-l10n-args"))
+ .itemList,
+ expectedItems[i],
+ "label of item " + i + " + was correct"
+ );
+ }
+
+ sharingMenu.dispatchEvent(new MouseEvent("popuphiding"));
+}
+
+var gTests = [
+ {
+ desc: "getUserMedia window/screen picking screen",
+ run: async function checkWindowOrScreen() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+ let notification = PopupNotifications.panel.firstElementChild;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ let noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ let separator = menulist.getItemAtIndex(1);
+ is(
+ separator.localName,
+ "menuseparator",
+ "the second item is a separator"
+ );
+
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should be hidden while there's no selection"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ let scaryScreenIndex;
+ for (let i = 2; i < count; ++i) {
+ let item = menulist.getItemAtIndex(i);
+ is(
+ parseInt(item.getAttribute("value")),
+ i - 2,
+ "the window/screen item has the correct index"
+ );
+ let type = item.getAttribute("devicetype");
+ ok(
+ ["window", "screen"].includes(type),
+ "the devicetype attribute is set correctly"
+ );
+ if (type == "screen") {
+ ok(item.scary, "the screen item is marked as scary");
+ scaryScreenIndex = i;
+ }
+ }
+ ok(
+ typeof scaryScreenIndex == "number",
+ "there's at least one scary screen, as as all screens are"
+ );
+
+ // Select a screen, a preview with a scary warning should appear.
+ menulist.getItemAtIndex(scaryScreenIndex).doCommand();
+ ok(
+ !document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be visible"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is visible"
+ );
+ ok(!notification.button.disabled, "Allow button is enabled");
+
+ // Select the 'Select Window or Screen' item again, the preview should be hidden.
+ menulist.getItemAtIndex(0).doCommand();
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be hidden"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ // Select the scary screen again so that we can have a stream.
+ menulist.getItemAtIndex(scaryScreenIndex).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+ verifyTabSharingPopup(["screen"]);
+
+ // we always show prompt for screen sharing.
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia window/screen picking window",
+ run: async function checkWindowOrScreen() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+ let notification = PopupNotifications.panel.firstElementChild;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ let noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ let separator = menulist.getItemAtIndex(1);
+ is(
+ separator.localName,
+ "menuseparator",
+ "the second item is a separator"
+ );
+
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should be hidden while there's no selection"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ let scaryWindowIndexes = [],
+ nonScaryWindowIndex,
+ scaryScreenIndex;
+ for (let i = 2; i < count; ++i) {
+ let item = menulist.getItemAtIndex(i);
+ is(
+ parseInt(item.getAttribute("value")),
+ i - 2,
+ "the window/screen item has the correct index"
+ );
+ let type = item.getAttribute("devicetype");
+ ok(
+ ["window", "screen"].includes(type),
+ "the devicetype attribute is set correctly"
+ );
+ if (type == "screen") {
+ ok(item.scary, "the screen item is marked as scary");
+ scaryScreenIndex = i;
+ } else if (item.scary) {
+ scaryWindowIndexes.push(i);
+ } else {
+ nonScaryWindowIndex = i;
+ }
+ }
+ if (isHeadless) {
+ is(
+ scaryWindowIndexes.length,
+ 0,
+ "there are no scary Firefox windows in headless mode"
+ );
+ } else {
+ ok(
+ scaryWindowIndexes.length,
+ "there's at least one scary window, as Firefox is running"
+ );
+ }
+ ok(
+ typeof scaryScreenIndex == "number",
+ "there's at least one scary screen, as all screens are"
+ );
+
+ if (!isHeadless) {
+ // Select one scary window, a preview with a scary warning should appear.
+ let scaryWindowIndex;
+ for (scaryWindowIndex of scaryWindowIndexes) {
+ menulist.getItemAtIndex(scaryWindowIndex).doCommand();
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should still be hidden"
+ );
+ try {
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "",
+ 100,
+ 100
+ );
+ break;
+ } catch (e) {
+ // A "scary window" is Firefox. Multiple Firefox windows have been
+ // observed to come and go during try runs, so we won't know which one
+ // is ours. To avoid intermittents, we ignore preview failing due to
+ // these going away on us, provided it succeeds on one of them.
+ }
+ }
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is visible"
+ );
+ // Select the 'Select Window' item again, the preview should be hidden.
+ menulist.getItemAtIndex(0).doCommand();
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ // Select the first window again so that we can have a stream.
+ menulist.getItemAtIndex(scaryWindowIndex).doCommand();
+ }
+
+ let sharingNonScaryWindow = typeof nonScaryWindowIndex == "number";
+
+ // If we have a non-scary window, select it and verify the warning isn't displayed.
+ // A non-scary window may not always exist on test machines.
+ if (sharingNonScaryWindow) {
+ menulist.getItemAtIndex(nonScaryWindowIndex).doCommand();
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should still be hidden"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is hidden"
+ );
+ } else {
+ info("no non-scary window available on this test machine");
+ }
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Window" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ if (sharingNonScaryWindow) {
+ await checkSharingUI({ screen: "Window" });
+ } else {
+ await checkSharingUI({ screen: "Window", browserwindow: true });
+ }
+
+ verifyTabSharingPopup(["window"]);
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio + window/screen",
+ run: async function checkAudioVideo() {
+ if (AppConstants.platform == "macosx") {
+ todo(
+ false,
+ "Bug 1323481 - On Mac on treeherder, but not locally, requesting microphone + screen never makes the permission prompt appear, and so causes the test to timeout"
+ );
+ return;
+ }
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["microphone", "screen"]);
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ // Select a screen, a preview with a scary warning should appear.
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(
+ !document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be visible"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarningBox").hidden,
+ "the scary warning is visible"
+ );
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, screen: "Screen" },
+ "expected screen and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, screen: "Screen" });
+
+ verifyTabSharingPopup(["microphone and screen"]);
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: 'getUserMedia screen, user clicks "Don\'t Allow"',
+ run: async function checkDontShare() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio + window/screen: stop sharing",
+ run: async function checkStopSharing() {
+ if (AppConstants.platform == "macosx") {
+ todo(
+ false,
+ "Bug 1323481 - On Mac on treeherder, but not locally, requesting microphone + screen never makes the permission prompt appear, and so causes the test to timeout"
+ );
+ return;
+ }
+
+ async function share(deviceTypes) {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(
+ /* audio */ deviceTypes.includes("microphone"),
+ /* video */ deviceTypes.some(t => t == "screen" || t == "camera"),
+ null,
+ deviceTypes.includes("screen") && "window"
+ );
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(deviceTypes);
+ if (screen) {
+ let menulist = document.getElementById(
+ "webRTC-selectWindow-menulist"
+ );
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+ }
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ }
+
+ async function check(expected = {}, expectedSharingLabel) {
+ let shared = Object.keys(expected).join(" and ");
+ if (shared) {
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + shared + " to be shared"
+ );
+ await checkSharingUI(expected);
+ verifyTabSharingPopup([expectedSharingLabel]);
+ } else {
+ await checkNotSharing();
+ verifyTabSharingPopup([""]);
+ }
+ }
+
+ info("Share screen and microphone");
+ let indicator = promiseIndicatorWindow();
+ await share(["microphone", "screen"]);
+ await indicator;
+ await check({ audio: true, screen: "Screen" }, "microphone and screen");
+
+ info("Share camera");
+ await share(["camera"]);
+ await check(
+ { video: true, audio: true, screen: "Screen" },
+ "microphone, screen, and camera"
+ );
+
+ info("Stop the screen share, mic+cam should continue");
+ await stopSharing("screen", true);
+ await check({ video: true, audio: true }, "microphone and camera");
+
+ info("Stop the camera, everything should stop.");
+ await stopSharing("camera");
+
+ info("Now, share only the screen...");
+ indicator = promiseIndicatorWindow();
+ await share(["screen"]);
+ await indicator;
+ await check({ screen: "Screen" }, "screen");
+
+ info("... and add camera and microphone in a second request.");
+ await share(["microphone", "camera"]);
+ await check(
+ { video: true, audio: true, screen: "Screen" },
+ "screen, microphone, and camera"
+ );
+
+ info("Stop the camera, this should stop everything.");
+ await stopSharing("camera");
+ },
+ },
+
+ {
+ desc: "getUserMedia window/screen: reloading the page removes all gUM UI",
+ run: async function checkReloading() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+ verifyTabSharingPopup(["screen"]);
+
+ await reloadAndAssertClosedStreams();
+ },
+ },
+
+ {
+ desc: "test showControlCenter from screen icon",
+ run: async function checkShowControlCenter() {
+ if (!USING_LEGACY_INDICATOR) {
+ info(
+ "Skipping since this test doesn't apply to the new global sharing " +
+ "indicator."
+ );
+ return;
+ }
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+ verifyTabSharingPopup(["screen"]);
+
+ ok(permissionPopupHidden(), "control center should be hidden");
+ if (IS_MAC) {
+ let activeStreams = webrtcUI.getActiveStreams(false, false, true);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ } else {
+ let win = Services.wm.getMostRecentWindow(
+ "Browser:WebRTCGlobalIndicator"
+ );
+ let elt = win.document.getElementById("screenShareButton");
+ EventUtils.synthesizeMouseAtCenter(elt, {}, win);
+ }
+ await TestUtils.waitForCondition(
+ () => !permissionPopupHidden(),
+ "wait for control center to open"
+ );
+ ok(!permissionPopupHidden(), "control center should be open");
+
+ gPermissionPanel._permissionPopup.hidePopup();
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "Only persistent block is possible for screen sharing",
+ run: async function checkPersistentPermissions() {
+ // This test doesn't apply when the notification silencing
+ // feature is enabled, since the "Remember this decision"
+ // checkbox doesn't exist.
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ return;
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ let devicePerms = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ browser
+ );
+ is(
+ devicePerms.state,
+ SitePermissions.UNKNOWN,
+ "starting without screen persistent permissions"
+ );
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["screen"]);
+ document
+ .getElementById("webRTC-selectWindow-menulist")
+ .getItemAtIndex(2)
+ .doCommand();
+
+ // Ensure that checking the 'Remember this decision' checkbox disables
+ // 'Allow'.
+ let notification = PopupNotifications.panel.firstElementChild;
+ ok(
+ notification.hasAttribute("warninghidden"),
+ "warning message is hidden"
+ );
+ let checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+ checkbox.click();
+ ok(checkbox.checked, "checkbox now checked");
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is shown"
+ );
+
+ // Click "Don't Allow" to save a persistent block permission.
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ let observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+
+ let permission = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ browser
+ );
+ is(permission.state, SitePermissions.BLOCK, "screen sharing is blocked");
+ is(
+ permission.scope,
+ SitePermissions.SCOPE_PERSISTENT,
+ "screen sharing is persistently blocked"
+ );
+
+ // Request screensharing again, expect an immediate failure.
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ expectObserverCalled("recording-window-ended"),
+ promiseMessage(permissionError),
+ promiseRequestDevice(false, true, null, "screen"),
+ ]);
+
+ // Now set the permission to allow and expect a prompt.
+ SitePermissions.setForPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ SitePermissions.ALLOW
+ );
+
+ // Request devices and expect a prompt despite the saved 'Allow' permission.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ // The 'remember' checkbox shouldn't be checked anymore.
+ notification = PopupNotifications.panel.firstElementChild;
+ ok(
+ notification.hasAttribute("warninghidden"),
+ "warning message is hidden"
+ );
+ checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.checked, "checkbox is not checked");
+
+ // Deny the request to cleanup...
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ browser
+ );
+ },
+ },
+
+ {
+ desc: "Switching between menu options maintains correct main action state while window sharing",
+ skipObserverVerification: true,
+ run: async function checkDoorhangerState() {
+ await enableObserverVerification();
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:newtab");
+ BrowserWindowTracker.orderedWindows[1].focus();
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let notification = PopupNotifications.panel.firstElementChild;
+ let checkbox = notification.checkbox;
+
+ menulist.getItemAtIndex(2).doCommand();
+ checkbox.click();
+ ok(checkbox.checked, "checkbox now checked");
+
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ // When the notification silencing feature is enabled, the checkbox
+ // controls that feature, and its state should not disable the
+ // "Allow" button.
+ ok(!notification.button.disabled, "Allow button is not disabled");
+ } else {
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is shown"
+ );
+ }
+
+ menulist.getItemAtIndex(3).doCommand();
+ ok(checkbox.checked, "checkbox still checked");
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ // When the notification silencing feature is enabled, the checkbox
+ // controls that feature, and its state should not disable the
+ // "Allow" button.
+ ok(!notification.button.disabled, "Allow button remains not disabled");
+ } else {
+ ok(notification.button.disabled, "Allow button remains disabled");
+ ok(
+ !notification.hasAttribute("warninghidden"),
+ "warning message is still shown"
+ );
+ }
+
+ await disableObserverVerification();
+
+ observerPromise = expectObserverCalled("recording-window-ended");
+
+ gBrowser.removeCurrentTab();
+ win.close();
+
+ await observerPromise;
+
+ await openNewTestTab();
+ },
+ },
+ {
+ desc: "Switching between tabs does not bleed state into other prompts",
+ skipObserverVerification: true,
+ run: async function checkSwitchingTabs() {
+ // Open a new window in the background to have a choice in the menulist.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:newtab");
+ await enableObserverVerification();
+ BrowserWindowTracker.orderedWindows[1].focus();
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "window");
+ await promise;
+ await observerPromise;
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ ok(notification.button.disabled, "Allow button is disabled");
+ await disableObserverVerification();
+
+ await openNewTestTab("get_user_media_in_xorigin_frame.html");
+ await enableObserverVerification();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+
+ notification = PopupNotifications.panel.firstElementChild;
+ ok(!notification.button.disabled, "Allow button is not disabled");
+
+ await disableObserverVerification();
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ win.close();
+
+ await openNewTestTab();
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js
new file mode 100644
index 0000000000..9b6cd2fd8e
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Tests that the given tab is the currently selected tab.
+ * @param {Element} aTab - Tab to test.
+ */
+function testSelected(aTab) {
+ is(aTab, gBrowser.selectedTab, "Tab is gBrowser.selectedTab");
+ is(aTab.getAttribute("selected"), "true", "Tab has property 'selected'");
+ is(
+ aTab.getAttribute("visuallyselected"),
+ "true",
+ "Tab has property 'visuallyselected'"
+ );
+}
+
+/**
+ * Tests that when closing a tab with active screen sharing, the screen sharing
+ * ends and the tab closes properly.
+ */
+add_task(async function testScreenSharingTabClose() {
+ let initialTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ // Open another foreground tab and ensure its selected.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ testSelected(tab);
+
+ // Start screen sharing in active tab
+ await shareDevices(tab.linkedBrowser, false, false, SHARE_WINDOW);
+ ok(tab._sharingState.webRTC.screen, "Tab has webRTC screen sharing state");
+
+ let recordingEndedPromise = expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ tab.linkedBrowser.browsingContext
+ );
+ let tabClosedPromise = BrowserTestUtils.waitForCondition(
+ () => gBrowser.selectedTab == initialTab,
+ "Waiting for tab to close"
+ );
+
+ // Close tab
+ BrowserTestUtils.removeTab(tab, { animate: true });
+
+ // Wait for screen sharing to end
+ await recordingEndedPromise;
+
+ // Wait for tab to be fully closed
+ await tabClosedPromise;
+
+ // Test that we're back to the initial tab.
+ testSelected(initialTab);
+
+ // There should be no active sharing for the selected tab.
+ ok(
+ !gBrowser.selectedTab._sharingState?.webRTC?.screen,
+ "Selected tab doesn't have webRTC screen sharing state"
+ );
+
+ BrowserTestUtils.removeTab(initialTab);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
new file mode 100644
index 0000000000..ef69d15971
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.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/. */
+
+var gTests = [
+ {
+ desc: "getUserMedia: tearing-off a tab keeps sharing indicators",
+ skipObserverVerification: true,
+ run: async function checkTearingOff() {
+ await enableObserverVerification();
+
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Don't listen to observer notifications in the tab any more, as
+ // they will need to be switched to the new window.
+ await disableObserverVerification();
+
+ info("tearing off the tab");
+ let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ await whenDelayedStartupFinished(win);
+ await checkSharingUI({ audio: true, video: true }, win);
+
+ await enableObserverVerification(win.gBrowser.selectedBrowser);
+
+ // Clicking the global sharing indicator should open the control center in
+ // the second window.
+ ok(permissionPopupHidden(win), "control center should be hidden");
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ // If the popup gets hidden before being shown, by stray focus/activate
+ // events, don't bother failing the test. It's enough to know that we
+ // started showing the popup.
+ let popup = win.gPermissionPanel._permissionPopup;
+ let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ let ev = await Promise.race([hiddenEvent, shownEvent]);
+ ok(ev.type, "Tried to show popup");
+ win.gPermissionPanel._permissionPopup.hidePopup();
+
+ ok(
+ permissionPopupHidden(window),
+ "control center should be hidden in the first window"
+ );
+
+ await disableObserverVerification();
+
+ // Closing the new window should remove all sharing indicators.
+ let promises = [
+ expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ ];
+
+ await BrowserTestUtils.closeWindow(win);
+ await Promise.all(promises);
+ await checkNotSharing();
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({ set: [["dom.ipc.processCount", 1]] });
+
+ // An empty tab where we can load the content script without leaving it
+ // behind at the end of the test.
+ BrowserTestUtils.addTab(gBrowser);
+
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js
new file mode 100644
index 0000000000..e3276cebc4
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js
@@ -0,0 +1,666 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+camera",
+ run: async function checkAudioVideoWhileLiveTracksExist_audio_camera() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // If there's an active audio+camera stream,
+ // gUM(audio+camera) returns a stream without prompting;
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+
+ promise = promiseMessage("ok");
+
+ await promiseRequestDevice(true, true);
+ await promise;
+ await Promise.all(observerPromises);
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await checkSharingUI({ audio: true, video: true });
+
+ // gUM(screen) causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise;
+
+ // Revoke screen block (only). Don't over-revoke ahead of remaining steps.
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+
+ // After closing all streams, gUM(audio+camera) causes a prompt.
+ await closeStream();
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera -camera",
+ run: async function checkAudioVideoWhileLiveTracksExist_audio_nocamera() {
+ // State: fresh
+
+ {
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ const request = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ const indicator = promiseIndicatorWindow();
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Stop the camera track.
+ await stopTracks("video");
+ await checkSharingUI({ audio: true, video: false });
+ }
+
+ // State: live audio
+
+ {
+ // If there's an active audio track from an audio+camera request,
+ // gUM(camera) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["camera"]);
+
+ // Allow and stop the camera again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("video");
+ await checkSharingUI({ audio: true, video: false });
+ }
+
+ // State: live audio
+
+ {
+ // If there's an active audio track from an audio+camera request,
+ // gUM(audio+camera) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // Allow and stop the camera again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("video");
+ await checkSharingUI({ audio: true, video: false });
+ }
+
+ // State: live audio
+
+ {
+ // After closing all streams, gUM(audio) causes a prompt.
+ await closeStream();
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone"]);
+
+ const response = expectObserverCalled("getUserMedia:response:deny");
+ const windowEnded = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await response;
+ await windowEnded;
+
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ }
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera -audio",
+ run: async function checkAudioVideoWhileLiveTracksExist_camera_noaudio() {
+ // State: fresh
+
+ {
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ const request = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ const indicator = promiseIndicatorWindow();
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // Stop the audio track.
+ await stopTracks("audio");
+ await checkSharingUI({ audio: false, video: true });
+ }
+
+ // State: live camera
+
+ {
+ // If there's an active video track from an audio+camera request,
+ // gUM(audio) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone"]);
+
+ // Allow and stop the microphone again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("audio");
+ await checkSharingUI({ audio: false, video: true });
+ }
+
+ // State: live camera
+
+ {
+ // If there's an active video track from an audio+camera request,
+ // gUM(audio+camera) causes a prompt.
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ // Allow and stop the microphone again.
+ const response = expectObserverCalled("getUserMedia:response:allow");
+ const deviceEvents = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await response;
+ await deviceEvents;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+ await checkSharingUI({ audio: true, video: true });
+
+ await stopTracks("audio");
+ await checkSharingUI({ audio: false, video: true });
+ }
+
+ // State: live camera
+
+ {
+ // After closing all streams, gUM(camera) causes a prompt.
+ await closeStream();
+ const request = expectObserverCalled("getUserMedia:request");
+ const popupShown = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await popupShown;
+ await request;
+ checkDeviceSelectors(["camera"]);
+
+ const response = expectObserverCalled("getUserMedia:response:deny");
+ const windowEnded = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await response;
+ await windowEnded;
+
+ await checkNotSharing();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ }
+ },
+ },
+
+ {
+ desc: "getUserMedia camera",
+ run: async function checkAudioVideoWhileLiveTracksExist_camera() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: false, video: true });
+
+ // If there's an active camera stream,
+ // gUM(audio) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(audio+camera) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(screen) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareScreen-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["screen"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(camera) returns a stream without prompting.
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await Promise.all(observerPromises);
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ await checkSharingUI({ audio: false, video: true });
+
+ // close all streams
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "getUserMedia audio",
+ run: async function checkAudioVideoWhileLiveTracksExist_audio() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, false);
+ await promise;
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ audio: true, video: false });
+
+ // If there's an active audio stream,
+ // gUM(camera) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["camera"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(audio+camera) causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // gUM(audio) returns a stream without prompting.
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(true, false);
+ await promise;
+ await observerPromises;
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true },
+ "expected microphone to be shared"
+ );
+
+ await checkSharingUI({ audio: true, video: false });
+
+ // close all streams
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js
new file mode 100644
index 0000000000..15329ec666
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_in_frame.js
@@ -0,0 +1,309 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+camera in frame 1",
+ run: async function checkAudioVideoWhileLiveTracksExist_frame() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ info("gUM(audio+camera) in frame 2 should prompt");
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame2");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // If there's an active audio+camera stream in frame 1,
+ // gUM(audio+camera) in frame 1 returns a stream without prompting;
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow"),
+ expectObserverCalled("recording-device-events"),
+ ];
+ promise = promiseMessage("ok");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromises;
+ await promiseNoPopupNotification("webRTC-shareDevices");
+
+ // close the stream
+ await closeStream(false, "frame1");
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera in frame 1 - part II",
+ run: async function checkAudioVideoWhileLiveTracksExist_frame_partII() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // If there's an active audio+camera stream in frame 1,
+ // gUM(audio+camera) in the top level window causes a prompt;
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ // close the stream
+ await closeStream(false, "frame1");
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera in frame 1 - reload",
+ run: async function checkAudioVideoWhileLiveTracksExist_frame_reload() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // reload frame 1
+ let observerPromises = [
+ expectObserverCalled("recording-device-stopped"),
+ expectObserverCalled("recording-device-events"),
+ expectObserverCalled("recording-window-ended"),
+ ];
+ await promiseReloadFrame("frame1");
+
+ await Promise.all(observerPromises);
+ await checkNotSharing();
+
+ // After the reload,
+ // gUM(audio+camera) in frame 1 causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame1");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+
+ {
+ desc: "getUserMedia audio+camera at the top level window",
+ run: async function checkAudioVideoWhileLiveTracksExist_topLevel() {
+ // create an active audio+camera stream at the top level window
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+ let indicator = promiseIndicatorWindow();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ audio: true, video: true });
+
+ // If there's an active audio+camera stream at the top level window,
+ // gUM(audio+camera) in frame 2 causes a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame2");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+
+ // close the stream
+ await closeStream();
+ SitePermissions.removeFromPrincipal(
+ null,
+ "screen",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "camera",
+ gBrowser.selectedBrowser
+ );
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests, { relativeURI: "get_user_media_in_frame.html" });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js
new file mode 100644
index 0000000000..ed270967d5
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_queue_request.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gTests = [
+ {
+ desc: "test queueing allow video behind allow video",
+ run: async function testQueuingAllowVideoBehindAllowVideo() {
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promiseRequestDevice(false, true);
+ await promise;
+ checkDeviceSelectors(["camera"]);
+ await observerPromise;
+
+ let promises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:allow", 2),
+ expectObserverCalled("recording-device-events", 2),
+ ];
+
+ await promiseMessage(
+ "ok",
+ () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ },
+ 2
+ );
+ await Promise.all(promises);
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { video: true },
+ "expected camera to be shared"
+ );
+
+ // close all streams
+ await closeStream();
+ },
+ },
+];
+
+add_task(async function test() {
+ SimpleTest.requestCompleteLog();
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js
new file mode 100644
index 0000000000..4a48a93853
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access_tear_off_tab.js
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gTests = [
+ {
+ desc: "getUserMedia: tearing-off a tab",
+ skipObserverVerification: true,
+ run: async function checkAudioVideoWhileLiveTracksExist_TearingOff() {
+ await enableObserverVerification();
+
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(["microphone", "camera"]);
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ video: true, audio: true });
+
+ // Don't listen to observer notifications in the tab any more, as
+ // they will need to be switched to the new window.
+ await disableObserverVerification();
+
+ info("tearing off the tab");
+ let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ await whenDelayedStartupFinished(win);
+ await SimpleTest.promiseFocus(win);
+ await checkSharingUI({ audio: true, video: true }, win);
+
+ await enableObserverVerification(win.gBrowser.selectedBrowser);
+
+ info("request audio+video and check if there is no prompt");
+ let observerPromises = [
+ expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalled(
+ "recording-device-events",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ ];
+ await promiseRequestDevice(
+ true,
+ true,
+ null,
+ null,
+ win.gBrowser.selectedBrowser
+ );
+ await Promise.all(observerPromises);
+
+ await disableObserverVerification();
+
+ observerPromises = [
+ expectObserverCalledOnClose(
+ "recording-device-events",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ expectObserverCalledOnClose(
+ "recording-window-ended",
+ 1,
+ win.gBrowser.selectedBrowser
+ ),
+ ];
+
+ await BrowserTestUtils.closeWindow(win);
+ await Promise.all(observerPromises);
+
+ await checkNotSharing();
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({ set: [["dom.ipc.processCount", 1]] });
+
+ // An empty tab where we can load the content script without leaving it
+ // behind at the end of the test.
+ BrowserTestUtils.addTab(gBrowser);
+
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_select_audio_output.js b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js
new file mode 100644
index 0000000000..87d2d42a3a
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+async function requestAudioOutput(options) {
+ await Promise.all([
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("recording-window-ended"),
+ promiseRequestAudioOutput(options),
+ ]);
+}
+
+async function requestAudioOutputExpectingPrompt(options) {
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ requestAudioOutput(options),
+ ]);
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareSpeaker-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(["speaker"]);
+}
+
+async function requestAudioOutputExpectingDeny(options) {
+ await Promise.all([
+ requestAudioOutput(options),
+ expectObserverCalled("getUserMedia:response:deny"),
+ promiseMessage(permissionError),
+ ]);
+}
+
+async function simulateAudioOutputRequest(options) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [options],
+ function simPrompt({ deviceCount, deviceId }) {
+ const devices = [...Array(deviceCount).keys()].map(i => ({
+ type: "audiooutput",
+ rawName: `name ${i}`,
+ deviceIndex: i,
+ rawId: `rawId ${i}`,
+ id: `id ${i}`,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIMediaDevice]),
+ }));
+ const req = {
+ type: "selectaudiooutput",
+ windowID: content.windowGlobalChild.outerWindowId,
+ devices,
+ getConstraints: () => ({}),
+ getAudioOutputOptions: () => ({ deviceId }),
+ isSecure: true,
+ isHandlingUserInput: true,
+ };
+ const { WebRTCChild } = SpecialPowers.ChromeUtils.importESModule(
+ "resource:///actors/WebRTCChild.sys.mjs"
+ );
+ WebRTCChild.observe(req, "getUserMedia:request");
+ }
+ );
+}
+
+async function allowPrompt() {
+ const observerPromise = expectObserverCalled("getUserMedia:response:allow");
+ PopupNotifications.panel.firstElementChild.button.click();
+ await observerPromise;
+}
+
+async function allow() {
+ await Promise.all([promiseMessage("ok"), allowPrompt()]);
+}
+
+async function denyPrompt() {
+ const observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ activateSecondaryAction(kActionDeny);
+ await observerPromise;
+}
+
+async function deny() {
+ await Promise.all([promiseMessage(permissionError), denyPrompt()]);
+}
+
+async function escapePrompt() {
+ const observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await observerPromise;
+}
+
+async function escape() {
+ await Promise.all([promiseMessage(permissionError), escapePrompt()]);
+}
+
+var gTests = [
+ {
+ desc: 'User clicks "Allow" and revokes',
+ run: async function checkAllow() {
+ await requestAudioOutputExpectingPrompt();
+ await allow();
+
+ info("selectAudioOutput() with no deviceId again should prompt again.");
+ await requestAudioOutputExpectingPrompt();
+ await allow();
+
+ info("selectAudioOutput() with same deviceId should not prompt again.");
+ await Promise.all([
+ expectObserverCalled("getUserMedia:response:allow"),
+ promiseMessage("ok"),
+ requestAudioOutput({ requestSameDevice: true }),
+ ]);
+
+ await revokePermission("speaker", true);
+ info("Same deviceId should prompt again after revoked permission.");
+ await requestAudioOutputExpectingPrompt({ requestSameDevice: true });
+ await allow();
+ await revokePermission("speaker", true);
+ },
+ },
+ {
+ desc: 'User clicks "Not Now"',
+ run: async function checkNotNow() {
+ await requestAudioOutputExpectingPrompt();
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices")
+ .secondaryActions[0].label,
+ "Not now",
+ "first secondary action label"
+ );
+ await deny();
+ info("selectAudioOutput() after Not Now should prompt again.");
+ await requestAudioOutputExpectingPrompt();
+ await escape();
+ },
+ },
+ {
+ desc: 'User presses "Esc"',
+ run: async function checkEsc() {
+ await requestAudioOutputExpectingPrompt();
+ await escape();
+ info("selectAudioOutput() after Esc should prompt again.");
+ await requestAudioOutputExpectingPrompt();
+ await allow();
+ await revokePermission("speaker", true);
+ },
+ },
+ {
+ desc: 'User clicks "Always Block"',
+ run: async function checkAlwaysBlock() {
+ await requestAudioOutputExpectingPrompt();
+ await Promise.all([
+ expectObserverCalled("getUserMedia:response:deny"),
+ promiseMessage(permissionError),
+ activateSecondaryAction(kActionNever),
+ ]);
+ info("selectAudioOutput() after Always Block should not prompt again.");
+ await requestAudioOutputExpectingDeny();
+ await revokePermission("speaker", true);
+ },
+ },
+ {
+ desc: "Single Device",
+ run: async function checkSingle() {
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount: 1 }),
+ ]);
+ checkDeviceSelectors(["speaker"]);
+ await escapePrompt();
+ },
+ },
+ {
+ desc: "Multi Device with deviceId",
+ run: async function checkMulti() {
+ const deviceCount = 4;
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
+ ]);
+ const selectorList = document.getElementById(
+ `webRTC-selectSpeaker-menulist`
+ );
+ is(selectorList.selectedIndex, 2, "pre-selected index");
+ checkDeviceSelectors(["speaker"]);
+ await allowPrompt();
+
+ info("Expect same-device request allowed without prompt");
+ await Promise.all([
+ expectObserverCalled("getUserMedia:response:allow"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
+ ]);
+
+ info("Expect prompt for different-device request");
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
+ ]);
+ await denyPrompt();
+
+ info("Expect prompt again for denied-device request");
+ await Promise.all([
+ promisePopupNotificationShown("webRTC-shareDevices"),
+ simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
+ ]);
+ await escapePrompt();
+
+ await revokePermission("speaker", true);
+ },
+ },
+ {
+ desc: "SitePermissions speaker block",
+ run: async function checkPermissionsBlock() {
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ "speaker",
+ SitePermissions.BLOCK
+ );
+ await requestAudioOutputExpectingDeny();
+ SitePermissions.removeFromPrincipal(gBrowser.contentPrincipal, "speaker");
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({ set: [["media.setsinkid.enabled", true]] });
+ await runTests(gTests);
+});
diff --git a/browser/base/content/test/webrtc/browser_global_mute_toggles.js b/browser/base/content/test/webrtc/browser_global_mute_toggles.js
new file mode 100644
index 0000000000..54713fa8c3
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_global_mute_toggles.js
@@ -0,0 +1,293 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+const MUTE_TOPICS = [
+ "getUserMedia:muteVideo",
+ "getUserMedia:unmuteVideo",
+ "getUserMedia:muteAudio",
+ "getUserMedia:unmuteAudio",
+];
+
+add_setup(async function () {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ["privacy.webrtc.globalMuteToggles", true],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Returns a Promise that resolves when the content process for
+ * <browser> fires the right observer notification based on the
+ * value of isMuted for the camera.
+ *
+ * Note: Callers must ensure that they first call
+ * BrowserTestUtils.startObservingTopics to monitor the mute and
+ * unmute observer notifications for this to work properly.
+ *
+ * @param {<xul:browser>} browser - The browser running in the content process
+ * to be monitored.
+ * @param {Boolean} isMuted - True if the muted topic should be fired.
+ * @return {Promise}
+ * @resolves {undefined} When the notification fires.
+ */
+function waitForCameraMuteState(browser, isMuted) {
+ let topic = isMuted ? "getUserMedia:muteVideo" : "getUserMedia:unmuteVideo";
+ return BrowserTestUtils.contentTopicObserved(browser.browsingContext, topic);
+}
+
+/**
+ * Returns a Promise that resolves when the content process for
+ * <browser> fires the right observer notification based on the
+ * value of isMuted for the microphone.
+ *
+ * Note: Callers must ensure that they first call
+ * BrowserTestUtils.startObservingTopics to monitor the mute and
+ * unmute observer notifications for this to work properly.
+ *
+ * @param {<xul:browser>} browser - The browser running in the content process
+ * to be monitored.
+ * @param {Boolean} isMuted - True if the muted topic should be fired.
+ * @return {Promise}
+ * @resolves {undefined} When the notification fires.
+ */
+function waitForMicrophoneMuteState(browser, isMuted) {
+ let topic = isMuted ? "getUserMedia:muteAudio" : "getUserMedia:unmuteAudio";
+ return BrowserTestUtils.contentTopicObserved(browser.browsingContext, topic);
+}
+
+/**
+ * Tests that the global mute toggles fire the right observer
+ * notifications in pre-existing content processes.
+ */
+add_task(async function test_notifications() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ Assert.ok(
+ microphoneMute.checked,
+ "Microphone toggle should now be checked."
+ );
+ Assert.ok(cameraMute.checked, "Camera toggle should now be checked.");
+
+ info("Unmuting microphone...");
+ let microphoneUnmuted = waitForMicrophoneMuteState(browser, false);
+ microphoneMute.click();
+ await microphoneUnmuted;
+ info("Microphone successfully unmuted.");
+
+ info("Unmuting camera...");
+ let cameraUnmuted = waitForCameraMuteState(browser, false);
+ cameraMute.click();
+ await cameraUnmuted;
+ info("Camera successfully unmuted.");
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+ });
+});
+
+/**
+ * Tests that if sharing stops while muted, and the indicator closes,
+ * then the mute state is reset.
+ */
+add_task(async function test_closing_indicator_resets_mute() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ Assert.ok(
+ microphoneMute.checked,
+ "Microphone toggle should now be checked."
+ );
+ Assert.ok(cameraMute.checked, "Camera toggle should now be checked.");
+
+ let allUnmuted = Promise.all([
+ waitForMicrophoneMuteState(browser, false),
+ waitForCameraMuteState(browser, false),
+ ]);
+
+ await closeStream();
+ await allUnmuted;
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+ });
+});
+
+/**
+ * Test that if the global mute state is set, then newly created
+ * content processes also have their tracks muted after sending
+ * a getUserMedia request.
+ */
+add_task(async function test_new_processes() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_PAGE,
+ });
+ let browser1 = tab1.linkedBrowser;
+
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser1, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser1.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser1, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser1, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ // We'll make sure a new process is being launched by observing
+ // for the ipc:content-created notification.
+ let processLaunched = TestUtils.topicObserved("ipc:content-created");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_PAGE,
+ forceNewProcess: true,
+ });
+ let browser2 = tab2.linkedBrowser;
+
+ await processLaunched;
+
+ await BrowserTestUtils.startObservingTopics(
+ browser2.browsingContext,
+ MUTE_TOPICS
+ );
+
+ let microphoneMuted2 = waitForMicrophoneMuteState(browser2, true);
+ let cameraMuted2 = waitForCameraMuteState(browser2, true);
+ info("Sharing the microphone and camera from a new process.");
+ await shareDevices(browser2, true /* camera */, true /* microphone */);
+ await Promise.all([microphoneMuted2, cameraMuted2]);
+
+ info("Unmuting microphone...");
+ let microphoneUnmuted = Promise.all([
+ waitForMicrophoneMuteState(browser1, false),
+ waitForMicrophoneMuteState(browser2, false),
+ ]);
+ microphoneMute.click();
+ await microphoneUnmuted;
+ info("Microphone successfully unmuted.");
+
+ info("Unmuting camera...");
+ let cameraUnmuted = Promise.all([
+ waitForCameraMuteState(browser1, false),
+ waitForCameraMuteState(browser2, false),
+ ]);
+ cameraMute.click();
+ await cameraUnmuted;
+ info("Camera successfully unmuted.");
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser1.browsingContext,
+ MUTE_TOPICS
+ );
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser2.browsingContext,
+ MUTE_TOPICS
+ );
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/base/content/test/webrtc/browser_indicator_popuphiding.js b/browser/base/content/test/webrtc/browser_indicator_popuphiding.js
new file mode 100644
index 0000000000..8d02eb5c70
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_indicator_popuphiding.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Regression test for bug 1668838 - make sure that a popuphiding
+ * event that fires for any popup not related to the device control
+ * menus is ignored and doesn't cause the targets contents to be all
+ * removed.
+ */
+add_task(async function test_popuphiding() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN
+ );
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ Assert.ok(doc.body, "Should have a document body in the indicator.");
+
+ let event = new indicator.MouseEvent("popuphiding", { bubbles: true });
+ doc.documentElement.dispatchEvent(event);
+
+ Assert.ok(doc.body, "Should still have a document body in the indicator.");
+ });
+
+ await checkNotSharing();
+});
diff --git a/browser/base/content/test/webrtc/browser_notification_silencing.js b/browser/base/content/test/webrtc/browser_notification_silencing.js
new file mode 100644
index 0000000000..e2c5c76e2d
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_notification_silencing.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Tests that the screen / window sharing permission popup offers the ability
+ * for users to silence DOM notifications while sharing.
+ */
+
+/**
+ * Helper function that exercises a specific browser to test whether or not the
+ * user can silence notifications via the display sharing permission panel.
+ *
+ * First, we ensure that notification silencing is disabled by default. Then, we
+ * request screen sharing from the browser, and check the checkbox that
+ * silences notifications. Once screen sharing is established, then we ensure
+ * that notification silencing is enabled. Then we stop sharing, and ensure that
+ * notification silencing is disabled again.
+ *
+ * @param {<xul:browser>} aBrowser - The window to run the test on. This browser
+ * should have TEST_PAGE loaded.
+ * @return Promise
+ * @resolves undefined - When the test on the browser is complete.
+ */
+async function testNotificationSilencing(aBrowser) {
+ let hasIndicator = Services.wm
+ .getEnumerator("Browser:WebRTCGlobalIndicator")
+ .hasMoreElements();
+
+ let window = aBrowser.ownerGlobal;
+
+ let alertsService = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIAlertsDoNotDisturb);
+ Assert.ok(alertsService, "Alerts Service implements nsIAlertsDoNotDisturb");
+ Assert.ok(
+ !alertsService.suppressForScreenSharing,
+ "Should not be silencing notifications to start."
+ );
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ aBrowser
+ );
+ let promise = promisePopupNotificationShown(
+ "webRTC-shareDevices",
+ null,
+ window
+ );
+ let indicatorPromise = hasIndicator
+ ? Promise.resolve()
+ : promiseIndicatorWindow();
+ await promiseRequestDevice(false, true, null, "screen", aBrowser);
+ await promise;
+ await observerPromise;
+
+ checkDeviceSelectors(["screen"], window);
+
+ let document = window.document;
+
+ // Select one of the windows / screens. It doesn't really matter which.
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let notification = window.PopupNotifications.panel.firstElementChild;
+ Assert.ok(
+ notification.hasAttribute("warninghidden"),
+ "Notification silencing warning message is hidden by default"
+ );
+
+ let checkbox = notification.checkbox;
+ Assert.ok(!!checkbox, "Notification silencing checkbox is present");
+ Assert.ok(!checkbox.checked, "checkbox is not checked by default");
+ checkbox.click();
+ Assert.ok(checkbox.checked, "checkbox now checked");
+ // The orginal behaviour of the checkbox disabled the Allow button. Let's
+ // make sure we're not still doing that.
+ Assert.ok(!notification.button.disabled, "Allow button is not disabled");
+ Assert.ok(
+ notification.hasAttribute("warninghidden"),
+ "No warning message is shown"
+ );
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow",
+ 1,
+ aBrowser
+ );
+ let observerPromise2 = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aBrowser
+ );
+ await promiseMessage(
+ "ok",
+ () => {
+ notification.button.click();
+ },
+ 1,
+ aBrowser
+ );
+ await observerPromise1;
+ await observerPromise2;
+ let indicator = await indicatorPromise;
+
+ Assert.ok(
+ alertsService.suppressForScreenSharing,
+ "Should now be silencing notifications"
+ );
+
+ let indicatorClosedPromise = hasIndicator
+ ? Promise.resolve()
+ : BrowserTestUtils.domWindowClosed(indicator);
+
+ await stopSharing("screen", true, aBrowser, window);
+ await indicatorClosedPromise;
+
+ Assert.ok(
+ !alertsService.suppressForScreenSharing,
+ "Should no longer be silencing notifications"
+ );
+}
+
+add_setup(async function () {
+ // Set prefs so that permissions prompts are shown and loopback devices
+ // are not used. To test the chrome we want prompts to be shown, and
+ // these tests are flakey when using loopback devices (though it would
+ // be desirable to make them work with loopback in future). See bug 1643711.
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Tests notification silencing in a normal browser window.
+ */
+add_task(async function testNormalWindow() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await testNotificationSilencing(browser);
+ }
+ );
+});
+
+/**
+ * Tests notification silencing in a private browser window.
+ */
+add_task(async function testPrivateWindow() {
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWindow.gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await testNotificationSilencing(browser);
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
+
+/**
+ * Tests notification silencing when sharing a screen while already
+ * sharing the microphone. Alone ensures that if we stop sharing the
+ * screen, but continue sharing the microphone, that notification
+ * silencing ends.
+ */
+add_task(async function testWhileSharingMic() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+
+ let indicatorPromise = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { audio: true, video: true },
+ "expected camera and microphone to be shared"
+ );
+
+ let indicator = await indicatorPromise;
+ await checkSharingUI({ audio: true, video: true });
+
+ await testNotificationSilencing(browser);
+
+ let indicatorClosedPromise = BrowserTestUtils.domWindowClosed(indicator);
+ await closeStream();
+ await indicatorClosedPromise;
+ }
+ );
+});
diff --git a/browser/base/content/test/webrtc/browser_stop_sharing_button.js b/browser/base/content/test/webrtc/browser_stop_sharing_button.js
new file mode 100644
index 0000000000..17ab66abc4
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_stop_sharing_button.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+add_setup(async function () {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display while
+ * also sharing their microphone or camera, that only the display
+ * stream is stopped.
+ */
+add_task(async function test_stop_sharing() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN
+ );
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = expectObserverCalled("recording-device-events");
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ // Ensure that we're still sharing the other streams.
+ await checkSharingUI({ audio: true, video: true });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+ });
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display while
+ * sharing their display on multiple sites, all of those display sharing
+ * streams are closed.
+ */
+add_task(async function test_stop_sharing_multiple() {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ info("Opening first tab");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab1.linkedBrowser, true, true, SHARE_SCREEN);
+
+ info("Opening second tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera and screen");
+ await shareDevices(tab2.linkedBrowser, true, false, SHARE_SCREEN);
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = TestUtils.waitForCondition(() => {
+ return !webrtcUI.showScreenSharingIndicator;
+ });
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ Assert.equal(gBrowser.selectedTab, tab2, "Should have tab2 selected.");
+ await checkSharingUI({ audio: false, video: true }, window, {
+ audio: true,
+ video: true,
+ });
+ BrowserTestUtils.removeTab(tab2);
+
+ Assert.equal(gBrowser.selectedTab, tab1, "Should have tab1 selected.");
+ await checkSharingUI({ audio: true, video: true }, window, {
+ audio: true,
+ video: true,
+ });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display, persistent
+ * permissions are not removed for camera or microphone devices.
+ */
+add_task(async function test_keep_permissions() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN,
+ true /* remember */
+ );
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = expectObserverCalled("recording-device-events");
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ // Ensure that we're still sharing the other streams.
+ await checkSharingUI({ audio: true, video: true }, undefined, undefined, {
+ audio: { scope: SitePermissions.SCOPE_PERSISTENT },
+ video: { scope: SitePermissions.SCOPE_PERSISTENT },
+ });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+
+ let { state: micState, scope: micScope } = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+
+ Assert.equal(micState, SitePermissions.ALLOW);
+ Assert.equal(micScope, SitePermissions.SCOPE_PERSISTENT);
+
+ let { state: camState, scope: camScope } = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ Assert.equal(camState, SitePermissions.ALLOW);
+ Assert.equal(camScope, SitePermissions.SCOPE_PERSISTENT);
+
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js b/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js
new file mode 100644
index 0000000000..b5d4229fdc
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+add_setup(async function () {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Tests that if the indicator is closed somehow by the user when streams
+ * still ongoing, that all of those streams it represents are stopped, and
+ * the most recent tab that a stream was shared with is selected.
+ *
+ * This test makes sure the global mute toggles for camera and microphone
+ * are disabled, so the indicator only represents display streams, and only
+ * those streams should be stopped on close.
+ */
+add_task(async function test_close_indicator_no_global_toggles() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.globalMuteToggles", false]],
+ });
+
+ let indicatorPromise = promiseIndicatorWindow();
+
+ info("Opening first tab");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab1.linkedBrowser, true, true, SHARE_SCREEN, false);
+
+ info("Opening second tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab2.linkedBrowser, true, true, SHARE_SCREEN, true);
+
+ info("Opening third tab");
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing screen");
+ await shareDevices(tab3.linkedBrowser, false, false, SHARE_SCREEN, false);
+
+ info("Opening fourth tab");
+ let tab4 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab4,
+ "Most recently opened tab is selected"
+ );
+
+ let indicator = await indicatorPromise;
+
+ indicator.close();
+
+ // Wait a tick of the event loop to give the unload handler in the indicator
+ // a chance to run.
+ await new Promise(resolve => executeSoon(resolve));
+
+ // Make sure the media capture state has a chance to flush up to the parent.
+ await getMediaCaptureState();
+
+ // The camera and microphone streams should still be active.
+ let camStreams = webrtcUI.getActiveStreams(true, false);
+ Assert.equal(camStreams.length, 2, "Should have found two camera streams");
+ let micStreams = webrtcUI.getActiveStreams(false, true);
+ Assert.equal(
+ micStreams.length,
+ 2,
+ "Should have found two microphone streams"
+ );
+
+ // The camera and microphone permission were remembered for tab2, so check to
+ // make sure that the permissions remain.
+ let { state: camState, scope: camScope } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "camera",
+ tab2.linkedBrowser
+ );
+ Assert.equal(camState, SitePermissions.ALLOW);
+ Assert.equal(camScope, SitePermissions.SCOPE_PERSISTENT);
+
+ let { state: micState, scope: micScope } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "microphone",
+ tab2.linkedBrowser
+ );
+ Assert.equal(micState, SitePermissions.ALLOW);
+ Assert.equal(micScope, SitePermissions.SCOPE_PERSISTENT);
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab3,
+ "Most recently tab that streams were shared with is selected"
+ );
+
+ SitePermissions.removeFromPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "camera",
+ tab2.linkedBrowser
+ );
+
+ SitePermissions.removeFromPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "microphone",
+ tab2.linkedBrowser
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+});
+
+/**
+ * Tests that if the indicator is closed somehow by the user when streams
+ * still ongoing, that all of those streams is represents are stopped, and
+ * the most recent tab that a stream was shared with is selected.
+ *
+ * This test makes sure the global mute toggles are enabled. This means that
+ * when the user manages to close the indicator, we should revoke camera
+ * and microphone permissions too.
+ */
+add_task(async function test_close_indicator_with_global_toggles() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.globalMuteToggles", true]],
+ });
+
+ let indicatorPromise = promiseIndicatorWindow();
+
+ info("Opening first tab");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab1.linkedBrowser, true, true, SHARE_SCREEN, false);
+
+ info("Opening second tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab2.linkedBrowser, true, true, SHARE_SCREEN, true);
+
+ info("Opening third tab");
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing screen");
+ await shareDevices(tab3.linkedBrowser, false, false, SHARE_SCREEN, false);
+
+ info("Opening fourth tab");
+ let tab4 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab4,
+ "Most recently opened tab is selected"
+ );
+
+ let indicator = await indicatorPromise;
+
+ indicator.close();
+
+ // Wait a tick of the event loop to give the unload handler in the indicator
+ // a chance to run.
+ await new Promise(resolve => executeSoon(resolve));
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ {},
+ "expected nothing to be shared"
+ );
+
+ // Ensuring we no longer have any active streams.
+ let streams = webrtcUI.getActiveStreams(true, true, true, true);
+ Assert.equal(streams.length, 0, "Should have found no active streams");
+
+ // The camera and microphone permissions should have been cleared.
+ let { state: camState } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "camera",
+ tab2.linkedBrowser
+ );
+ Assert.equal(camState, SitePermissions.UNKNOWN);
+
+ let { state: micState } = SitePermissions.getForPrincipal(
+ tab2.linkedBrowser.contentPrincipal,
+ "microphone",
+ tab2.linkedBrowser
+ );
+ Assert.equal(micState, SitePermissions.UNKNOWN);
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab3,
+ "Most recently tab that streams were shared with is selected"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+});
diff --git a/browser/base/content/test/webrtc/browser_tab_switch_warning.js b/browser/base/content/test/webrtc/browser_tab_switch_warning.js
new file mode 100644
index 0000000000..f0625ab4ca
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_tab_switch_warning.js
@@ -0,0 +1,538 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the warning that is displayed when switching to background
+ * tabs when sharing the browser window or screen
+ */
+
+// The number of tabs to have in the background for testing.
+const NEW_BACKGROUND_TABS_TO_OPEN = 5;
+const WARNING_PANEL_ID = "sharing-tabs-warning-panel";
+const ALLOW_BUTTON_ID = "sharing-warning-proceed-to-tab";
+const DISABLE_WARNING_FOR_SESSION_CHECKBOX_ID =
+ "sharing-warning-disable-for-session";
+const WINDOW_SHARING_HEADER_ID = "sharing-warning-window-panel-header";
+const SCREEN_SHARING_HEADER_ID = "sharing-warning-screen-panel-header";
+// The number of milliseconds we're willing to wait for the
+// warning panel before we decide that it's not coming.
+const WARNING_PANEL_TIMEOUT_MS = 1000;
+const CTRL_TAB_RUO_PREF = "browser.ctrlTab.sortByRecentlyUsed";
+
+/**
+ * Common helper function that pretendToShareWindow and pretendToShareScreen
+ * call into. Ensures that the first tab is selected, and then (optionally)
+ * does the first "freebie" tab switch to the second tab.
+ *
+ * @param {boolean} doFirstTabSwitch - True if this function should take
+ * care of doing the "freebie" tab switch for you.
+ * @return {Promise}
+ * @resolves {undefined} - Once the simulation is set up.
+ */
+async function pretendToShareDisplay(doFirstTabSwitch) {
+ Assert.equal(
+ gBrowser.selectedTab,
+ gBrowser.tabs[0],
+ "Should start on the first tab."
+ );
+
+ webrtcUI.sharingDisplay = true;
+ if (doFirstTabSwitch) {
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
+ }
+}
+
+/**
+ * Simulates the sharing of a particular browser window. The
+ * simulation doesn't actually share the window over WebRTC, but
+ * does enough to convince webrtcUI that the window is in the shared
+ * window list.
+ *
+ * It is assumed that the first tab is the selected tab when calling
+ * this function.
+ *
+ * This helper function can also automatically perform the first
+ * "freebie" tab switch that never warns. This is its default behaviour.
+ *
+ * @param {DOM Window} aWindow - The window that we're simulating sharing.
+ * @param {boolean} doFirstTabSwitch - True if this function should take
+ * care of doing the "freebie" tab switch for you. Defaults to true.
+ * @return {Promise}
+ * @resolves {undefined} - Once the simulation is set up.
+ */
+async function pretendToShareWindow(aWindow, doFirstTabSwitch = true) {
+ // Poke into webrtcUI so that it thinks that the current browser
+ // window is being shared.
+ webrtcUI.sharedBrowserWindows.add(aWindow);
+ await pretendToShareDisplay(doFirstTabSwitch);
+}
+
+/**
+ * Simulates the sharing of the screen. The simulation doesn't actually share
+ * the screen over WebRTC, but does enough to convince webrtcUI that the screen
+ * is being shared.
+ *
+ * It is assumed that the first tab is the selected tab when calling
+ * this function.
+ *
+ * This helper function can also automatically perform the first
+ * "freebie" tab switch that never warns. This is its default behaviour.
+ *
+ * @param {boolean} doFirstTabSwitch - True if this function should take
+ * care of doing the "freebie" tab switch for you. Defaults to true.
+ * @return {Promise}
+ * @resolves {undefined} - Once the simulation is set up.
+ */
+async function pretendToShareScreen(doFirstTabSwitch = true) {
+ // Poke into webrtcUI so that it thinks that the current screen is being
+ // shared.
+ webrtcUI.sharingScreen = true;
+ await pretendToShareDisplay(doFirstTabSwitch);
+}
+
+/**
+ * Resets webrtcUI's notion of what is being shared. This also clears
+ * out any simulated shared windows, and resets any state that only
+ * persists for a sharing session.
+ *
+ * This helper function will also:
+ * 1. Switch back to the first tab if it's not already selected.
+ * 2. Check if the tab switch warning panel is open, and if so, close it.
+ *
+ * @return {Promise}
+ * @resolves {undefined} - Once the state is reset.
+ */
+async function resetDisplaySharingState() {
+ let firstTabBC = gBrowser.browsers[0].browsingContext;
+ webrtcUI.streamAddedOrRemoved(firstTabBC, { remove: true });
+
+ if (gBrowser.selectedTab !== gBrowser.tabs[0]) {
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+ }
+
+ let panel = document.getElementById(WARNING_PANEL_ID);
+ if (panel && (panel.state == "open" || panel.state == "showing")) {
+ info("Closing the warning panel.");
+ let panelHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await panelHidden;
+ }
+}
+
+/**
+ * Checks to make sure that a tab switch warning doesn't show
+ * within WARNING_PANEL_TIMEOUT_MS milliseconds.
+ *
+ * @return {Promise}
+ * @resolves {undefined} - Once the check is complete.
+ */
+async function ensureNoWarning() {
+ let timerExpired = false;
+ let sawWarning = false;
+
+ let resolver;
+ let timeoutOrPopupShowingPromise = new Promise(resolve => {
+ resolver = resolve;
+ });
+
+ let onPopupShowing = event => {
+ if (event.target.id == WARNING_PANEL_ID) {
+ sawWarning = true;
+ resolver();
+ }
+ };
+ // The panel might not have been lazily-inserted yet, so we
+ // attach the popupshowing handler to the window instead.
+ window.addEventListener("popupshowing", onPopupShowing);
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ let timer = setTimeout(() => {
+ timerExpired = true;
+ resolver();
+ }, WARNING_PANEL_TIMEOUT_MS);
+
+ await timeoutOrPopupShowingPromise;
+
+ clearTimeout(timer);
+ window.removeEventListener("popupshowing", onPopupShowing);
+
+ Assert.ok(timerExpired, "Timer should have expired.");
+ Assert.ok(!sawWarning, "Should not have shown the tab switch warning.");
+}
+
+/**
+ * Checks to make sure that a tab switch warning appears for
+ * a particular tab.
+ *
+ * @param {<xul:tab>} tab - The tab that the warning should be anchored to.
+ * @return {Promise}
+ * @resolves {undefined} - Once the check is complete.
+ */
+async function ensureWarning(tab) {
+ let popupShowingEvent = await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshowing",
+ false,
+ event => {
+ return event.target.id == WARNING_PANEL_ID;
+ }
+ );
+ let panel = popupShowingEvent.target;
+
+ Assert.equal(
+ panel.anchorNode,
+ tab,
+ "Expected the warning to be anchored to the right tab."
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.sharedTabWarning", true]],
+ });
+
+ // Loads up NEW_BACKGROUND_TABS_TO_OPEN background tabs at about:blank,
+ // and waits until they're fully open.
+ let uris = new Array(NEW_BACKGROUND_TABS_TO_OPEN).fill("about:blank");
+
+ let loadPromises = Promise.all(
+ uris.map(uri => BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true))
+ );
+
+ gBrowser.loadTabs(uris, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await loadPromises;
+
+ // Switches to the first tab and closes all of the rest.
+ registerCleanupFunction(async () => {
+ await resetDisplaySharingState();
+ gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+ });
+});
+
+/**
+ * Tests that when sharing the window that the first tab switch does _not_ show
+ * the warning. This is because we presume that the first tab switch since
+ * starting display sharing is for a tab that is intentionally being shared.
+ */
+add_task(async function testFirstTabSwitchAllowed() {
+ pretendToShareWindow(window, false);
+
+ let targetTab = gBrowser.tabs[1];
+
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await noWarningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that the second tab switch after sharing is not allowed
+ * without a warning. Also tests that the warning can "allow"
+ * the tab switch to proceed, and that no warning is subsequently
+ * shown for the "allowed" tab. Finally, ensures that if the sharing
+ * session ends and a new session begins, that warnings are shown
+ * again for the allowed tabs.
+ */
+add_task(async function testWarningOnSecondTabSwitch() {
+ pretendToShareWindow(window);
+ let originalTab = gBrowser.selectedTab;
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on the second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ // Not only should we have warned, but we should have prevented
+ // the tab switch from occurring.
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should still be on the original tab."
+ );
+
+ // Now test the "Allow" button in the warning to make sure the tab
+ // switch goes through.
+ let tabSwitchPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "TabSwitchDone"
+ );
+ let allowButton = document.getElementById(ALLOW_BUTTON_ID);
+ allowButton.click();
+ await tabSwitchPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs to the target."
+ );
+
+ // We shouldn't see a warning when switching back to that first
+ // "freebie" tab.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+ await noWarningPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should have switched tabs back to the original tab."
+ );
+
+ // We shouldn't see a warning when switching back to the tab that
+ // we had just allowed.
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await noWarningPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs back to the target tab."
+ );
+
+ // Reset the sharing state, and make sure that warnings can
+ // be displayed again.
+ await resetDisplaySharingState();
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ //
+ // Make sure we get the warning again when switching to the
+ // target tab.
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that warnings can be skipped for a session via the
+ * checkbox in the warning panel. Also checks that once the
+ * session ends and a new one begins that warnings are displayed
+ * again.
+ */
+add_task(async function testDisableWarningForSession() {
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on the second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ // Check the checkbox to suppress warnings for the rest of this session.
+ let checkbox = document.getElementById(
+ DISABLE_WARNING_FOR_SESSION_CHECKBOX_ID
+ );
+ checkbox.checked = true;
+
+ // Now test the "Allow" button in the warning to make sure the tab
+ // switch goes through.
+ let tabSwitchPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "TabSwitchDone"
+ );
+ let allowButton = document.getElementById(ALLOW_BUTTON_ID);
+ allowButton.click();
+ await tabSwitchPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs to the target tab."
+ );
+
+ // Switching to the 4th and 5th tabs should now not show warnings.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
+ await noWarningPromise;
+
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
+ await noWarningPromise;
+
+ // Reset the sharing state, and make sure that warnings can
+ // be displayed again.
+ await resetDisplaySharingState();
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ //
+ // Make sure we get the warning again when switching to the
+ // target tab.
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that we don't show a warning when sharing a different
+ * window than the one we're switching tabs in.
+ */
+add_task(async function testOtherWindow() {
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(window);
+ pretendToShareWindow(otherWin);
+
+ // Switching to the 4th and 5th tabs should now not show warnings.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
+ await noWarningPromise;
+
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
+ await noWarningPromise;
+
+ await BrowserTestUtils.closeWindow(otherWin);
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that we show a different label when sharing the screen
+ * vs when sharing a window.
+ */
+add_task(async function testWindowVsScreenLabel() {
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch.
+ // Let's now switch to the third tab.
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on this second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ let windowHeader = document.getElementById(WINDOW_SHARING_HEADER_ID);
+ let screenHeader = document.getElementById(SCREEN_SHARING_HEADER_ID);
+ Assert.ok(
+ !BrowserTestUtils.is_hidden(windowHeader),
+ "Should be showing window sharing header"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(screenHeader),
+ "Should not be showing screen sharing header"
+ );
+
+ // Reset the sharing state, and then pretend to share the screen.
+ await resetDisplaySharingState();
+ pretendToShareScreen();
+
+ // Ensure that we show the warning on this second tab switch
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(windowHeader),
+ "Should not be showing window sharing header"
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_hidden(screenHeader),
+ "Should be showing screen sharing header"
+ );
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that tab switching via the keyboard can also trigger the
+ * tab switch warnings.
+ */
+add_task(async function testKeyboardTabSwitching() {
+ let pressCtrlTab = async (expectPanel = false) => {
+ let promise;
+ if (expectPanel) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_TAB", {
+ ctrlKey: true,
+ shiftKey: false,
+ });
+ await promise;
+ };
+
+ let releaseCtrl = async () => {
+ let promise;
+ if (ctrlTab.isOpen) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popuphidden");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+ return promise;
+ };
+
+ // Ensure that the (on by default) ctrl-tab switch panel is enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [[CTRL_TAB_RUO_PREF, true]],
+ });
+
+ pretendToShareWindow(window);
+ let originalTab = gBrowser.selectedTab;
+ await pressCtrlTab(true);
+
+ // The Ctrl-Tab MRU list should be:
+ // 0: Second tab (currently selected)
+ // 1: First tab
+ // 2: Last tab
+ //
+ // Having pressed Ctrl-Tab once, 1 (First tab) is selected in the
+ // panel. We want a tab that will warn, so let's hit Ctrl-Tab again
+ // to choose 2 (Last tab).
+ let targetTab = ctrlTab.tabList[2];
+ await pressCtrlTab();
+
+ let warningPromise = ensureWarning(targetTab);
+ await releaseCtrl();
+ await warningPromise;
+
+ // Hide the warning without allowing the tab switch.
+ let panel = document.getElementById(WARNING_PANEL_ID);
+ panel.hidePopup();
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should not have changed from the original tab."
+ );
+
+ // Now switch to the in-order tab switching keyboard shortcut mode.
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [[CTRL_TAB_RUO_PREF, false]],
+ });
+
+ // Hitting Ctrl-Tab should choose the _next_ tab over from
+ // the originalTab, which should be the third tab.
+ targetTab = gBrowser.tabs[2];
+
+ warningPromise = ensureWarning(targetTab);
+ await pressCtrlTab();
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
diff --git a/browser/base/content/test/webrtc/browser_webrtc_hooks.js b/browser/base/content/test/webrtc/browser_webrtc_hooks.js
new file mode 100644
index 0000000000..e980b15286
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_webrtc_hooks.js
@@ -0,0 +1,371 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ORIGIN = "https://example.com";
+
+async function tryPeerConnection(browser, expectedError = null) {
+ let errtype = await SpecialPowers.spawn(browser, [], async function () {
+ let pc = new content.RTCPeerConnection();
+ try {
+ await pc.createOffer({ offerToReceiveAudio: true });
+ return null;
+ } catch (err) {
+ return err.name;
+ }
+ });
+
+ let detail = expectedError
+ ? `createOffer() threw a ${expectedError}`
+ : "createOffer() succeeded";
+ is(errtype, expectedError, detail);
+}
+
+// Helper for tests that use the peer-request-allowed and -blocked events.
+// A test that expects some of those events does the following:
+// - call Events.on() before the test to setup event handlers
+// - call Events.expect(name) after a specific event is expected to have
+// occured. This will fail if the event didn't occur, and will return
+// the details passed to the handler for furhter checking.
+// - call Events.off() at the end of the test to clean up. At this point, if
+// any events were triggered that the test did not expect, the test fails.
+const Events = {
+ events: ["peer-request-allowed", "peer-request-blocked"],
+ details: new Map(),
+ handlers: new Map(),
+ on() {
+ for (let event of this.events) {
+ let handler = data => {
+ if (this.details.has(event)) {
+ ok(false, `Got multiple ${event} events`);
+ }
+ this.details.set(event, data);
+ };
+ webrtcUI.on(event, handler);
+ this.handlers.set(event, handler);
+ }
+ },
+ expect(event) {
+ let result = this.details.get(event);
+ isnot(result, undefined, `${event} event was triggered`);
+ this.details.delete(event);
+
+ // All events should have a good origin
+ is(result.origin, ORIGIN, `${event} event has correct origin`);
+
+ return result;
+ },
+ off() {
+ for (let event of this.events) {
+ webrtcUI.off(event, this.handlers.get(event));
+ this.handlers.delete(event);
+ }
+ for (let [event] of this.details) {
+ ok(false, `Got unexpected event ${event}`);
+ }
+ },
+};
+
+var gTests = [
+ {
+ desc: "Basic peer-request-allowed event",
+ run: async function testPeerRequestEvent(browser) {
+ Events.on();
+
+ await tryPeerConnection(browser);
+
+ let details = Events.expect("peer-request-allowed");
+ isnot(
+ details.callID,
+ undefined,
+ "peer-request-allowed event includes callID"
+ );
+ isnot(
+ details.windowID,
+ undefined,
+ "peer-request-allowed event includes windowID"
+ );
+
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Immediate peer connection blocker can allow",
+ run: async function testBlocker(browser) {
+ Events.on();
+
+ let blockerCalled = false;
+ let blocker = params => {
+ is(
+ params.origin,
+ ORIGIN,
+ "Peer connection blocker origin parameter is correct"
+ );
+ blockerCalled = true;
+ return "allow";
+ };
+
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await tryPeerConnection(browser);
+ is(blockerCalled, true, "Blocker was called");
+ Events.expect("peer-request-allowed");
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Deferred peer connection blocker can allow",
+ run: async function testDeferredBlocker(browser) {
+ Events.on();
+
+ let blocker = params => Promise.resolve("allow");
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await tryPeerConnection(browser);
+ Events.expect("peer-request-allowed");
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Immediate peer connection blocker can deny",
+ run: async function testBlockerDeny(browser) {
+ Events.on();
+
+ let blocker = params => "deny";
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await tryPeerConnection(browser, "NotAllowedError");
+
+ Events.expect("peer-request-blocked");
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Multiple blockers work (both allow)",
+ run: async function testMultipleAllowBlockers(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser);
+
+ Events.expect("peer-request-allowed");
+ ok(blocker1Called, "First blocker was called");
+ ok(blocker2Called, "Second blocker was called");
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Multiple blockers work (allow then deny)",
+ run: async function testAllowDenyBlockers(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "deny";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser, "NotAllowedError");
+
+ Events.expect("peer-request-blocked");
+ ok(blocker1Called, "First blocker was called");
+ ok(blocker2Called, "Second blocker was called");
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Multiple blockers work (deny first)",
+ run: async function testDenyAllowBlockers(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "deny";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser, "NotAllowedError");
+
+ Events.expect("peer-request-blocked");
+ ok(blocker1Called, "First blocker was called");
+ ok(
+ !blocker2Called,
+ "Peer connection blocker after a deny is not invoked"
+ );
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Blockers may be removed",
+ run: async function testRemoveBlocker(browser) {
+ Events.on();
+
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+
+ await tryPeerConnection(browser);
+
+ Events.expect("peer-request-allowed");
+
+ ok(!blocker1Called, "Removed peer connection blocker is not invoked");
+ ok(blocker2Called, "Second peer connection blocker was invoked");
+
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Blocker that throws is ignored",
+ run: async function testBlockerThrows(browser) {
+ Events.on();
+ let blocker1Called = false,
+ blocker1 = params => {
+ blocker1Called = true;
+ throw new Error("kaboom");
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker1);
+
+ let blocker2Called = false,
+ blocker2 = params => {
+ blocker2Called = true;
+ return "allow";
+ };
+ webrtcUI.addPeerConnectionBlocker(blocker2);
+
+ await tryPeerConnection(browser);
+
+ Events.expect("peer-request-allowed");
+ ok(blocker1Called, "First blocker was invoked");
+ ok(blocker2Called, "Second blocker was invoked");
+
+ webrtcUI.removePeerConnectionBlocker(blocker1);
+ webrtcUI.removePeerConnectionBlocker(blocker2);
+ Events.off();
+ },
+ },
+
+ {
+ desc: "Cancel peer request",
+ run: async function testBlockerCancel(browser) {
+ let blocker,
+ blockerPromise = new Promise(resolve => {
+ blocker = params => {
+ resolve();
+ // defer indefinitely
+ return new Promise(innerResolve => {});
+ };
+ });
+ webrtcUI.addPeerConnectionBlocker(blocker);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ new content.RTCPeerConnection().createOffer({
+ offerToReceiveAudio: true,
+ });
+ });
+
+ await blockerPromise;
+
+ let eventPromise = new Promise(resolve => {
+ webrtcUI.on("peer-request-cancel", function listener(details) {
+ resolve(details);
+ webrtcUI.off("peer-request-cancel", listener);
+ });
+ });
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.location.reload();
+ });
+
+ let details = await eventPromise;
+ isnot(
+ details.callID,
+ undefined,
+ "peer-request-cancel event includes callID"
+ );
+ is(
+ details.origin,
+ ORIGIN,
+ "peer-request-cancel event has correct origin"
+ );
+
+ webrtcUI.removePeerConnectionBlocker(blocker);
+ },
+ },
+];
+
+add_task(async function test() {
+ await runTests(gTests, {
+ skipObserverVerification: true,
+ cleanup() {
+ is(
+ webrtcUI.peerConnectionBlockers.size,
+ 0,
+ "Peer connection blockers list is empty"
+ );
+ },
+ });
+});
diff --git a/browser/base/content/test/webrtc/get_user_media.html b/browser/base/content/test/webrtc/get_user_media.html
new file mode 100644
index 0000000000..8003785e14
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML += `${m}<br>`;
+ top.postMessage(m, "*");
+}
+
+var gStreams = [];
+var gVideoEvents = [];
+var gAudioEvents = [];
+
+async function requestDevice(aAudio, aVideo, aShare, aBadDevice = false) {
+ const opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ }
+ if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ if (aVideo && aBadDevice) {
+ opts.video = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ if (aAudio && aBadDevice) {
+ opts.audio = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia(opts)
+ gStreams.push(stream);
+
+ const videoTrack = stream.getVideoTracks()[0];
+ if (videoTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ videoTrack.addEventListener(name, () => gVideoEvents.push(name));
+ }
+ }
+
+ const audioTrack = stream.getAudioTracks()[0];
+ if (audioTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ audioTrack.addEventListener(name, () => gAudioEvents.push(name));
+ }
+ }
+ message("ok");
+ } catch (err) {
+ message("error: " + err);
+ }
+}
+
+let selectedAudioOutputId;
+async function requestAudioOutput(options = {}) {
+ const audioOutputOptions = options.requestSameDevice && {
+ deviceId: selectedAudioOutputId,
+ };
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ try {
+ ({ deviceId: selectedAudioOutputId } =
+ await navigator.mediaDevices.selectAudioOutput(audioOutputOptions));
+ message("ok");
+ } catch (err) {
+ message("error: " + err);
+ }
+}
+
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+ if (aKind == "video") {
+ gVideoEvents = [];
+ } else if (aKind == "audio") {
+ gAudioEvents = [];
+ }
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ gVideoEvents = [];
+ gAudioEvents = [];
+ message("closed");
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media2.html b/browser/base/content/test/webrtc/get_user_media2.html
new file mode 100644
index 0000000000..810b00d47b
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media2.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML += `${m}<br>`;
+ top.postMessage(m, "*");
+}
+
+var gStreams = [];
+var gVideoEvents = [];
+var gAudioEvents = [];
+
+async function requestDevice(aAudio, aVideo, aShare, aBadDevice = false) {
+ const opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ }
+ if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ if (aVideo && aBadDevice) {
+ opts.video = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ if (aAudio && aBadDevice) {
+ opts.audio = {
+ deviceId: "bad device",
+ };
+ opts.fake = true;
+ }
+
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia(opts)
+ gStreams.push(stream);
+
+ const videoTrack = stream.getVideoTracks()[0];
+ if (videoTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ videoTrack.addEventListener(name, () => gVideoEvents.push(name));
+ }
+ }
+
+ const audioTrack = stream.getAudioTracks()[0];
+ if (audioTrack) {
+ for (const name of ["mute", "unmute", "ended"]) {
+ audioTrack.addEventListener(name, () => gAudioEvents.push(name));
+ }
+ }
+ message("ok");
+ } catch (err) {
+ message("error: " + err);
+ }
+}
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+ if (aKind == "video") {
+ gVideoEvents = [];
+ } else if (aKind == "audio") {
+ gAudioEvents = [];
+ }
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ gVideoEvents = [];
+ gAudioEvents = [];
+ message("closed");
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media_in_frame.html b/browser/base/content/test/webrtc/get_user_media_in_frame.html
new file mode 100644
index 0000000000..5bffd9cb8c
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_frame.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML += `${m}<br>`;
+ window.top.postMessage(m, "*");
+}
+
+var gStreams = [];
+
+function requestDevice(aAudio, aVideo, aShare) {
+ var opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ } else if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ window.navigator.mediaDevices.getUserMedia(opts)
+ .then(stream => {
+ gStreams.push(stream);
+ message("ok");
+ }, err => message("error: " + err));
+}
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ message("closed");
+}
+
+const query = document.location.search.substring(1);
+const params = new URLSearchParams(query);
+const origins = params.getAll("origin");
+const nested = params.getAll("nested");
+const gumpage = nested.length
+ ? "get_user_media_in_frame.html"
+ : "get_user_media.html";
+let id = 1;
+if (!origins.length) {
+ for(let i = 0; i < 2; ++i) {
+ const iframe = document.createElement("iframe");
+ iframe.id = `frame${id++}`;
+ iframe.src = gumpage;
+ document.body.appendChild(iframe);
+ }
+} else {
+ for (let origin of origins) {
+ const iframe = document.createElement("iframe");
+ iframe.id = `frame${id++}`;
+ const base = new URL("browser/browser/base/content/test/webrtc/", origin).href;
+ const url = new URL(gumpage, base);
+ for (let nestedOrigin of nested) {
+ url.searchParams.append("origin", nestedOrigin);
+ }
+ iframe.src = url.href;
+ iframe.allow = "camera;microphone";
+ iframe.style = `width:${300 * Math.max(1, nested.length) + (nested.length ? 50 : 0)}px;`;
+ document.body.appendChild(iframe);
+ }
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html
new file mode 100644
index 0000000000..6a1c88cbe1
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML += `${m}<br>`;
+ window.parent.postMessage(m, "*");
+}
+
+var gStreams = [];
+
+function requestDevice(aAudio, aVideo, aShare) {
+ var opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = { mediaSource: aShare };
+ } else if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ window.navigator.mediaDevices.getUserMedia(opts)
+ .then(stream => {
+ gStreams.push(stream);
+ message("ok");
+ }, err => message("error: " + err));
+}
+message("pending");
+
+function stopTracks(aKind) {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ if (track.kind == aKind) {
+ track.stop();
+ stream.removeTrack(track);
+ }
+ }
+ }
+ gStreams = gStreams.filter(s => !!s.getTracks().length);
+}
+
+function closeStream() {
+ for (let stream of gStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ gStreams = [];
+ message("closed");
+}
+</script>
+<iframe id="frame1" allow="camera;microphone;display-capture" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+<iframe id="frame2" allow="camera;microphone" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+<iframe id="frame3" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+<iframe id="frame4" allow="camera *;microphone *;display-capture *" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html
new file mode 100644
index 0000000000..bed446a7da
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Permissions Test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+ <iframe id="frameAncestor"
+ src="https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html"
+ allow="camera 'src' https://test1.example.com;microphone 'src' https://test1.example.com;display-capture 'src' https://test1.example.com"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/gracePeriod/browser.ini b/browser/base/content/test/webrtc/gracePeriod/browser.ini
new file mode 100644
index 0000000000..0f9503fe81
--- /dev/null
+++ b/browser/base/content/test/webrtc/gracePeriod/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files =
+ ../get_user_media.html
+ ../get_user_media_in_frame.html
+ ../get_user_media_in_xorigin_frame.html
+ ../get_user_media_in_xorigin_frame_ancestor.html
+ ../head.js
+prefs =
+ privacy.webrtc.allowSilencingNotifications=true
+ privacy.webrtc.legacyGlobalIndicator=false
+ privacy.webrtc.sharedTabWarning=false
+
+[../browser_devices_get_user_media_grace.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js
new file mode 100644
index 0000000000..13526e91b6
--- /dev/null
+++ b/browser/base/content/test/webrtc/head.js
@@ -0,0 +1,1338 @@
+var { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
+const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev";
+const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev";
+const PREF_FAKE_STREAMS = "media.navigator.streams.fake";
+const PREF_FOCUS_SOURCE = "media.getusermedia.window.focus_source.enabled";
+
+const STATE_CAPTURE_ENABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
+const STATE_CAPTURE_DISABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
+
+const USING_LEGACY_INDICATOR = Services.prefs.getBoolPref(
+ "privacy.webrtc.legacyGlobalIndicator",
+ false
+);
+
+const ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref(
+ "privacy.webrtc.allowSilencingNotifications",
+ false
+);
+
+const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref(
+ "privacy.webrtc.globalMuteToggles",
+ false
+);
+
+const INDICATOR_PATH = USING_LEGACY_INDICATOR
+ ? "chrome://browser/content/webrtcLegacyIndicator.xhtml"
+ : "chrome://browser/content/webrtcIndicator.xhtml";
+
+const IS_MAC = AppConstants.platform == "macosx";
+
+const SHARE_SCREEN = 1;
+const SHARE_WINDOW = 2;
+
+let observerTopics = [
+ "getUserMedia:response:allow",
+ "getUserMedia:revoke",
+ "getUserMedia:response:deny",
+ "getUserMedia:request",
+ "recording-device-events",
+ "recording-window-ended",
+];
+
+// Structured hierarchy of subframes. Keys are frame id:s, The children member
+// contains nested sub frames if any. The noTest member make a frame be ignored
+// for testing if true.
+let gObserveSubFrames = {};
+// Object of subframes to test. Each element contains the members bc and id, for
+// the frames BrowsingContext and id, respectively.
+let gSubFramesToTest = [];
+let gBrowserContextsToObserve = [];
+
+function whenDelayedStartupFinished(aWindow) {
+ return TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == aWindow
+ );
+}
+
+function promiseIndicatorWindow() {
+ let startTime = performance.now();
+
+ // We don't show the legacy indicator window on Mac.
+ if (USING_LEGACY_INDICATOR && IS_MAC) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(win) {
+ win.addEventListener(
+ "load",
+ function () {
+ if (win.location.href !== INDICATOR_PATH) {
+ info("ignoring a window with this url: " + win.location.href);
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "domwindowopened");
+ executeSoon(() => {
+ ChromeUtils.addProfilerMarker("promiseIndicatorWindow", {
+ startTime,
+ category: "Test",
+ });
+ resolve(win);
+ });
+ },
+ { once: true }
+ );
+ }, "domwindowopened");
+ });
+}
+
+async function assertWebRTCIndicatorStatus(expected) {
+ let ui = ChromeUtils.import("resource:///modules/webrtcUI.jsm").webrtcUI;
+ let expectedState = expected ? "visible" : "hidden";
+ let msg = "WebRTC indicator " + expectedState;
+ if (!expected && ui.showGlobalIndicator) {
+ // It seems the global indicator is not always removed synchronously
+ // in some cases.
+ await TestUtils.waitForCondition(
+ () => !ui.showGlobalIndicator,
+ "waiting for the global indicator to be hidden"
+ );
+ }
+ is(ui.showGlobalIndicator, !!expected, msg);
+
+ let expectVideo = false,
+ expectAudio = false,
+ expectScreen = "";
+ if (expected) {
+ if (expected.video) {
+ expectVideo = true;
+ }
+ if (expected.audio) {
+ expectAudio = true;
+ }
+ if (expected.screen) {
+ expectScreen = expected.screen;
+ }
+ }
+ is(
+ Boolean(ui.showCameraIndicator),
+ expectVideo,
+ "camera global indicator as expected"
+ );
+ is(
+ Boolean(ui.showMicrophoneIndicator),
+ expectAudio,
+ "microphone global indicator as expected"
+ );
+ is(
+ ui.showScreenSharingIndicator,
+ expectScreen,
+ "screen global indicator as expected"
+ );
+
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ let menu = win.document.getElementById("tabSharingMenu");
+ is(
+ !!menu && !menu.hidden,
+ !!expected,
+ "WebRTC menu should be " + expectedState
+ );
+ }
+
+ if (USING_LEGACY_INDICATOR && IS_MAC) {
+ return;
+ }
+
+ if (!expected) {
+ let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
+ if (win) {
+ await new Promise((resolve, reject) => {
+ win.addEventListener("unload", function listener(e) {
+ if (e.target == win.document) {
+ win.removeEventListener("unload", listener);
+ executeSoon(resolve);
+ }
+ });
+ });
+ }
+ }
+
+ let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator");
+ let hasWindow = indicator.hasMoreElements();
+ is(hasWindow, !!expected, "popup " + msg);
+ if (hasWindow) {
+ let document = indicator.getNext().document;
+ let docElt = document.documentElement;
+
+ if (document.readyState != "complete") {
+ info("Waiting for the sharing indicator's document to load");
+ await new Promise(resolve => {
+ document.addEventListener(
+ "readystatechange",
+ function onReadyStateChange() {
+ if (document.readyState != "complete") {
+ return;
+ }
+ document.removeEventListener(
+ "readystatechange",
+ onReadyStateChange
+ );
+ executeSoon(resolve);
+ }
+ );
+ });
+ }
+
+ if (
+ !USING_LEGACY_INDICATOR &&
+ expected.screen &&
+ expected.screen.startsWith("Window")
+ ) {
+ // These tests were originally written to express window sharing by
+ // having expected.screen start with "Window". This meant that the
+ // legacy indicator is expected to have the "sharingscreen" attribute
+ // set to true when sharing a window.
+ //
+ // The new indicator, however, differentiates between screen, window
+ // and browser window sharing. If we're using the new indicator, we
+ // update the expectations accordingly. This can be removed once we
+ // are able to remove the tests for the legacy indicator.
+ expected.screen = null;
+ expected.window = true;
+ }
+
+ if (!USING_LEGACY_INDICATOR && !SHOW_GLOBAL_MUTE_TOGGLES) {
+ expected.video = false;
+ expected.audio = false;
+
+ let visible = docElt.getAttribute("visible") == "true";
+
+ if (!expected.screen && !expected.window && !expected.browserwindow) {
+ ok(!visible, "Indicator should not be visible in this configuation.");
+ } else {
+ ok(visible, "Indicator should be visible.");
+ }
+ }
+
+ for (let item of ["video", "audio", "screen", "window", "browserwindow"]) {
+ let expectedValue;
+
+ if (USING_LEGACY_INDICATOR) {
+ expectedValue = expected && expected[item] ? "true" : "";
+ } else {
+ expectedValue = expected && expected[item] ? "true" : null;
+ }
+
+ is(
+ docElt.getAttribute("sharing" + item),
+ expectedValue,
+ item + " global indicator attribute as expected"
+ );
+ }
+
+ ok(!indicator.hasMoreElements(), "only one global indicator window");
+ }
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = BrowserTestUtils.waitForPopupEvent(
+ win.PopupNotifications.panel,
+ "shown"
+ );
+ notification.reshow();
+ return panelPromise;
+}
+
+function ignoreEvent(aSubject, aTopic, aData) {
+ // With e10s disabled, our content script receives notifications for the
+ // preview displayed in our screen sharing permission prompt; ignore them.
+ const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
+ const nsIPropertyBag = Ci.nsIPropertyBag;
+ if (
+ aTopic == "recording-device-events" &&
+ aSubject.QueryInterface(nsIPropertyBag).getProperty("requestURL") ==
+ kBrowserURL
+ ) {
+ return true;
+ }
+ if (aTopic == "recording-window-ended") {
+ let win = Services.wm.getOuterWindowWithId(aData).top;
+ if (win.document.documentURI == kBrowserURL) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function expectObserverCalledInProcess(aTopic, aCount = 1) {
+ let promises = [];
+ for (let count = aCount; count > 0; count--) {
+ promises.push(TestUtils.topicObserved(aTopic, ignoreEvent));
+ }
+ return promises;
+}
+
+function expectObserverCalled(
+ aTopic,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic, aCount);
+ }
+
+ let browsingContext = Element.isInstance(browser)
+ ? browser.browsingContext
+ : browser;
+
+ return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic, aCount);
+}
+
+// This is a special version of expectObserverCalled that should only
+// be used when expecting a notification upon closing a window. It uses
+// the per-process message manager instead of actors to send the
+// notifications.
+function expectObserverCalledOnClose(
+ aTopic,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic, aCount);
+ }
+
+ let browsingContext = Element.isInstance(browser)
+ ? browser.browsingContext
+ : browser;
+
+ return new Promise(resolve => {
+ BrowserTestUtils.sendAsyncMessage(
+ browsingContext,
+ "BrowserTestUtils:ObserveTopic",
+ {
+ topic: aTopic,
+ count: 1,
+ filterFunctionSource: ((subject, topic, data) => {
+ Services.cpmm.sendAsyncMessage("WebRTCTest:ObserverCalled", {
+ topic,
+ });
+ return true;
+ }).toSource(),
+ }
+ );
+
+ function observerCalled(message) {
+ if (message.data.topic == aTopic) {
+ Services.ppmm.removeMessageListener(
+ "WebRTCTest:ObserverCalled",
+ observerCalled
+ );
+ resolve();
+ }
+ }
+ Services.ppmm.addMessageListener(
+ "WebRTCTest:ObserverCalled",
+ observerCalled
+ );
+ });
+}
+
+function promiseMessage(
+ aMessage,
+ aAction,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ let startTime = performance.now();
+ let promise = ContentTask.spawn(
+ browser,
+ [aMessage, aCount],
+ async function ([expectedMessage, expectedCount]) {
+ return new Promise(resolve => {
+ function listenForMessage({ data }) {
+ if (
+ (!expectedMessage || data == expectedMessage) &&
+ --expectedCount == 0
+ ) {
+ content.removeEventListener("message", listenForMessage);
+ resolve(data);
+ }
+ }
+ content.addEventListener("message", listenForMessage);
+ });
+ }
+ );
+ if (aAction) {
+ aAction();
+ }
+ return promise.then(data => {
+ ChromeUtils.addProfilerMarker(
+ "promiseMessage",
+ { startTime, category: "Test" },
+ data
+ );
+ return data;
+ });
+}
+
+function promisePopupNotificationShown(aName, aAction, aWindow = window) {
+ let startTime = performance.now();
+ return new Promise(resolve => {
+ aWindow.PopupNotifications.panel.addEventListener(
+ "popupshown",
+ function () {
+ ok(
+ !!aWindow.PopupNotifications.getNotification(aName),
+ aName + " notification shown"
+ );
+ ok(aWindow.PopupNotifications.isPanelOpen, "notification panel open");
+ ok(
+ !!aWindow.PopupNotifications.panel.firstElementChild,
+ "notification panel populated"
+ );
+
+ executeSoon(() => {
+ ChromeUtils.addProfilerMarker(
+ "promisePopupNotificationShown",
+ { startTime, category: "Test" },
+ aName
+ );
+ resolve();
+ });
+ },
+ { once: true }
+ );
+
+ if (aAction) {
+ aAction();
+ }
+ });
+}
+
+async function promisePopupNotification(aName) {
+ return TestUtils.waitForCondition(
+ () => PopupNotifications.getNotification(aName),
+ aName + " notification appeared"
+ );
+}
+
+async function promiseNoPopupNotification(aName) {
+ return TestUtils.waitForCondition(
+ () => !PopupNotifications.getNotification(aName),
+ aName + " notification removed"
+ );
+}
+
+const kActionAlways = 1;
+const kActionDeny = 2;
+const kActionNever = 3;
+
+async function activateSecondaryAction(aAction) {
+ let notification = PopupNotifications.panel.firstElementChild;
+ switch (aAction) {
+ case kActionNever:
+ if (notification.notification.secondaryActions.length > 1) {
+ // "Always Block" is the first (and only) item in the menupopup.
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(notification.menupopup, "popupshown"),
+ notification.menubutton.click(),
+ ]);
+ notification.menupopup.querySelector("menuitem").click();
+ return;
+ }
+ if (!notification.checkbox.checked) {
+ notification.checkbox.click();
+ }
+ // fallthrough
+ case kActionDeny:
+ notification.secondaryButton.click();
+ break;
+ case kActionAlways:
+ if (!notification.checkbox.checked) {
+ notification.checkbox.click();
+ }
+ notification.button.click();
+ break;
+ }
+}
+
+async function getMediaCaptureState() {
+ let startTime = performance.now();
+
+ function gatherBrowsingContexts(aBrowsingContext) {
+ let list = [aBrowsingContext];
+
+ let children = aBrowsingContext.children;
+ for (let child of children) {
+ list.push(...gatherBrowsingContexts(child));
+ }
+
+ return list;
+ }
+
+ function combine(x, y) {
+ if (
+ x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
+ y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED
+ ) {
+ return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
+ }
+ if (
+ x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED ||
+ y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
+ ) {
+ return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
+ }
+ return Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ }
+
+ let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+
+ for (let bc of gatherBrowsingContexts(
+ gBrowser.selectedBrowser.browsingContext
+ )) {
+ let state = await SpecialPowers.spawn(bc, [], async function () {
+ let mediaManagerService = Cc[
+ "@mozilla.org/mediaManagerService;1"
+ ].getService(Ci.nsIMediaManagerService);
+
+ let hasCamera = {};
+ let hasMicrophone = {};
+ let hasScreenShare = {};
+ let hasWindowShare = {};
+ let hasBrowserShare = {};
+ let devices = {};
+ mediaManagerService.mediaCaptureWindowState(
+ content,
+ hasCamera,
+ hasMicrophone,
+ hasScreenShare,
+ hasWindowShare,
+ hasBrowserShare,
+ devices,
+ false
+ );
+
+ return {
+ video: hasCamera.value,
+ audio: hasMicrophone.value,
+ screen: hasScreenShare.value,
+ window: hasWindowShare.value,
+ browser: hasBrowserShare.value,
+ };
+ });
+
+ video = combine(state.video, video);
+ audio = combine(state.audio, audio);
+ screen = combine(state.screen, screen);
+ window = combine(state.window, window);
+ browser = combine(state.browser, browser);
+ }
+
+ let result = {};
+
+ if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.video = true;
+ }
+ if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.audio = true;
+ }
+
+ if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Screen";
+ } else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Window";
+ } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Browser";
+ }
+
+ ChromeUtils.addProfilerMarker("getMediaCaptureState", {
+ startTime,
+ category: "Test",
+ });
+ return result;
+}
+
+async function stopSharing(
+ aType = "camera",
+ aShouldKeepSharing = false,
+ aFrameBC,
+ aWindow = window
+) {
+ let promiseRecordingEvent = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aFrameBC
+ );
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:revoke",
+ 1,
+ aFrameBC
+ );
+
+ // If we are stopping screen sharing and expect to still have another stream,
+ // "recording-window-ended" won't be fired.
+ let observerPromise2 = null;
+ if (!aShouldKeepSharing) {
+ observerPromise2 = expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ aFrameBC
+ );
+ }
+
+ await revokePermission(aType, aShouldKeepSharing, aFrameBC, aWindow);
+ await promiseRecordingEvent;
+ await observerPromise1;
+ await observerPromise2;
+
+ if (!aShouldKeepSharing) {
+ await checkNotSharing();
+ }
+}
+
+async function revokePermission(
+ aType = "camera",
+ aShouldKeepSharing = false,
+ aFrameBC,
+ aWindow = window
+) {
+ aWindow.gPermissionPanel._identityPermissionBox.click();
+ let popup = aWindow.gPermissionPanel._permissionPopup;
+ // If the popup gets hidden before being shown, by stray focus/activate
+ // events, don't bother failing the test. It's enough to know that we
+ // started showing the popup.
+ let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ await Promise.race([hiddenEvent, shownEvent]);
+ let doc = aWindow.document;
+ let permissions = doc.getElementById("permission-popup-permission-list");
+ let cancelButton = permissions.querySelector(
+ ".permission-popup-permission-icon." +
+ aType +
+ "-icon ~ " +
+ ".permission-popup-permission-remove-button"
+ );
+
+ cancelButton.click();
+ popup.hidePopup();
+
+ if (!aShouldKeepSharing) {
+ await checkNotSharing();
+ }
+}
+
+function getBrowsingContextForFrame(aBrowsingContext, aFrameId) {
+ if (!aFrameId) {
+ return aBrowsingContext;
+ }
+
+ return SpecialPowers.spawn(aBrowsingContext, [aFrameId], frameId => {
+ return content.document.getElementById(frameId).browsingContext;
+ });
+}
+
+async function getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowsingContext,
+ aSubFrames
+) {
+ let pendingBrowserSubFrames = [
+ { bc: aBrowsingContext, subFrames: aSubFrames },
+ ];
+ let browsingContextsAndFrames = [];
+ while (pendingBrowserSubFrames.length) {
+ let { bc, subFrames } = pendingBrowserSubFrames.shift();
+ for (let id of Object.keys(subFrames)) {
+ let subBc = await getBrowsingContextForFrame(bc, id);
+ if (subFrames[id].children) {
+ pendingBrowserSubFrames.push({
+ bc: subBc,
+ subFrames: subFrames[id].children,
+ });
+ }
+ if (subFrames[id].noTest) {
+ continue;
+ }
+ let observeBC = subFrames[id].observe ? subBc : undefined;
+ browsingContextsAndFrames.push({ bc: subBc, id, observeBC });
+ }
+ }
+ return browsingContextsAndFrames;
+}
+
+async function promiseRequestDevice(
+ aRequestAudio,
+ aRequestVideo,
+ aFrameId,
+ aType,
+ aBrowsingContext,
+ aBadDevice = false
+) {
+ info("requesting devices");
+ let bc =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(gBrowser.selectedBrowser, aFrameId));
+ return SpecialPowers.spawn(
+ bc,
+ [{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
+ async function (args) {
+ let global = content.wrappedJSObject;
+ global.requestDevice(
+ args.aRequestAudio,
+ args.aRequestVideo,
+ args.aType,
+ args.aBadDevice
+ );
+ }
+ );
+}
+
+async function promiseRequestAudioOutput(options) {
+ info("requesting audio output");
+ const bc = gBrowser.selectedBrowser;
+ return SpecialPowers.spawn(bc, [options], async function (opts) {
+ const global = content.wrappedJSObject;
+ global.requestAudioOutput(Cu.cloneInto(opts, content));
+ });
+}
+
+async function stopTracks(
+ aKind,
+ aAlreadyStopped,
+ aLastTracks,
+ aFrameId,
+ aBrowsingContext,
+ aBrowsingContextToObserve
+) {
+ // If the observers are listening to other frames, listen for a notification
+ // on the right subframe.
+ let frameBC =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(
+ gBrowser.selectedBrowser.browsingContext,
+ aFrameId
+ ));
+
+ let observerPromises = [];
+ if (!aAlreadyStopped) {
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ }
+ if (aLastTracks) {
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ }
+
+ info(`Stopping all ${aKind} tracks`);
+ await SpecialPowers.spawn(frameBC, [aKind], async function (kind) {
+ content.wrappedJSObject.stopTracks(kind);
+ });
+
+ await Promise.all(observerPromises);
+}
+
+async function closeStream(
+ aAlreadyClosed,
+ aFrameId,
+ aDontFlushObserverVerification,
+ aBrowsingContext,
+ aBrowsingContextToObserve
+) {
+ // Check that spurious notifications that occur while closing the
+ // stream are handled separately. Tests that use skipObserverVerification
+ // should pass true for aDontFlushObserverVerification.
+ if (!aDontFlushObserverVerification) {
+ await disableObserverVerification();
+ await enableObserverVerification();
+ }
+
+ // If the observers are listening to other frames, listen for a notification
+ // on the right subframe.
+ let frameBC =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(
+ gBrowser.selectedBrowser.browsingContext,
+ aFrameId
+ ));
+
+ let observerPromises = [];
+ if (!aAlreadyClosed) {
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ observerPromises.push(
+ expectObserverCalled(
+ "recording-window-ended",
+ 1,
+ aBrowsingContextToObserve
+ )
+ );
+ }
+
+ info("closing the stream");
+ await SpecialPowers.spawn(frameBC, [], async function () {
+ content.wrappedJSObject.closeStream();
+ });
+
+ await Promise.all(observerPromises);
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+async function reloadAsUser() {
+ info("reloading as a user");
+
+ const reloadButton = document.getElementById("reload-button");
+ await TestUtils.waitForCondition(() => !reloadButton.disabled);
+ // Disable observers as the page is being reloaded which can destroy
+ // the actors listening to the notifications.
+ await disableObserverVerification();
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ reloadButton.click();
+ await loadedPromise;
+
+ await enableObserverVerification();
+}
+
+async function reloadFromContent() {
+ info("reloading from content");
+
+ // Disable observers as the page is being reloaded which can destroy
+ // the actors listening to the notifications.
+ await disableObserverVerification();
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await ContentTask.spawn(gBrowser.selectedBrowser, null, () =>
+ content.location.reload()
+ );
+
+ await loadedPromise;
+
+ await enableObserverVerification();
+}
+
+async function reloadAndAssertClosedStreams() {
+ await reloadFromContent();
+ await checkNotSharing();
+}
+
+/**
+ * @param {("microphone"|"camera"|"screen")[]} aExpectedTypes
+ * @param {Window} [aWindow]
+ */
+function checkDeviceSelectors(aExpectedTypes, aWindow = window) {
+ for (const type of aExpectedTypes) {
+ if (!["microphone", "camera", "screen", "speaker"].includes(type)) {
+ throw new Error(`Bad device type name ${type}`);
+ }
+ }
+ let document = aWindow.document;
+
+ let expectedDescribedBy = "webRTC-shareDevices-notification-description";
+ for (let type of ["Camera", "Microphone", "Speaker"]) {
+ let selector = document.getElementById(`webRTC-select${type}`);
+ if (!aExpectedTypes.includes(type.toLowerCase())) {
+ ok(selector.hidden, `${type} selector hidden`);
+ continue;
+ }
+ ok(!selector.hidden, `${type} selector visible`);
+ let selectorList = document.getElementById(`webRTC-select${type}-menulist`);
+ let label = document.getElementById(
+ `webRTC-select${type}-single-device-label`
+ );
+ // If there's only 1 device listed, then we should show the label
+ // instead of the menulist.
+ if (selectorList.itemCount == 1) {
+ ok(selectorList.hidden, `${type} selector list should be hidden.`);
+ ok(!label.hidden, `${type} selector label should not be hidden.`);
+ is(
+ label.value,
+ selectorList.selectedItem.getAttribute("label"),
+ `${type} label should be showing the lone device label.`
+ );
+ expectedDescribedBy += ` webRTC-select${type}-icon webRTC-select${type}-single-device-label`;
+ } else {
+ ok(!selectorList.hidden, `${type} selector list should not be hidden.`);
+ ok(label.hidden, `${type} selector label should be hidden.`);
+ }
+ }
+ let ariaDescribedby =
+ aWindow.PopupNotifications.panel.getAttribute("aria-describedby");
+ is(ariaDescribedby, expectedDescribedBy, "aria-describedby");
+
+ let screenSelector = document.getElementById("webRTC-selectWindowOrScreen");
+ if (aExpectedTypes.includes("screen")) {
+ ok(!screenSelector.hidden, "screen selector visible");
+ } else {
+ ok(screenSelector.hidden, "screen selector hidden");
+ }
+}
+
+/**
+ * Tests the siteIdentity icons, the permission panel and the global indicator
+ * UI state.
+ * @param {Object} aExpected - Expected state for the current tab.
+ * @param {window} [aWin] - Top level chrome window to test state of.
+ * @param {Object} [aExpectedGlobal] - Expected state for all tabs.
+ * @param {Object} [aExpectedPerm] - Expected permission states keyed by device
+ * type.
+ */
+async function checkSharingUI(
+ aExpected,
+ aWin = window,
+ aExpectedGlobal = null,
+ aExpectedPerm = null
+) {
+ function isPaused(streamState) {
+ if (typeof streamState == "string") {
+ return streamState.includes("Paused");
+ }
+ return streamState == STATE_CAPTURE_DISABLED;
+ }
+
+ let doc = aWin.document;
+ // First check the icon above the control center (i) icon.
+ let permissionBox = doc.getElementById("identity-permission-box");
+ let webrtcSharingIcon = doc.getElementById("webrtc-sharing-icon");
+ ok(webrtcSharingIcon.hasAttribute("sharing"), "sharing attribute is set");
+ let sharing = webrtcSharingIcon.getAttribute("sharing");
+ if (aExpected.screen) {
+ is(sharing, "screen", "showing screen icon in the identity block");
+ } else if (aExpected.video == STATE_CAPTURE_ENABLED) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio == STATE_CAPTURE_ENABLED) {
+ is(sharing, "microphone", "showing mic icon in the identity block");
+ } else if (aExpected.video) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio) {
+ is(sharing, "microphone", "showing mic icon in the identity block");
+ }
+
+ let allStreamsPaused = Object.values(aExpected).every(isPaused);
+ is(
+ webrtcSharingIcon.hasAttribute("paused"),
+ allStreamsPaused,
+ "sharing icon(s) should be in paused state when paused"
+ );
+
+ // Then check the sharing indicators inside the permission popup.
+ permissionBox.click();
+ let popup = aWin.gPermissionPanel._permissionPopup;
+ // If the popup gets hidden before being shown, by stray focus/activate
+ // events, don't bother failing the test. It's enough to know that we
+ // started showing the popup.
+ let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ await Promise.race([hiddenEvent, shownEvent]);
+ let permissions = doc.getElementById("permission-popup-permission-list");
+ for (let id of ["microphone", "camera", "screen"]) {
+ let convertId = idToConvert => {
+ if (idToConvert == "camera") {
+ return "video";
+ }
+ if (idToConvert == "microphone") {
+ return "audio";
+ }
+ return idToConvert;
+ };
+ let expected = aExpected[convertId(id)];
+
+ // Extract the expected permission for the device type.
+ // Defaults to temporary allow.
+ let { state, scope } = aExpectedPerm?.[convertId(id)] || {};
+ if (state == null) {
+ state = SitePermissions.ALLOW;
+ }
+ if (scope == null) {
+ scope = SitePermissions.SCOPE_TEMPORARY;
+ }
+
+ is(
+ !!aWin.gPermissionPanel._sharingState.webRTC[id],
+ !!expected,
+ "sharing state for " + id + " as expected"
+ );
+ let item = permissions.querySelectorAll(
+ ".permission-popup-permission-item-" + id
+ );
+ let stateLabel = item?.[0]?.querySelector(
+ ".permission-popup-permission-state-label"
+ );
+ let icon = permissions.querySelectorAll(
+ ".permission-popup-permission-icon." + id + "-icon"
+ );
+ if (expected) {
+ is(item.length, 1, "should show " + id + " item in permission panel");
+ is(
+ stateLabel?.textContent,
+ SitePermissions.getCurrentStateLabel(state, id, scope),
+ "should show correct item label for " + id
+ );
+ is(icon.length, 1, "should show " + id + " icon in permission panel");
+ is(
+ icon[0].classList.contains("in-use"),
+ expected && !isPaused(expected),
+ "icon should have the in-use class, unless paused"
+ );
+ } else if (!icon.length && !item.length && !stateLabel) {
+ ok(true, "should not show " + id + " item in the permission panel");
+ ok(true, "should not show " + id + " icon in the permission panel");
+ ok(
+ true,
+ "should not show " + id + " state label in the permission panel"
+ );
+ } else {
+ // This will happen if there are persistent permissions set.
+ ok(
+ !icon[0].classList.contains("in-use"),
+ "if shown, the " + id + " icon should not have the in-use class"
+ );
+ is(item.length, 1, "should not show more than 1 " + id + " item");
+ is(icon.length, 1, "should not show more than 1 " + id + " icon");
+ }
+ }
+ aWin.gPermissionPanel._permissionPopup.hidePopup();
+ await TestUtils.waitForCondition(
+ () => permissionPopupHidden(aWin),
+ "identity popup should be hidden"
+ );
+
+ // Check the global indicators.
+ await assertWebRTCIndicatorStatus(aExpectedGlobal || aExpected);
+}
+
+async function checkNotSharing() {
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ {},
+ "expected nothing to be shared"
+ );
+
+ ok(
+ !document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
+ "no sharing indicator on the control center icon"
+ );
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+async function checkNotSharingWithinGracePeriod() {
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ {},
+ "expected nothing to be shared"
+ );
+
+ ok(
+ document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
+ "has sharing indicator on the control center icon"
+ );
+ ok(
+ document.getElementById("webrtc-sharing-icon").hasAttribute("paused"),
+ "sharing indicator is paused"
+ );
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+async function promiseReloadFrame(aFrameId, aBrowsingContext) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true,
+ arg => {
+ return true;
+ }
+ );
+ let bc =
+ aBrowsingContext ??
+ (await getBrowsingContextForFrame(
+ gBrowser.selectedBrowser.browsingContext,
+ aFrameId
+ ));
+ await SpecialPowers.spawn(bc, [], async function () {
+ content.location.reload();
+ });
+ return loadedPromise;
+}
+
+function promiseChangeLocationFrame(aFrameId, aNewLocation) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser.browsingContext,
+ [{ aFrameId, aNewLocation }],
+ async function (args) {
+ let frame = content.wrappedJSObject.document.getElementById(
+ args.aFrameId
+ );
+ return new Promise(resolve => {
+ function listener() {
+ frame.removeEventListener("load", listener, true);
+ resolve();
+ }
+ frame.addEventListener("load", listener, true);
+
+ content.wrappedJSObject.document.getElementById(
+ args.aFrameId
+ ).contentWindow.location = args.aNewLocation;
+ });
+ }
+ );
+}
+
+async function openNewTestTab(leaf = "get_user_media.html") {
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+ );
+ let absoluteURI = rootDir + leaf;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, absoluteURI);
+ return tab.linkedBrowser;
+}
+
+// Enabling observer verification adds listeners for all of the webrtc
+// observer topics. If any notifications occur for those topics that
+// were not explicitly requested, a failure will occur.
+async function enableObserverVerification(browser = gBrowser.selectedBrowser) {
+ // Skip these checks in single process mode as it isn't worth implementing it.
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ gBrowserContextsToObserve = [browser.browsingContext];
+
+ // A list of subframe indicies to also add observers to. This only
+ // supports one nested level.
+ if (gObserveSubFrames) {
+ let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
+ browser,
+ gObserveSubFrames
+ );
+ for (let { observeBC } of bcsAndFrameIds) {
+ if (observeBC) {
+ gBrowserContextsToObserve.push(observeBC);
+ }
+ }
+ }
+
+ for (let bc of gBrowserContextsToObserve) {
+ await BrowserTestUtils.startObservingTopics(bc, observerTopics);
+ }
+}
+
+async function disableObserverVerification() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ for (let bc of gBrowserContextsToObserve) {
+ await BrowserTestUtils.stopObservingTopics(bc, observerTopics).catch(
+ reason => {
+ ok(false, "Failed " + reason);
+ }
+ );
+ }
+}
+
+function permissionPopupHidden(win = window) {
+ let popup = win.gPermissionPanel._permissionPopup;
+ return !popup || popup.state == "closed";
+}
+
+async function runTests(tests, options = {}) {
+ let browser = await openNewTestTab(options.relativeURI);
+
+ is(
+ PopupNotifications._currentNotifications.length,
+ 0,
+ "should start the test without any prior popup notification"
+ );
+ ok(
+ permissionPopupHidden(),
+ "should start the test with the permission panel hidden"
+ );
+
+ // Set prefs so that permissions prompts are shown and loopback devices
+ // are not used. To test the chrome we want prompts to be shown, and
+ // these tests are flakey when using loopback devices (though it would
+ // be desirable to make them work with loopback in future). See bug 1643711.
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ // When the frames are in different processes, add observers to each frame,
+ // to ensure that the notifications don't get sent in the wrong process.
+ gObserveSubFrames = SpecialPowers.useRemoteSubframes ? options.subFrames : {};
+
+ for (let testCase of tests) {
+ let startTime = performance.now();
+ info(testCase.desc);
+ if (
+ !testCase.skipObserverVerification &&
+ !options.skipObserverVerification
+ ) {
+ await enableObserverVerification();
+ }
+ await testCase.run(browser, options.subFrames);
+ if (
+ !testCase.skipObserverVerification &&
+ !options.skipObserverVerification
+ ) {
+ await disableObserverVerification();
+ }
+ if (options.cleanup) {
+ await options.cleanup();
+ }
+ ChromeUtils.addProfilerMarker(
+ "browser-test",
+ { startTime, category: "Test" },
+ testCase.desc
+ );
+ }
+
+ // Some tests destroy the original tab and leave a new one in its place.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+/**
+ * Given a browser from a tab in this window, chooses to share
+ * some combination of camera, mic or screen.
+ *
+ * @param {<xul:browser} browser - The browser to share devices with.
+ * @param {boolean} camera - True to share a camera device.
+ * @param {boolean} mic - True to share a microphone device.
+ * @param {Number} [screenOrWin] - One of either SHARE_WINDOW or SHARE_SCREEN
+ * to share a window or screen. Defaults to neither.
+ * @param {boolean} remember - True to persist the permission to the
+ * SitePermissions database as SitePermissions.SCOPE_PERSISTENT. Note that
+ * callers are responsible for clearing this persistent permission.
+ * @return {Promise}
+ * @resolves {undefined} - Once the sharing is complete.
+ */
+async function shareDevices(
+ browser,
+ camera,
+ mic,
+ screenOrWin = 0,
+ remember = false
+) {
+ if (camera || mic) {
+ let promise = promisePopupNotificationShown(
+ "webRTC-shareDevices",
+ null,
+ window
+ );
+
+ await promiseRequestDevice(mic, camera, null, null, browser);
+ await promise;
+
+ const expectedDeviceSelectorTypes = [
+ camera && "camera",
+ mic && "microphone",
+ ].filter(x => x);
+ checkDeviceSelectors(expectedDeviceSelectorTypes);
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+
+ let rememberCheck = PopupNotifications.panel.querySelector(
+ ".popup-notification-checkbox"
+ );
+ rememberCheck.checked = remember;
+
+ promise = promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await promise;
+ }
+
+ if (screenOrWin) {
+ let promise = promisePopupNotificationShown(
+ "webRTC-shareDevices",
+ null,
+ window
+ );
+
+ await promiseRequestDevice(false, true, null, "screen", browser);
+ await promise;
+
+ checkDeviceSelectors(["screen"], window);
+
+ let document = window.document;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let displayMediaSource;
+
+ if (screenOrWin == SHARE_SCREEN) {
+ displayMediaSource = "screen";
+ } else if (screenOrWin == SHARE_WINDOW) {
+ displayMediaSource = "window";
+ } else {
+ throw new Error("Got an invalid argument to shareDevices.");
+ }
+
+ let menuitem = null;
+ for (let i = 0; i < menulist.itemCount; ++i) {
+ let current = menulist.getItemAtIndex(i);
+ if (current.mediaSource == displayMediaSource) {
+ menuitem = current;
+ break;
+ }
+ }
+
+ Assert.ok(menuitem, "Should have found an appropriate display menuitem");
+ menuitem.doCommand();
+
+ let notification = window.PopupNotifications.panel.firstElementChild;
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage(
+ "ok",
+ () => {
+ notification.button.click();
+ },
+ 1,
+ browser
+ );
+ await observerPromise1;
+ await observerPromise2;
+ }
+}
diff --git a/browser/base/content/test/webrtc/legacyIndicator/browser.ini b/browser/base/content/test/webrtc/legacyIndicator/browser.ini
new file mode 100644
index 0000000000..dc1c9e3104
--- /dev/null
+++ b/browser/base/content/test/webrtc/legacyIndicator/browser.ini
@@ -0,0 +1,63 @@
+[DEFAULT]
+support-files =
+ ../get_user_media.html
+ ../get_user_media_in_frame.html
+ ../get_user_media_in_xorigin_frame.html
+ ../get_user_media_in_xorigin_frame_ancestor.html
+ ../head.js
+prefs =
+ privacy.webrtc.allowSilencingNotifications=false
+ privacy.webrtc.legacyGlobalIndicator=true
+ privacy.webrtc.sharedTabWarning=false
+ privacy.webrtc.deviceGracePeriodTimeoutMs=0
+
+[../browser_devices_get_user_media.js]
+skip-if =
+ (os == "linux") # linux: bug 976544, bug 1616011
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_anim.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_default_permissions.js]
+[../browser_devices_get_user_media_in_frame.js]
+skip-if = debug # bug 1369731
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_in_xorigin_frame.js]
+skip-if =
+ debug # bug 1369731
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[../browser_devices_get_user_media_in_xorigin_frame_chain.js]
+[../browser_devices_get_user_media_multi_process.js]
+skip-if =
+ (debug && os == "win") # bug 1393761
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[../browser_devices_get_user_media_paused.js]
+skip-if =
+ (os == "win" && !debug) # Bug 1440900
+ (os =="linux" && !debug && bits == 64) # Bug 1440900
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+[../browser_devices_get_user_media_queue_request.js]
+skip-if =
+ os == "linux" # Bug 1775945
+ os == "win" && !debug # Bug 1775945
+[../browser_devices_get_user_media_screen.js]
+skip-if =
+ (os == 'linux') # Bug 1503991
+ apple_silicon # bug 1707735
+ apple_catalina # platform migration
+ os == "win"
+[../browser_devices_get_user_media_tear_off_tab.js]
+skip-if =
+ os == "linux" # Bug 1775945
+ os == "win" && !debug # Bug 1775945
+[../browser_devices_get_user_media_unprompted_access.js]
+skip-if = (os == "linux") # Bug 1712012
+[../browser_devices_get_user_media_unprompted_access_in_frame.js]
+[../browser_devices_get_user_media_unprompted_access_queue_request.js]
+[../browser_devices_get_user_media_unprompted_access_tear_off_tab.js]
+skip-if = (os == "win" && bits == 64) # win8: bug 1334752
+[../browser_webrtc_hooks.js]
diff --git a/browser/base/content/test/webrtc/peerconnection_connect.html b/browser/base/content/test/webrtc/peerconnection_connect.html
new file mode 100644
index 0000000000..5af6a4aafd
--- /dev/null
+++ b/browser/base/content/test/webrtc/peerconnection_connect.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="Page that opens a two peerconnections, and starts ICE"></div>
+<script>
+ const test = async () => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+
+ async function iceConnected(pc) {
+ return new Promise(r => {
+ if (pc.iceConnectionState == "connected") {
+ r();
+ }
+ pc.oniceconnectionstatechange = () => {
+ if (pc.iceConnectionState == "connected") {
+ r();
+ }
+ }
+ });
+ }
+
+ offerer.onicecandidate = e => answerer.addIceCandidate(e.candidate);
+ answerer.onicecandidate = e => offerer.addIceCandidate(e.candidate);
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+ await iceConnected(offerer);
+ await iceConnected(answerer);
+ offerer.close();
+ answerer.close();
+ };
+ test();
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/single_peerconnection.html b/browser/base/content/test/webrtc/single_peerconnection.html
new file mode 100644
index 0000000000..4b4432c51b
--- /dev/null
+++ b/browser/base/content/test/webrtc/single_peerconnection.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="Page that opens a single peerconnection"></div>
+<script>
+ let test = async () => {
+ let pc = new RTCPeerConnection();
+ pc.addTransceiver('audio');
+ pc.addTransceiver('video');
+ await pc.setLocalDescription();
+ await new Promise(r => {
+ pc.onicegatheringstatechange = () => {
+ if (pc.iceGatheringState == "complete") {
+ r();
+ }
+ };
+ });
+ };
+ test();
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/zoom/browser.ini b/browser/base/content/test/zoom/browser.ini
new file mode 100644
index 0000000000..720cf9b88e
--- /dev/null
+++ b/browser/base/content/test/zoom/browser.ini
@@ -0,0 +1,34 @@
+[DEFAULT]
+support-files =
+ head.js
+ ../general/moz.png
+ zoom_test.html
+
+[browser_background_link_zoom_reset.js]
+https_first_disabled = true
+[browser_background_zoom.js]
+https_first_disabled = true
+[browser_default_zoom.js]
+[browser_default_zoom_fission.js]
+[browser_default_zoom_multitab.js]
+https_first_disabled = true
+[browser_default_zoom_sitespecific.js]
+[browser_image_zoom_tabswitch.js]
+https_first_disabled = true
+skip-if = (os == "mac") #Bug 1526628
+[browser_mousewheel_zoom.js]
+https_first_disabled = true
+[browser_sitespecific_background_pref.js]
+https_first_disabled = true
+[browser_sitespecific_image_zoom.js]
+[browser_sitespecific_video_zoom.js]
+https_first_disabled = true
+support-files =
+ ../general/video.ogg
+skip-if = os == "win" && debug || (verify && debug && (os == 'linux')) # Bug 1315042
+[browser_subframe_textzoom.js]
+[browser_tabswitch_zoom_flicker.js]
+https_first_disabled = true
+skip-if = (debug && os == "linux" && bits == 64) || (!debug && os == "win") # Bug 1652383
+[browser_tooltip_zoom.js]
+[browser_zoom_commands.js]
diff --git a/browser/base/content/test/zoom/browser_background_link_zoom_reset.js b/browser/base/content/test/zoom/browser_background_link_zoom_reset.js
new file mode 100644
index 0000000000..ac224cc4a5
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_background_link_zoom_reset.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 TEST_PAGE = "/browser/browser/base/content/test/zoom/zoom_test.html";
+var gTestTab, gBgTab, gTestZoom;
+
+function testBackgroundLoad() {
+ (async function () {
+ is(
+ ZoomManager.zoom,
+ gTestZoom,
+ "opening a background tab should not change foreground zoom"
+ );
+
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gBgTab);
+
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTestTab);
+ finish();
+ })();
+}
+
+function testInitialZoom() {
+ (async function () {
+ is(ZoomManager.zoom, 1, "initial zoom level should be 1");
+ FullZoom.enlarge();
+
+ gTestZoom = ZoomManager.zoom;
+ isnot(gTestZoom, 1, "zoom level should have changed");
+
+ gBgTab = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.load(gBgTab, "http://mochi.test:8888" + TEST_PAGE);
+ })().then(testBackgroundLoad, FullZoomHelper.failAndContinue(finish));
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ (async function () {
+ gTestTab = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTestTab);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await FullZoomHelper.load(gTestTab, "http://example.org" + TEST_PAGE);
+ })().then(testInitialZoom, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_background_zoom.js b/browser/base/content/test/zoom/browser_background_zoom.js
new file mode 100644
index 0000000000..a4faf29ad9
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_background_zoom.js
@@ -0,0 +1,115 @@
+var gTestPage =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+var gTestImage =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/moz.png";
+var gTab1, gTab2, gTab3;
+var gLevel;
+
+function test() {
+ waitForExplicitFinish();
+ registerCleanupFunction(async () => {
+ await new Promise(resolve => {
+ ContentPrefService2.removeByName(FullZoom.name, Cu.createLoadContext(), {
+ handleCompletion: resolve,
+ });
+ });
+ });
+
+ (async function () {
+ gTab1 = BrowserTestUtils.addTab(gBrowser, gTestPage);
+ gTab2 = BrowserTestUtils.addTab(gBrowser);
+ gTab3 = BrowserTestUtils.addTab(gBrowser);
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.load(gTab1, gTestPage);
+ await FullZoomHelper.load(gTab2, gTestPage);
+ })().then(secondPageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function secondPageLoaded() {
+ (async function () {
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+ FullZoomHelper.zoomTest(gTab3, 1, "Initial zoom of tab 3 should be 1");
+
+ // Now have three tabs, two with the test page, one blank. Tab 1 is selected
+ // Zoom tab 1
+ FullZoom.enlarge();
+ gLevel = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1));
+
+ ok(gLevel > 1, "New zoom for tab 1 should be greater than 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2");
+ FullZoomHelper.zoomTest(gTab3, 1, "Zooming tab 1 should not affect tab 3");
+
+ await FullZoomHelper.load(gTab3, gTestPage);
+ })().then(thirdPageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function thirdPageLoaded() {
+ (async function () {
+ FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed");
+ FullZoomHelper.zoomTest(gTab2, 1, "Tab 2 should still not be affected");
+ FullZoomHelper.zoomTest(
+ gTab3,
+ gLevel,
+ "Tab 3 should have zoomed as it was loading in the background"
+ );
+
+ // Switching to tab 2 should update its zoom setting.
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed");
+ FullZoomHelper.zoomTest(gTab2, gLevel, "Tab 2 should be zoomed now");
+ FullZoomHelper.zoomTest(gTab3, gLevel, "Tab 3 should still be zoomed");
+
+ await FullZoomHelper.load(gTab1, gTestImage);
+ })().then(imageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function imageLoaded() {
+ (async function () {
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should be 1 when image was loaded in the background"
+ );
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should still be 1 when tab with image is selected"
+ );
+ })().then(imageZoomSwitch, FullZoomHelper.failAndContinue(finish));
+}
+
+function imageZoomSwitch() {
+ (async function () {
+ await FullZoomHelper.navigate(FullZoomHelper.BACK);
+ await FullZoomHelper.navigate(FullZoomHelper.FORWARD);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Tab 1 should not be zoomed when an image loads"
+ );
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Tab 1 should still not be zoomed when deselected"
+ );
+ })().then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+var finishTestStarted = false;
+function finishTest() {
+ (async function () {
+ ok(!finishTestStarted, "finishTest called more than once");
+ finishTestStarted = true;
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab3);
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_default_zoom.js b/browser/base/content/test/zoom/browser_default_zoom.js
new file mode 100644
index 0000000000..bf0533fdad
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_init_default_zoom() {
+ const TEST_PAGE_URL =
+ "data:text/html;charset=utf-8,<body>test_init_default_zoom</body>";
+
+ // Prepare the test tab
+ info("Creating tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser = gBrowser.getBrowserForTab(tab);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+
+ // 100% global zoom
+ info("Getting default zoom");
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 1, "Global zoom is init at 100%");
+ // 100% tab zoom
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser),
+ 1,
+ "Current zoom is init at 100%"
+ );
+ info("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
+
+add_task(async function test_set_default_zoom() {
+ const TEST_PAGE_URL =
+ "data:text/html;charset=utf-8,<body>test_set_default_zoom</body>";
+
+ // Prepare the test tab
+ info("Creating tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser = gBrowser.getBrowserForTab(tab);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+
+ // 120% global zoom
+ info("Changing default zoom");
+ await FullZoomHelper.changeDefaultZoom(120);
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 1.2, "Global zoom is at 120%");
+
+ // 120% tab zoom
+ await TestUtils.waitForCondition(() => {
+ info("Current zoom is: " + ZoomManager.getZoomForBrowser(tabBrowser));
+ return ZoomManager.getZoomForBrowser(tabBrowser) == 1.2;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser),
+ 1.2,
+ "Current zoom matches changed default zoom"
+ );
+ info("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+ await FullZoomHelper.changeDefaultZoom(100);
+});
+
+add_task(async function test_enlarge_reduce_reset_local_zoom() {
+ const TEST_PAGE_URL =
+ "data:text/html;charset=utf-8,<body>test_enlarge_reduce_reset_local_zoom</body>";
+
+ // Prepare the test tab
+ info("Creating tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser = gBrowser.getBrowserForTab(tab);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+
+ // 120% global zoom
+ info("Changing default zoom");
+ await FullZoomHelper.changeDefaultZoom(120);
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 1.2, "Global zoom is at 120%");
+
+ await TestUtils.waitForCondition(() => {
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+ return ZoomManager.getZoomForBrowser(tabBrowser) == 1.2;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser),
+ 1.2,
+ "Current tab zoom matches default zoom"
+ );
+
+ await FullZoom.enlarge();
+ info("Enlarged!");
+ defaultZoom = await FullZoomHelper.getGlobalValue();
+ info("Current global zoom is " + defaultZoom);
+
+ // 133% tab zoom
+ await TestUtils.waitForCondition(() => {
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+ return ZoomManager.getZoomForBrowser(tabBrowser) == 1.33;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser),
+ 1.33,
+ "Increasing zoom changes zoom of current tab."
+ );
+ defaultZoom = await FullZoomHelper.getGlobalValue();
+ // 120% global zoom
+ is(
+ defaultZoom,
+ 1.2,
+ "Increasing zoom of current tab doesn't change default zoom."
+ );
+ info("Reducing...");
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+ await FullZoom.reduce(); // 120% tab zoom
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+ await FullZoom.reduce(); // 110% tab zoom
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+ await FullZoom.reduce(); // 100% tab zoom
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+
+ await TestUtils.waitForCondition(() => {
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+ return ZoomManager.getZoomForBrowser(tabBrowser) == 1;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser),
+ 1,
+ "Decreasing zoom changes zoom of current tab."
+ );
+ defaultZoom = await FullZoomHelper.getGlobalValue();
+ // 120% global zoom
+ is(
+ defaultZoom,
+ 1.2,
+ "Decreasing zoom of current tab doesn't change default zoom."
+ );
+ info("Resetting...");
+ FullZoom.reset(); // 120% tab zoom
+ await TestUtils.waitForCondition(() => {
+ info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser));
+ return ZoomManager.getZoomForBrowser(tabBrowser) == 1.2;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser),
+ 1.2,
+ "Reseting zoom causes current tab to zoom to default zoom."
+ );
+
+ // no reset necessary, it was performed as part of the test
+ info("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
diff --git a/browser/base/content/test/zoom/browser_default_zoom_fission.js b/browser/base/content/test/zoom/browser_default_zoom_fission.js
new file mode 100644
index 0000000000..4d3d8b3896
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom_fission.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_sitespecific_iframe_global_zoom() {
+ const TEST_PAGE_URL =
+ 'data:text/html;charset=utf-8,<body>test_sitespecific_iframe_global_zoom<iframe src=""></iframe></body>';
+ const TEST_IFRAME_URL = "https://example.com/";
+
+ // Prepare the test tab
+ console.log("Creating tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser = gBrowser.getBrowserForTab(tab);
+ console.log("Loading tab");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+
+ // 67% global zoom
+ console.log("Changing default zoom");
+ await FullZoomHelper.changeDefaultZoom(67);
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 0.67, "Global zoom is at 67%");
+ await TestUtils.waitForCondition(() => {
+ console.log("Current zoom is: ", ZoomManager.getZoomForBrowser(tabBrowser));
+ return ZoomManager.getZoomForBrowser(tabBrowser) == 0.67;
+ });
+
+ let zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser).toFixed(2);
+ is(zoomLevel, "0.67", "tab zoom has been set to 67%");
+
+ let frameLoadedPromise = BrowserTestUtils.browserLoaded(
+ tabBrowser,
+ true,
+ TEST_IFRAME_URL
+ );
+ console.log("Spawinging iframe");
+ SpecialPowers.spawn(tabBrowser, [TEST_IFRAME_URL], url => {
+ content.document.querySelector("iframe").src = url;
+ });
+ console.log("Awaiting frame promise");
+ let loadedURL = await frameLoadedPromise;
+ is(loadedURL, TEST_IFRAME_URL, "got the load event for the iframe");
+
+ let frameZoom = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser.browsingContext.children[0],
+ [],
+ async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.docShell.browsingContext.fullZoom.toFixed(2) == 0.67;
+ });
+ return content.docShell.browsingContext.fullZoom.toFixed(2);
+ }
+ );
+
+ is(frameZoom, zoomLevel, "global zoom is reflected in iframe");
+ console.log("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
+
+add_task(async function test_sitespecific_global_zoom_enlarge() {
+ const TEST_PAGE_URL =
+ 'data:text/html;charset=utf-8,<body>test_sitespecific_global_zoom_enlarge<iframe src=""></iframe></body>';
+ const TEST_IFRAME_URL = "https://example.org/";
+
+ // Prepare the test tab
+ console.log("Adding tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser = gBrowser.getBrowserForTab(tab);
+ console.log("Awaiting tab load");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+
+ // 67% global zoom persists from previous test
+
+ let frameLoadedPromise = BrowserTestUtils.browserLoaded(
+ tabBrowser,
+ true,
+ TEST_IFRAME_URL
+ );
+ console.log("Spawning iframe");
+ SpecialPowers.spawn(tabBrowser, [TEST_IFRAME_URL], url => {
+ content.document.querySelector("iframe").src = url;
+ });
+ console.log("Awaiting iframe load");
+ let loadedURL = await frameLoadedPromise;
+ is(loadedURL, TEST_IFRAME_URL, "got the load event for the iframe");
+ console.log("Enlarging tab");
+ await FullZoom.enlarge();
+ // 80% local zoom
+ await TestUtils.waitForCondition(() => {
+ console.log("Current zoom is: ", ZoomManager.getZoomForBrowser(tabBrowser));
+ return ZoomManager.getZoomForBrowser(tabBrowser) == 0.8;
+ });
+
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser).toFixed(2),
+ "0.80",
+ "Local zoom is increased"
+ );
+
+ let frameZoom = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser.browsingContext.children[0],
+ [],
+ async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.docShell.browsingContext.fullZoom.toFixed(2) == 0.8;
+ });
+ return content.docShell.browsingContext.fullZoom.toFixed(2);
+ }
+ );
+
+ is(frameZoom, "0.80", "(without fission) iframe zoom matches page zoom");
+ console.log("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
diff --git a/browser/base/content/test/zoom/browser_default_zoom_multitab.js b/browser/base/content/test/zoom/browser_default_zoom_multitab.js
new file mode 100644
index 0000000000..d204020889
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom_multitab.js
@@ -0,0 +1,190 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_multidomain_global_zoom() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const TEST_PAGE_URL_1 = "http://example.com/";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const TEST_PAGE_URL_2 = "http://example.org/";
+
+ // Prepare the test tabs
+ console.log("Creating tab 1");
+ let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_1);
+ let tabBrowser1 = gBrowser.getBrowserForTab(tab1);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+
+ console.log("Creating tab 2");
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_2);
+ let tabBrowser2 = gBrowser.getBrowserForTab(tab2);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+
+ // 67% global zoom
+ console.log("Changing default zoom");
+ await FullZoomHelper.changeDefaultZoom(67);
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 0.67, "Global zoom is set to 67%");
+
+ // 67% local zoom tab 1
+ console.log("Selecting tab 1");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Currnet zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser1)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser1),
+ 0.67,
+ "Setting default zoom causes tab 1 (background) to zoom to default zoom."
+ );
+
+ // 67% local zoom tab 2
+ console.log("Selecting tab 2");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser2)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.67;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser2),
+ 0.67,
+ "Setting default zoom causes tab 2 (foreground) to zoom to default zoom."
+ );
+ console.log("Enlarging tab");
+ await FullZoom.enlarge();
+ // 80% local zoom tab 2
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser2)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.8;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser2),
+ 0.8,
+ "Enlarging local zoom of tab 2."
+ );
+
+ // 67% local zoom tab 1
+ console.log("Selecting tab 1");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser1)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser1),
+ 0.67,
+ "Tab 1 is unchanged by tab 2's enlarge call."
+ );
+ console.log("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+ console.log("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
+
+add_task(async function test_site_specific_global_zoom() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const TEST_PAGE_URL_1 = "http://example.net/";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const TEST_PAGE_URL_2 = "http://example.net/";
+
+ // Prepare the test tabs
+ console.log("Adding tab 1");
+ let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_1);
+ console.log("Getting tab 1 browser");
+ let tabBrowser1 = gBrowser.getBrowserForTab(tab1);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+
+ console.log("Adding tab 2");
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_2);
+ console.log("Getting tab 2 browser");
+ let tabBrowser2 = gBrowser.getBrowserForTab(tab2);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+
+ console.log("checking global zoom");
+ // 67% global zoom persists from previous test
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 0.67, "Default zoom is 67%");
+
+ // 67% local zoom tab 1
+ console.log("Selecting tab 1");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ console.log("Awaiting condition");
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 1 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser1)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser1),
+ 0.67,
+ "Setting default zoom causes tab 1 (background) to zoom to default zoom."
+ );
+
+ // 67% local zoom tab 2
+ console.log("Selecting tab 2");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ console.log("Awaiting condition");
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 2 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser2)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.67;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser2),
+ 0.67,
+ "Setting default zoom causes tab 2 (foreground) to zoom to default zoom."
+ );
+
+ // 80% site specific zoom
+ console.log("Selecting tab 1");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ console.log("Enlarging");
+ await FullZoom.enlarge();
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 1 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser1)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.8;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser1),
+ 0.8,
+ "Changed local zoom in tab one."
+ );
+ console.log("Selecting tab 2");
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 2 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser2)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.8;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser2),
+ 0.8,
+ "Second tab respects site specific zoom."
+ );
+ console.log("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+ console.log("Removing tab");
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
diff --git a/browser/base/content/test/zoom/browser_default_zoom_multitab_002.js b/browser/base/content/test/zoom/browser_default_zoom_multitab_002.js
new file mode 100644
index 0000000000..3e85bffa51
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom_multitab_002.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_site_specific_global_zoom() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const TEST_PAGE_URL_1 = "http://example.net";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const TEST_PAGE_URL_2 = "http://example.net";
+
+ // Prepare the test tabs
+ console.log("Adding tab 1");
+ let tab1 = BrowserTestUtils.addTab(gBrowser);
+ let tabBrowser1 = gBrowser.getBrowserForTab(tab1);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await FullZoomHelper.load(tab1, TEST_PAGE_URL_1);
+
+ console.log("Adding tab 2");
+ let tab2 = BrowserTestUtils.addTab(gBrowser);
+ let tabBrowser2 = gBrowser.getBrowserForTab(tab2);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ await FullZoomHelper.load(tab2, TEST_PAGE_URL_2);
+
+ // 67% global zoom
+ await FullZoomHelper.changeDefaultZoom(67);
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 0.67, "Global zoom is set to 67%");
+
+ // 67% local zoom tab 1
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 1 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser1)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser1),
+ 0.67,
+ "Setting default zoom causes tab 1 (background) to zoom to default zoom."
+ );
+
+ // 67% local zoom tab 2
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 2 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser2)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.67;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser2),
+ 0.67,
+ "Setting default zoom causes tab 2 (foreground) to zoom to default zoom."
+ );
+
+ // 80% site specific zoom
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ console.log("Enlarging");
+ await FullZoom.enlarge();
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 1 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser1)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.8;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser1),
+ 0.8,
+ "Changed local zoom in tab one."
+ );
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ await TestUtils.waitForCondition(() => {
+ console.log(
+ "Current tab 2 zoom is: ",
+ ZoomManager.getZoomForBrowser(tabBrowser2)
+ );
+ return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.8;
+ });
+ is(
+ ZoomManager.getZoomForBrowser(tabBrowser2),
+ 0.8,
+ "Second tab respects site specific zoom."
+ );
+
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
diff --git a/browser/base/content/test/zoom/browser_default_zoom_sitespecific.js b/browser/base/content/test/zoom/browser_default_zoom_sitespecific.js
new file mode 100644
index 0000000000..923f3b687a
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom_sitespecific.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_disabled_ss_multi() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.zoom.siteSpecific", false]],
+ });
+ const TEST_PAGE_URL = "https://example.org/";
+
+ // Prepare the test tabs
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser2 = gBrowser.getBrowserForTab(tab2);
+ let isLoaded = BrowserTestUtils.browserLoaded(
+ tabBrowser2,
+ false,
+ TEST_PAGE_URL
+ );
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ await isLoaded;
+
+ let zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser2);
+ is(zoomLevel, 1, "tab 2 zoom has been set to 100%");
+
+ let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser1 = gBrowser.getBrowserForTab(tab1);
+ isLoaded = BrowserTestUtils.browserLoaded(tabBrowser1, false, TEST_PAGE_URL);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await isLoaded;
+
+ zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1);
+ is(zoomLevel, 1, "tab 1 zoom has been set to 100%");
+
+ // 67% global zoom
+ await FullZoomHelper.changeDefaultZoom(67);
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 0.67, "Global zoom is at 67%");
+
+ await TestUtils.waitForCondition(
+ () => ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67
+ );
+ zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1);
+ is(zoomLevel, 0.67, "tab 1 zoom has been set to 67%");
+
+ await FullZoom.enlarge();
+
+ zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1);
+ is(zoomLevel, 0.8, "tab 1 zoom has been set to 80%");
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser2);
+ is(zoomLevel, 1, "tab 2 zoom remains 100%");
+
+ let tab3 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser3 = gBrowser.getBrowserForTab(tab3);
+ isLoaded = BrowserTestUtils.browserLoaded(tabBrowser3, false, TEST_PAGE_URL);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab3);
+ await isLoaded;
+
+ zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser3);
+ is(zoomLevel, 0.67, "tab 3 zoom has been set to 67%");
+
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
+
+add_task(async function test_disabled_ss_custom() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.zoom.siteSpecific", false]],
+ });
+ const TEST_PAGE_URL = "https://example.org/";
+
+ // 150% global zoom
+ await FullZoomHelper.changeDefaultZoom(150);
+ let defaultZoom = await FullZoomHelper.getGlobalValue();
+ is(defaultZoom, 1.5, "Global zoom is at 150%");
+
+ // Prepare test tab
+ let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL);
+ let tabBrowser1 = gBrowser.getBrowserForTab(tab1);
+ let isLoaded = BrowserTestUtils.browserLoaded(
+ tabBrowser1,
+ false,
+ TEST_PAGE_URL
+ );
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await isLoaded;
+
+ await TestUtils.waitForCondition(
+ () => ZoomManager.getZoomForBrowser(tabBrowser1) == 1.5
+ );
+ let zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1);
+ is(zoomLevel, 1.5, "tab 1 zoom has been set to 150%");
+
+ await FullZoom.enlarge();
+
+ zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1);
+ is(zoomLevel, 1.7, "tab 1 zoom has been set to 170%");
+
+ await BrowserTestUtils.reloadTab(tab1);
+
+ zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1);
+ is(zoomLevel, 1.7, "tab 1 zoom remains 170%");
+
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+});
diff --git a/browser/base/content/test/zoom/browser_image_zoom_tabswitch.js b/browser/base/content/test/zoom/browser_image_zoom_tabswitch.js
new file mode 100644
index 0000000000..83ffab26e8
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_image_zoom_tabswitch.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+function test() {
+ let tab1, tab2;
+ const TEST_IMAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/moz.png";
+
+ waitForExplicitFinish();
+
+ (async function () {
+ tab1 = BrowserTestUtils.addTab(gBrowser);
+ tab2 = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await FullZoomHelper.load(tab1, TEST_IMAGE);
+
+ is(ZoomManager.zoom, 1, "initial zoom level for first should be 1");
+
+ FullZoom.enlarge();
+ let zoom = ZoomManager.zoom;
+ isnot(zoom, 1, "zoom level should have changed");
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ is(ZoomManager.zoom, 1, "initial zoom level for second tab should be 1");
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ is(
+ ZoomManager.zoom,
+ zoom,
+ "zoom level for first tab should not have changed"
+ );
+
+ await FullZoomHelper.removeTabAndWaitForLocationChange(tab1);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(tab2);
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_mousewheel_zoom.js b/browser/base/content/test/zoom/browser_mousewheel_zoom.js
new file mode 100644
index 0000000000..a814f1dba6
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_mousewheel_zoom.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 TEST_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+
+var gTab1, gTab2, gLevel1;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Scroll on Ctrl + mousewheel
+ SpecialPowers.pushPrefEnv({ set: [["mousewheel.with_control.action", 3]] });
+
+ (async function () {
+ gTab1 = BrowserTestUtils.addTab(gBrowser);
+ gTab2 = BrowserTestUtils.addTab(gBrowser);
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.load(gTab1, TEST_PAGE);
+ await FullZoomHelper.load(gTab2, TEST_PAGE);
+ })().then(zoomTab1, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab1() {
+ (async function () {
+ is(gBrowser.selectedTab, gTab1, "Tab 1 is selected");
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+
+ let browser1 = gBrowser.getBrowserForTab(gTab1);
+ await BrowserTestUtils.synthesizeMouse(
+ null,
+ 10,
+ 10,
+ {
+ wheel: true,
+ ctrlKey: true,
+ deltaY: -1,
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ },
+ browser1
+ );
+
+ info("Waiting for tab 1 to be zoomed");
+ await TestUtils.waitForCondition(() => {
+ gLevel1 = ZoomManager.getZoomForBrowser(browser1);
+ return gLevel1 > 1;
+ });
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(
+ gTab2,
+ gLevel1,
+ "Tab 2 should have zoomed along with tab 1"
+ );
+ })().then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+function finishTest() {
+ (async function () {
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_sitespecific_background_pref.js b/browser/base/content/test/zoom/browser_sitespecific_background_pref.js
new file mode 100644
index 0000000000..5756c4d8d3
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_sitespecific_background_pref.js
@@ -0,0 +1,35 @@
+function test() {
+ waitForExplicitFinish();
+
+ (async function () {
+ let testPage =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+ let tab1 = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ await FullZoomHelper.load(tab1, testPage);
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.load(tab2, testPage);
+
+ await FullZoom.enlarge();
+ let tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser);
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ let tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser);
+ is(tab2Zoom, tab1Zoom, "Zoom should affect background tabs");
+
+ Services.prefs.setBoolPref("browser.zoom.updateBackgroundTabs", false);
+ await FullZoom.reset();
+ gBrowser.selectedTab = tab1;
+ tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser);
+ tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser);
+ isnot(tab1Zoom, tab2Zoom, "Zoom should not affect background tabs");
+
+ if (Services.prefs.prefHasUserValue("browser.zoom.updateBackgroundTabs")) {
+ Services.prefs.clearUserPref("browser.zoom.updateBackgroundTabs");
+ }
+ await FullZoomHelper.removeTabAndWaitForLocationChange(tab1);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(tab2);
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_sitespecific_image_zoom.js b/browser/base/content/test/zoom/browser_sitespecific_image_zoom.js
new file mode 100644
index 0000000000..fae454fd04
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_sitespecific_image_zoom.js
@@ -0,0 +1,52 @@
+var tabElm, zoomLevel;
+function start_test_prefNotSet() {
+ (async function () {
+ is(ZoomManager.zoom, 1, "initial zoom level should be 1");
+ FullZoom.enlarge();
+
+ // capture the zoom level to test later
+ zoomLevel = ZoomManager.zoom;
+ isnot(zoomLevel, 1, "zoom level should have changed");
+
+ await FullZoomHelper.load(
+ gBrowser.selectedTab,
+ "http://mochi.test:8888/browser/browser/base/content/test/general/moz.png"
+ );
+ })().then(continue_test_prefNotSet, FullZoomHelper.failAndContinue(finish));
+}
+
+function continue_test_prefNotSet() {
+ (async function () {
+ is(ZoomManager.zoom, 1, "zoom level pref should not apply to an image");
+ await FullZoom.reset();
+
+ await FullZoomHelper.load(
+ gBrowser.selectedTab,
+ "http://mochi.test:8888/browser/browser/base/content/test/zoom/zoom_test.html"
+ );
+ })().then(end_test_prefNotSet, FullZoomHelper.failAndContinue(finish));
+}
+
+function end_test_prefNotSet() {
+ (async function () {
+ is(ZoomManager.zoom, zoomLevel, "the zoom level should have persisted");
+
+ // Reset the zoom so that other tests have a fresh zoom level
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange();
+ finish();
+ })();
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ (async function () {
+ tabElm = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tabElm);
+ await FullZoomHelper.load(
+ tabElm,
+ "http://mochi.test:8888/browser/browser/base/content/test/zoom/zoom_test.html"
+ );
+ })().then(start_test_prefNotSet, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_sitespecific_video_zoom.js b/browser/base/content/test/zoom/browser_sitespecific_video_zoom.js
new file mode 100644
index 0000000000..ba61a2f5a5
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_sitespecific_video_zoom.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/. */
+"use strict";
+
+const TEST_PAGE =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+const TEST_VIDEO =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/general/video.ogg";
+
+var gTab1, gTab2, gLevel1;
+
+function test() {
+ waitForExplicitFinish();
+
+ (async function () {
+ gTab1 = BrowserTestUtils.addTab(gBrowser);
+ gTab2 = BrowserTestUtils.addTab(gBrowser);
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.load(gTab1, TEST_PAGE);
+ await FullZoomHelper.load(gTab2, TEST_VIDEO);
+ })().then(zoomTab1, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab1() {
+ (async function () {
+ is(gBrowser.selectedTab, gTab1, "Tab 1 is selected");
+
+ // Reset zoom level if we run this test > 1 time in same browser session.
+ var level1 = ZoomManager.getZoomForBrowser(
+ gBrowser.getBrowserForTab(gTab1)
+ );
+ if (level1 > 1) {
+ FullZoom.reduce();
+ }
+
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+
+ FullZoom.enlarge();
+ gLevel1 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1));
+
+ ok(gLevel1 > 1, "New zoom for tab 1 should be greater than 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2");
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(
+ gTab2,
+ 1,
+ "Tab 2 is still unzoomed after it is selected"
+ );
+ FullZoomHelper.zoomTest(gTab1, gLevel1, "Tab 1 is still zoomed");
+ })().then(zoomTab2, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab2() {
+ (async function () {
+ is(gBrowser.selectedTab, gTab2, "Tab 2 is selected");
+
+ FullZoom.reduce();
+ let level2 = ZoomManager.getZoomForBrowser(
+ gBrowser.getBrowserForTab(gTab2)
+ );
+
+ ok(level2 < 1, "New zoom for tab 2 should be less than 1");
+ FullZoomHelper.zoomTest(
+ gTab1,
+ gLevel1,
+ "Zooming tab 2 should not affect tab 1"
+ );
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ gLevel1,
+ "Tab 1 should have the same zoom after it's selected"
+ );
+ })().then(testNavigation, FullZoomHelper.failAndContinue(finish));
+}
+
+function testNavigation() {
+ (async function () {
+ await FullZoomHelper.load(gTab1, TEST_VIDEO);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should be 1 when a video was loaded"
+ );
+ await waitForNextTurn(); // trying to fix orange bug 806046
+ await FullZoomHelper.navigate(FullZoomHelper.BACK);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ gLevel1,
+ "Zoom should be restored when a page is loaded"
+ );
+ await waitForNextTurn(); // trying to fix orange bug 806046
+ await FullZoomHelper.navigate(FullZoomHelper.FORWARD);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should be 1 again when navigating back to a video"
+ );
+ })().then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+function waitForNextTurn() {
+ return new Promise(resolve => {
+ setTimeout(() => resolve(), 0);
+ });
+}
+
+var finishTestStarted = false;
+function finishTest() {
+ (async function () {
+ ok(!finishTestStarted, "finishTest called more than once");
+ finishTestStarted = true;
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_subframe_textzoom.js b/browser/base/content/test/zoom/browser_subframe_textzoom.js
new file mode 100644
index 0000000000..e5d40cf585
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_subframe_textzoom.js
@@ -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/. */
+
+/*
+ * Test the fix for bug 441778 to ensure site-specific page zoom doesn't get
+ * modified by sub-document loads of content from a different domain.
+ */
+
+function test() {
+ waitForExplicitFinish();
+
+ const TEST_PAGE_URL = 'data:text/html,<body><iframe src=""></iframe></body>';
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const TEST_IFRAME_URL = "http://test2.example.org/";
+
+ (async function () {
+ // Prepare the test tab
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+
+ let testBrowser = tab.linkedBrowser;
+
+ await FullZoomHelper.load(tab, TEST_PAGE_URL);
+
+ // Change the zoom level and then save it so we can compare it to the level
+ // after loading the sub-document.
+ FullZoom.enlarge();
+ var zoomLevel = ZoomManager.zoom;
+
+ // Start the sub-document load.
+ await new Promise(resolve => {
+ executeSoon(function () {
+ BrowserTestUtils.browserLoaded(testBrowser, true).then(url => {
+ is(url, TEST_IFRAME_URL, "got the load event for the iframe");
+ is(
+ ZoomManager.zoom,
+ zoomLevel,
+ "zoom is retained after sub-document load"
+ );
+
+ FullZoomHelper.removeTabAndWaitForLocationChange().then(() =>
+ resolve()
+ );
+ });
+ SpecialPowers.spawn(testBrowser, [TEST_IFRAME_URL], url => {
+ content.document.querySelector("iframe").src = url;
+ });
+ });
+ });
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_tabswitch_zoom_flicker.js b/browser/base/content/test/zoom/browser_tabswitch_zoom_flicker.js
new file mode 100644
index 0000000000..df1c3816ae
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_tabswitch_zoom_flicker.js
@@ -0,0 +1,45 @@
+var tab;
+
+function test() {
+ // ----------
+ // Test setup
+
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("browser.zoom.updateBackgroundTabs", true);
+ Services.prefs.setBoolPref("browser.zoom.siteSpecific", true);
+
+ let uri =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+
+ (async function () {
+ tab = BrowserTestUtils.addTab(gBrowser);
+ await FullZoomHelper.load(tab, uri);
+
+ // -------------------------------------------------------------------
+ // Test - Trigger a tab switch that should update the zoom level
+ await FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+ ok(true, "applyPrefToSetting was called");
+ })().then(endTest, FullZoomHelper.failAndContinue(endTest));
+}
+
+// -------------
+// Test clean-up
+function endTest() {
+ (async function () {
+ await FullZoomHelper.removeTabAndWaitForLocationChange(tab);
+
+ tab = null;
+
+ if (Services.prefs.prefHasUserValue("browser.zoom.updateBackgroundTabs")) {
+ Services.prefs.clearUserPref("browser.zoom.updateBackgroundTabs");
+ }
+
+ if (Services.prefs.prefHasUserValue("browser.zoom.siteSpecific")) {
+ Services.prefs.clearUserPref("browser.zoom.siteSpecific");
+ }
+
+ finish();
+ })();
+}
diff --git a/browser/base/content/test/zoom/browser_tooltip_zoom.js b/browser/base/content/test/zoom/browser_tooltip_zoom.js
new file mode 100644
index 0000000000..f7627b1749
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_tooltip_zoom.js
@@ -0,0 +1,41 @@
+add_task(async function test_zoom_tooltip() {
+ const TEST_PAGE_URL = 'data:text/html,<html title="tooltiptext">';
+ await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async function (browser) {
+ FullZoom.setZoom(2.0, browser);
+
+ const tooltip = document.getElementById("remoteBrowserTooltip");
+ const popupShown = new Promise(resolve => {
+ tooltip.addEventListener("popupshown", resolve, { once: true });
+ });
+
+ // Fire a mousemove to trigger the tooltip.
+ // Margin from the anchor and stuff depends on the platform, but these
+ // should be big enough so that all platforms pass, but not big enough so
+ // that it'd pass even when messing up the coordinates would.
+ const DISTANCE = 300;
+ const EPSILON = 25;
+
+ EventUtils.synthesizeMouse(browser, DISTANCE, DISTANCE, {
+ type: "mousemove",
+ });
+
+ await popupShown;
+ ok(
+ true,
+ `popup should be shown (coords: ${tooltip.screenX}, ${tooltip.screenY})`
+ );
+
+ isfuzzy(
+ tooltip.screenX,
+ browser.screenX + DISTANCE,
+ EPSILON,
+ "Should be at the right x position, more or less"
+ );
+ isfuzzy(
+ tooltip.screenY,
+ browser.screenY + DISTANCE,
+ EPSILON,
+ "Should be at the right y position, more or less"
+ );
+ });
+});
diff --git a/browser/base/content/test/zoom/browser_zoom_commands.js b/browser/base/content/test/zoom/browser_zoom_commands.js
new file mode 100644
index 0000000000..88b6f42059
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_zoom_commands.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL =
+ "data:text/html;charset=utf-8,<body>test_zoom_levels</body>";
+
+/**
+ * Waits for the zoom commands in the window to have the expected enabled
+ * state.
+ *
+ * @param {Object} expectedState
+ * An object where each key represents one of the zoom commands,
+ * and the value is a boolean that is true if the command should
+ * be enabled, and false if it should be disabled.
+ *
+ * The keys are "enlarge", "reduce" and "reset" for readability,
+ * and internally this function maps those keys to the appropriate
+ * commands.
+ * @returns Promise
+ * @resolves undefined
+ */
+async function waitForCommandEnabledState(expectedState) {
+ const COMMAND_MAP = {
+ enlarge: "cmd_fullZoomEnlarge",
+ reduce: "cmd_fullZoomReduce",
+ reset: "cmd_fullZoomReset",
+ };
+
+ await TestUtils.waitForCondition(() => {
+ for (let commandKey in expectedState) {
+ let commandID = COMMAND_MAP[commandKey];
+ let command = document.getElementById(commandID);
+ let expectedEnabled = expectedState[commandKey];
+
+ if (command.hasAttribute("disabled") == expectedEnabled) {
+ return false;
+ }
+ }
+ Assert.ok("Commands finally reached the expected state.");
+ return true;
+ }, "Waiting for commands to reach the right state.");
+}
+
+/**
+ * Tests that the "Zoom Text Only" command is in the right checked
+ * state.
+ *
+ * @param {boolean} isChecked
+ * True if the command should have its "checked" attribute set to
+ * "true". Otherwise, ensures that the attribute is set to "false".
+ */
+function assertTextZoomCommandCheckedState(isChecked) {
+ let command = document.getElementById("cmd_fullZoomToggle");
+ Assert.equal(
+ command.getAttribute("checked"),
+ "" + isChecked,
+ "Text zoom command has expected checked attribute"
+ );
+}
+
+/**
+ * Tests that zoom commands are properly updated when changing
+ * zoom levels and/or preferences on an individual browser.
+ */
+add_task(async function test_update_browser_zoom() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async browser => {
+ let currentZoom = await FullZoomHelper.getGlobalValue();
+ Assert.equal(
+ currentZoom,
+ 1,
+ "We expect to start at the default zoom level."
+ );
+
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: true,
+ reset: false,
+ });
+ assertTextZoomCommandCheckedState(false);
+
+ // We'll run two variations of this test - one with text zoom enabled,
+ // and the other without.
+ for (let textZoom of [true, false]) {
+ info(`Running variation with textZoom set to ${textZoom}`);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.zoom.full", !textZoom]],
+ });
+
+ // 120% global zoom
+ info("Changing default zoom by a single level");
+ ZoomManager.zoom = 1.2;
+
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: true,
+ reset: true,
+ });
+ await assertTextZoomCommandCheckedState(textZoom);
+
+ // Now max out the zoom level.
+ ZoomManager.zoom = ZoomManager.MAX;
+
+ await waitForCommandEnabledState({
+ enlarge: false,
+ reduce: true,
+ reset: true,
+ });
+ await assertTextZoomCommandCheckedState(textZoom);
+
+ // Now min out the zoom level.
+ ZoomManager.zoom = ZoomManager.MIN;
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: false,
+ reset: true,
+ });
+ await assertTextZoomCommandCheckedState(textZoom);
+
+ // Now reset back to the default zoom level
+ ZoomManager.zoom = 1;
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: true,
+ reset: false,
+ });
+ await assertTextZoomCommandCheckedState(textZoom);
+ }
+ });
+});
+
+/**
+ * Tests that zoom commands are properly updated when changing
+ * zoom levels when the default zoom is not at 1.0.
+ */
+add_task(async function test_update_browser_zoom() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async browser => {
+ let currentZoom = await FullZoomHelper.getGlobalValue();
+ Assert.equal(
+ currentZoom,
+ 1,
+ "We expect to start at the default zoom level."
+ );
+
+ // Now change the default zoom to 200%, which is what we'll switch
+ // back to when choosing to reset the zoom level.
+ //
+ // It's a bit maddening that changeDefaultZoom takes values in integer
+ // units from 30 to 500, whereas ZoomManager.zoom takes things in float
+ // units from 0.3 to 5.0, but c'est la vie for now.
+ await FullZoomHelper.changeDefaultZoom(200);
+ registerCleanupFunction(async () => {
+ await FullZoomHelper.changeDefaultZoom(100);
+ });
+
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: true,
+ reset: false,
+ });
+
+ // 120% global zoom
+ info("Changing default zoom by a single level");
+ ZoomManager.zoom = 2.2;
+
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: true,
+ reset: true,
+ });
+ await assertTextZoomCommandCheckedState(false);
+
+ // Now max out the zoom level.
+ ZoomManager.zoom = ZoomManager.MAX;
+
+ await waitForCommandEnabledState({
+ enlarge: false,
+ reduce: true,
+ reset: true,
+ });
+ await assertTextZoomCommandCheckedState(false);
+
+ // Now min out the zoom level.
+ ZoomManager.zoom = ZoomManager.MIN;
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: false,
+ reset: true,
+ });
+ await assertTextZoomCommandCheckedState(false);
+
+ // Now reset back to the default zoom level
+ ZoomManager.zoom = 2;
+ await waitForCommandEnabledState({
+ enlarge: true,
+ reduce: true,
+ reset: false,
+ });
+ await assertTextZoomCommandCheckedState(false);
+ });
+});
diff --git a/browser/base/content/test/zoom/head.js b/browser/base/content/test/zoom/head.js
new file mode 100644
index 0000000000..d5f09aa5e2
--- /dev/null
+++ b/browser/base/content/test/zoom/head.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gContentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+);
+
+let gLoadContext = Cu.createLoadContext();
+
+registerCleanupFunction(async function () {
+ await new Promise(resolve => {
+ gContentPrefs.removeByName(window.FullZoom.name, gLoadContext, {
+ handleResult() {},
+ handleCompletion() {
+ resolve();
+ },
+ });
+ });
+});
+
+var FullZoomHelper = {
+ async changeDefaultZoom(newZoom) {
+ let nonPrivateLoadContext = Cu.createLoadContext();
+ /* Because our setGlobal function takes in a browsing context, and
+ * because we want to keep this property consistent across both private
+ * and non-private contexts, we crate a non-private context and use that
+ * to set the property, regardless of our actual context.
+ */
+
+ let parsedZoomValue = parseFloat((parseInt(newZoom) / 100).toFixed(2));
+ await new Promise(resolve => {
+ gContentPrefs.setGlobal(
+ FullZoom.name,
+ parsedZoomValue,
+ nonPrivateLoadContext,
+ {
+ handleCompletion(reason) {
+ resolve();
+ },
+ }
+ );
+ });
+ // The zoom level is used to update the commands associated with
+ // increasing, decreasing or resetting the Zoom levels. There are
+ // a series of async things we need to wait for (writing the content
+ // pref to the database, and then reading that content pref back out
+ // again and reacting to it), so waiting for the zoom level to reach
+ // the expected level is actually simplest to make sure we're okay to
+ // proceed.
+ await TestUtils.waitForCondition(() => {
+ return ZoomManager.zoom == parsedZoomValue;
+ });
+ },
+
+ async getGlobalValue() {
+ return new Promise(resolve => {
+ let cachedVal = parseFloat(
+ gContentPrefs.getCachedGlobal(FullZoom.name, gLoadContext)
+ );
+ if (cachedVal) {
+ // We've got cached information, though it may be we've cached
+ // an undefined value, or the cached info is invalid. To ensure
+ // a valid return, we opt to return the default 1.0 in the
+ // undefined and invalid cases.
+ resolve(parseFloat(cachedVal.value) || 1.0);
+ return;
+ }
+ let value = 1.0;
+ gContentPrefs.getGlobal(FullZoom.name, gLoadContext, {
+ handleResult(pref) {
+ if (pref.value) {
+ value = parseFloat(pref.value);
+ }
+ },
+ handleCompletion(reason) {
+ resolve(value);
+ },
+ handleError(error) {
+ console.error(error);
+ },
+ });
+ });
+ },
+
+ waitForLocationChange: function waitForLocationChange() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(subj, topic, data) {
+ Services.obs.removeObserver(obs, topic);
+ resolve();
+ }, "browser-fullZoom:location-change");
+ });
+ },
+
+ selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(
+ tab
+ ) {
+ if (!tab) {
+ throw new Error("tab must be given.");
+ }
+ if (gBrowser.selectedTab == tab) {
+ return Promise.resolve();
+ }
+
+ return Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ this.waitForLocationChange(),
+ ]);
+ },
+
+ removeTabAndWaitForLocationChange: function removeTabAndWaitForLocationChange(
+ tab
+ ) {
+ tab = tab || gBrowser.selectedTab;
+ let selected = gBrowser.selectedTab == tab;
+ gBrowser.removeTab(tab);
+ if (selected) {
+ return this.waitForLocationChange();
+ }
+ return Promise.resolve();
+ },
+
+ load: function load(tab, url) {
+ return new Promise(resolve => {
+ let didLoad = false;
+ let didZoom = false;
+
+ promiseTabLoadEvent(tab, url).then(event => {
+ didLoad = true;
+ if (didZoom) {
+ resolve();
+ }
+ }, true);
+
+ this.waitForLocationChange().then(function () {
+ didZoom = true;
+ if (didLoad) {
+ resolve();
+ }
+ });
+ });
+ },
+
+ zoomTest: function zoomTest(tab, val, msg) {
+ is(ZoomManager.getZoomForBrowser(tab.linkedBrowser), val, msg);
+ },
+
+ BACK: 0,
+ FORWARD: 1,
+ navigate: function navigate(direction) {
+ return new Promise(resolve => {
+ let didPs = false;
+ let didZoom = false;
+
+ BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "pageshow",
+ true
+ ).then(() => {
+ didPs = true;
+ if (didZoom) {
+ resolve();
+ }
+ });
+
+ if (direction == this.BACK) {
+ gBrowser.goBack();
+ } else if (direction == this.FORWARD) {
+ gBrowser.goForward();
+ }
+
+ this.waitForLocationChange().then(function () {
+ didZoom = true;
+ if (didPs) {
+ resolve();
+ }
+ });
+ });
+ },
+
+ failAndContinue: function failAndContinue(func) {
+ return function (err) {
+ console.error(err);
+ ok(false, err);
+ func();
+ };
+ },
+};
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+async function promiseTabLoadEvent(tab, url) {
+ console.info("Wait tab event: load");
+ if (url) {
+ console.info("Expecting load for: ", url);
+ }
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ console.info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ console.info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url) {
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
diff --git a/browser/base/content/test/zoom/zoom_test.html b/browser/base/content/test/zoom/zoom_test.html
new file mode 100644
index 0000000000..bf80490cad
--- /dev/null
+++ b/browser/base/content/test/zoom/zoom_test.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=416661
+-->
+ <head>
+ <title>Test for zoom setting</title>
+
+ </head>
+ <body>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=416661">Bug 416661</a>
+ <p>Site specific zoom settings should not apply to image documents.</p>
+ </body>
+</html>