summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/base/content/test
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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.toml88
-rw-r--r--browser/base/content/test/about/browser_aboutCertError.js551
-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.js159
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_noSubjectAltName.js67
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_offlineSupport.js52
-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.js66
-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.js74
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_telemetry.js101
-rw-r--r--browser/base/content/test/about/browser_aboutNetError.js242
-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.js176
-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.js317
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js183
-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.js175
-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.js221
-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.toml28
-rw-r--r--browser/base/content/test/alerts/browser_notification_close.js108
-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.toml5
-rw-r--r--browser/base/content/test/backforward/browser_history_menu.js175
-rw-r--r--browser/base/content/test/caps/browser.toml8
-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.toml16
-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.js66
-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.js230
-rw-r--r--browser/base/content/test/captivePortal/head.js270
-rw-r--r--browser/base/content/test/chrome/chrome.toml5
-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.toml3
-rw-r--r--browser/base/content/test/contentTheme/browser_contentTheme_in_process_tab.js80
-rw-r--r--browser/base/content/test/contextMenu/browser.toml101
-rw-r--r--browser/base/content/test/contextMenu/browser_bug1798178.js89
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu.js2034
-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.js71
-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.js189
-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.toml150
-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.js148
-rw-r--r--browser/base/content/test/favicons/browser_favicon_credentials.js89
-rw-r--r--browser/base/content/test/favicons/browser_favicon_crossorigin.js64
-rw-r--r--browser/base/content/test/favicons/browser_favicon_empty_data.js72
-rw-r--r--browser/base/content/test/favicons/browser_favicon_load.js167
-rw-r--r--browser/base/content/test/favicons/browser_favicon_nostore.js169
-rw-r--r--browser/base/content/test/favicons/browser_favicon_referer.js65
-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.js28
-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_empty.html12
-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.toml34
-rw-r--r--browser/base/content/test/forms/browser_selectpopup.js914
-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_hr.js55
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_large.js323
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_searchfocus.js36
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_showPicker.js60
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_text_transform.js40
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_toplevel.js25
-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.mjs102
-rw-r--r--browser/base/content/test/fullscreen/browser.toml65
-rw-r--r--browser/base/content/test/fullscreen/browser_bug1557041.js47
-rw-r--r--browser/base/content/test/fullscreen/browser_bug1620341.js102
-rw-r--r--browser/base/content/test/fullscreen/browser_domFS_statuspanel.js95
-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.js290
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_warning.js280
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js136
-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.js172
-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.toml536
-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.js332
-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.js126
-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.js101
-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.js59
-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_bug477014.js36
-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.js25
-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_contentAreaClick.js329
-rw-r--r--browser/base/content/test/general/browser_ctrlTab.js464
-rw-r--r--browser/base/content/test/general/browser_datachoices_notification.js293
-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.js58
-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.js1150
-rw-r--r--browser/base/content/test/general/browser_hide_removing.js27
-rw-r--r--browser/base/content/test/general/browser_homeDrop.js111
-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.js71
-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.js218
-rw-r--r--browser/base/content/test/general/browser_newWindowDrop.js225
-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.js211
-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.js55
-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.js204
-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.js417
-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_tabkeynavigation.js223
-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.js339
-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.sjs23
-rw-r--r--browser/base/content/test/general/refresh_meta.sjs35
-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.toml3
-rw-r--r--browser/base/content/test/gesture/browser_gesture_navigation.js233
-rw-r--r--browser/base/content/test/historySwipeAnimation/browser.toml3
-rw-r--r--browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js49
-rw-r--r--browser/base/content/test/keyboard/browser.toml22
-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.js51
-rw-r--r--browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js334
-rw-r--r--browser/base/content/test/keyboard/browser_toolbarKeyNav.js643
-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.js61
-rw-r--r--browser/base/content/test/menubar/browser.toml22
-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.js23
-rw-r--r--browser/base/content/test/menubar/browser_file_share.js136
-rw-r--r--browser/base/content/test/menubar/browser_history_recently_closed_tabs.js397
-rw-r--r--browser/base/content/test/menubar/browser_search_bookmarks.js60
-rw-r--r--browser/base/content/test/menubar/browser_search_history.js56
-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.toml9
-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.toml7
-rw-r--r--browser/base/content/test/notificationbox/browser_notification_stacking.js78
-rw-r--r--browser/base/content/test/notificationbox/browser_notificationbar_telemetry.js224
-rw-r--r--browser/base/content/test/notificationbox/browser_tabnotificationbox_switch_tabs.js143
-rw-r--r--browser/base/content/test/outOfProcess/browser.toml19
-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.js84
-rw-r--r--browser/base/content/test/pageActions/browser.toml8
-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.toml15
-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.js177
-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.toml34
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js89
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js32
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_image_info.js57
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_images.js113
-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.js356
-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.mjs77
-rw-r--r--browser/base/content/test/performance/StartupContentSubframe.sys.mjs55
-rw-r--r--browser/base/content/test/performance/browser.toml115
-rw-r--r--browser/base/content/test/performance/browser_appmenu.js130
-rw-r--r--browser/base/content/test/performance/browser_hidden_browser_vsync.js56
-rw-r--r--browser/base/content/test/performance/browser_panel_vsync.js69
-rw-r--r--browser/base/content/test/performance/browser_preferences_usage.js272
-rw-r--r--browser/base/content/test/performance/browser_startup.js246
-rw-r--r--browser/base/content/test/performance/browser_startup_content.js186
-rw-r--r--browser/base/content/test/performance/browser_startup_content_mainthreadio.js465
-rw-r--r--browser/base/content/test/performance/browser_startup_content_subframe.js151
-rw-r--r--browser/base/content/test/performance/browser_startup_flicker.js72
-rw-r--r--browser/base/content/test/performance/browser_startup_hiddenwindow.js47
-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.js67
-rw-r--r--browser/base/content/test/performance/browser_windowopen.js164
-rw-r--r--browser/base/content/test/performance/file_empty.html1
-rw-r--r--browser/base/content/test/performance/head.js1001
-rw-r--r--browser/base/content/test/performance/hidpi/browser.toml8
-rw-r--r--browser/base/content/test/performance/io/browser.toml38
-rw-r--r--browser/base/content/test/performance/lowdpi/browser.toml8
-rw-r--r--browser/base/content/test/performance/moz.build17
-rw-r--r--browser/base/content/test/performance/triage.json70
-rw-r--r--browser/base/content/test/perftest.toml3
-rw-r--r--browser/base/content/test/perftest_browser_xhtml_dom.js85
-rw-r--r--browser/base/content/test/permissions/browser.toml51
-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.js278
-rw-r--r--browser/base/content/test/permissions/browser_permissions.js698
-rw-r--r--browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js45
-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.js82
-rw-r--r--browser/base/content/test/permissions/browser_reservedkey.js312
-rw-r--r--browser/base/content/test/permissions/browser_site_scoped_permissions.js124
-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.toml15
-rw-r--r--browser/base/content/test/plugins/browser_bug797677.js45
-rw-r--r--browser/base/content/test/plugins/browser_enable_DRM_prompt.js298
-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.js204
-rw-r--r--browser/base/content/test/plugins/plugin_bug797677.html5
-rw-r--r--browser/base/content/test/popupNotifications/browser.toml98
-rw-r--r--browser/base/content/test/popupNotifications/browser_displayURI.js149
-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.js378
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_4.js290
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_5.js502
-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.js402
-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.js70
-rw-r--r--browser/base/content/test/popupNotifications/head.js367
-rw-r--r--browser/base/content/test/popups/browser.toml93
-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.js568
-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/privateBrowsing/browser.toml3
-rw-r--r--browser/base/content/test/privateBrowsing/browser_private_browsing_simplified_ui.js48
-rw-r--r--browser/base/content/test/protectionsUI/benignPage.html18
-rw-r--r--browser/base/content/test/protectionsUI/browser.toml97
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI.js738
-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.js279
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cookie_banner.js489
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js533
-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.js43
-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_info_message.js90
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js104
-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.js179
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js415
-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.js403
-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.js400
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_suspicious_fingerprinters_subview.js427
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js89
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js144
-rw-r--r--browser/base/content/test/protectionsUI/canvas-fingerprinter.html22
-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/font-fingerprinter.html102
-rw-r--r--browser/base/content/test/protectionsUI/head.js247
-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.toml44
-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.sjs40
-rw-r--r--browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs40
-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.toml39
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission.js1
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js111
-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_cookiePermission_subDomains_v2.js288
-rw-r--r--browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js86
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-cookie-exceptions.js274
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-formhistory.js32
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-history.js136
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-offlineData.js249
-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_sanitize-timespans_v2.js1190
-rw-r--r--browser/base/content/test/sanitize/browser_sanitizeDialog.js837
-rw-r--r--browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js1429
-rw-r--r--browser/base/content/test/sanitize/browser_sanitizeOnShutdown_migration.js312
-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.js371
-rw-r--r--browser/base/content/test/sanitize/site_data_test.html29
-rw-r--r--browser/base/content/test/sidebar/browser.toml13
-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.js289
-rw-r--r--browser/base/content/test/siteIdentity/browser.toml194
-rw-r--r--browser/base/content/test/siteIdentity/browser_about_blank_same_document_tabswitch.js86
-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.js339
-rw-r--r--browser/base/content/test/siteIdentity/browser_check_identity_state.js720
-rw-r--r--browser/base/content/test/siteIdentity/browser_check_identity_state_pdf.js82
-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.js38
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js55
-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.js246
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js237
-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.js130
-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.js345
-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.js195
-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.js422
-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.html43
-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.html44
-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.html43
-rw-r--r--browser/base/content/test/startup/browser.toml4
-rw-r--r--browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js136
-rw-r--r--browser/base/content/test/static/browser.toml29
-rw-r--r--browser/base/content/test/static/browser_all_files_referenced.js1088
-rw-r--r--browser/base/content/test/static/browser_misused_characters_in_strings.js247
-rw-r--r--browser/base/content/test/static/browser_parsable_css.js577
-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.toml7
-rw-r--r--browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js33
-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.toml16
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendpage.js474
-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.js280
-rw-r--r--browser/base/content/test/sync/browser_sync.js935
-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.toml44
-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.js95
-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.toml50
-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.js236
-rw-r--r--browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js137
-rw-r--r--browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js100
-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.js219
-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_close_event.js164
-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.toml29
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml21
-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_aboutRestartRequired_noForkServer.toml14
-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.js242
-rw-r--r--browser/base/content/test/tabdialogs/browser.toml20
-rw-r--r--browser/base/content/test/tabdialogs/browser_multiple_dialog_navigation.js64
-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.toml345
-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.js684
-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.js63
-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.js210
-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.js493
-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.js143
-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.js203
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js87
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js90
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js160
-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.js74
-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_bookmarks_toolbar_height.js129
-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_openURI_background.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.js114
-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.js95
-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.js118
-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.js102
-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.js82
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_drag.js257
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js37
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_visibility.js53
-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_preview.js154
-rw-r--r--browser/base/content/test/tabs/browser_tab_tooltips.js149
-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/browser_window_open_modifiers.js175
-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_observe_height_changes.html21
-rw-r--r--browser/base/content/test/tabs/file_rel_opener_noopener.html12
-rw-r--r--browser/base/content/test/tabs/file_window_open.html70
-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.toml4
-rw-r--r--browser/base/content/test/touch/browser_menu_touch.js198
-rw-r--r--browser/base/content/test/utilityOverlay/browser.toml3
-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.toml49
-rw-r--r--browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js26
-rw-r--r--browser/base/content/test/webextensions/browser_extension_sideloading.js468
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background.js313
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js142
-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.js29
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_local_file.js43
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js21
-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.js64
-rw-r--r--browser/base/content/test/webextensions/browser_update_checkForUpdates.js17
-rw-r--r--browser/base/content/test/webextensions/browser_update_interactive_noprompt.js80
-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.js679
-rw-r--r--browser/base/content/test/webrtc/browser.toml159
-rw-r--r--browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js486
-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.js889
-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.js771
-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.js250
-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.js893
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js72
-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.js257
-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_macos_indicator_hiding.js145
-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.html123
-rw-r--r--browser/base/content/test/webrtc/get_user_media2.html106
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_frame.html97
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html70
-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.toml15
-rw-r--r--browser/base/content/test/webrtc/head.js1330
-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.toml54
-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
1093 files changed, 123840 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.toml b/browser/base/content/test/about/browser.toml
new file mode 100644
index 0000000000..900c6b7140
--- /dev/null
+++ b/browser/base/content/test/about/browser.toml
@@ -0,0 +1,88 @@
+[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)",
+ "win10_2009 && 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"]
+fail-if = ["a11y_checks"] # Bug 1854233 text-link may not be focusable
+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..9af82b807f
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError.js
@@ -0,0 +1,551 @@
+/* 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.startLoadingURIString(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.startLoadingURIString(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.toLowerCase(),
+ };
+ });
+ 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.toLowerCase(),
+ cdlTextContent: cdl.textContent,
+ cdlTagName: cdl.tagName.toLowerCase(),
+ };
+ });
+
+ 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.toLowerCase(),
+ "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.toLowerCase(),
+ "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..ebc4344712
--- /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.isHidden(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.isHidden(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.isHidden(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..0d7ebbe4f2
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_mitm.js
@@ -0,0 +1,159 @@
+/* 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],
+ [PREF_ENTERPRISE_ROOTS, false],
+ ],
+ });
+
+ 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..bed9517e34
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_offlineSupport.js
@@ -0,0 +1,52 @@
+/* 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");
+ Assert.equal(
+ 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..191b86be65
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_POST.js
@@ -0,0 +1,66 @@
+/* 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 engine;
+ await promiseContentSearchChange(browser, async () => {
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: "https://example.com/browser/browser/base/content/test/about/POSTSearchEngine.xml",
+ setAsDefault: true,
+ });
+ 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..fddceda6f1
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_suggestion.js
@@ -0,0 +1,74 @@
+/* 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,
+ });
+ 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..8222a0f6e8
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError.js
@@ -0,0 +1,242 @@
+/* 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.isVisible(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.isVisible(tlsVersionNotice),
+ "TLS version notice is visible"
+ );
+
+ const learnMoreLink = doc.getElementById("learnMoreLink");
+ ok(ContentTaskUtils.isVisible(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.isVisible(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..c8028a4cf4
--- /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.startLoadingURIString(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..d82d4d6877
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError_native_fallback.js
@@ -0,0 +1,176 @@
+/* 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;
+ Assert.equal(
+ 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 {
+ Assert.notEqual(
+ 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..f5fd240643
--- /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.startLoadingURIString(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..aaa6eac8bd
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbar.js
@@ -0,0 +1,317 @@
+/* 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.startLoadingURIString(
+ 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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(
+ 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..8175c53c5d
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js
@@ -0,0 +1,183 @@
+/* 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",
+ },
+];
+
+async function emptyToolbarMessageVisible(visible, win = window) {
+ info("Empty toolbar message should be " + (visible ? "visible" : "hidden"));
+ let emptyMessage = win.document.getElementById("personal-toolbar-empty");
+ await BrowserTestUtils.waitForMutationCondition(
+ emptyMessage,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => emptyMessage.hidden != visible
+ );
+}
+
+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 exampleTab = 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
+ let placesItems = document.getElementById("PlacesToolbarItems");
+ let promiseBookmarksOnToolbar = BrowserTestUtils.waitForMutationCondition(
+ placesItems,
+ { childList: true },
+ () => placesItems.childNodes.length
+ );
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible on newtab",
+ });
+ await promiseBookmarksOnToolbar;
+ await emptyToolbarMessageVisible(false);
+
+ // 2: Toolbar should get hidden when switching tab to example.com
+ let promiseToolbar = waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar should be hidden on example.com",
+ });
+ await BrowserTestUtils.switchTab(gBrowser, exampleTab);
+ await promiseToolbar;
+
+ // 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",
+ });
+ await emptyToolbarMessageVisible(true);
+
+ // 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, exampleTab);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ promiseBookmarksOnToolbar = BrowserTestUtils.waitForMutationCondition(
+ placesItems,
+ { childList: true },
+ () => placesItems.childNodes.length
+ );
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible with Bookmarks Toolbar Items restored",
+ });
+ await promiseBookmarksOnToolbar;
+ await emptyToolbarMessageVisible(false);
+
+ // 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, exampleTab);
+ 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",
+ });
+ await emptyToolbarMessageVisible(true);
+
+ // 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, exampleTab);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible when there is a visible button in the toolbar",
+ });
+ await emptyToolbarMessageVisible(false);
+
+ await BrowserTestUtils.removeTab(newtab);
+ await BrowserTestUtils.removeTab(exampleTab);
+ 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..21507509bc
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutStopReload.js
@@ -0,0 +1,175 @@
+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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(
+ 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.startLoadingURIString(
+ 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..6fef282730
--- /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.startLoadingURIString(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..bc70aeea31
--- /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.isHidden(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..6c0f1242b6
--- /dev/null
+++ b/browser/base/content/test/about/head.js
@@ -0,0 +1,221 @@
+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.startLoadingURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+/**
+ * Wait for the user's default 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 == "CurrentEngine") {
+ 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,
+ `Waiting for ${expectedEngineNameChild} to be set`
+ );
+ 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.toml b/browser/base/content/test/alerts/browser.toml
new file mode 100644
index 0000000000..d0d56f7392
--- /dev/null
+++ b/browser/base/content/test/alerts/browser.toml
@@ -0,0 +1,28 @@
+[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..7568f1cc2d
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_close.js
@@ -0,0 +1,108 @@
+"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.
+ Assert.less(
+ 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.toml b/browser/base/content/test/backforward/browser.toml
new file mode 100644
index 0000000000..f066cdfdf2
--- /dev/null
+++ b/browser/base/content/test/backforward/browser.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+["browser_history_menu.js"]
+fail-if = ["a11y_checks"] # Bug 1854233 navigator-toolbox may not be focusable
+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.toml b/browser/base/content/test/caps/browser.toml
new file mode 100644
index 0000000000..0d3cbca7b3
--- /dev/null
+++ b/browser/base/content/test/caps/browser.toml
@@ -0,0 +1,8 @@
+[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.toml b/browser/base/content/test/captivePortal/browser.toml
new file mode 100644
index 0000000000..bdf17daf9c
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser.toml
@@ -0,0 +1,16 @@
+[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..aaccc77152
--- /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.
+ await ensurePortalNotification(win1);
+ ensureNoPortalTab(win1);
+ await 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);
+ await ensurePortalNotification(win1);
+ await 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..3d9cdb8616
--- /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.startLoadingURIString(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 = await ensurePortalNotification(win);
+ await testShowLoginPageButtonVisibility(notification, "visible");
+
+ async function testPortalTabSelectedAndButtonNotVisible() {
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be selected."
+ );
+ await 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();
+ await testPortalTabSelectedAndButtonNotVisible();
+
+ // Close the tab. The button should become visible.
+ BrowserTestUtils.removeTab(tab);
+ ensureNoPortalTab(win);
+ await 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);
+ await 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;
+ await 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..0108855a8e
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js
@@ -0,0 +1,66 @@
+/* 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();
+ Assert.equal(CPS.state, CPS.LOCKED_PORTAL, "Captive portal is locked again");
+ errorTab = await openCaptivePortalErrorTab();
+ let portalTab2 = await openCaptivePortalLoginTab(errorTab);
+ Assert.notEqual(
+ 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..6389338a6f
--- /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.isVisible(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.isVisible(advancedButton),
+ "Captive portal UI is visible"
+ );
+
+ info("Clicking on the advanced button");
+ const advPanel = doc.getElementById("badCertAdvancedPanel");
+ ok(
+ !ContentTaskUtils.isVisible(advPanel),
+ "Advanced panel is not yet visible"
+ );
+ await EventUtils.synthesizeMouseAtCenter(advancedButton, {}, content);
+ ok(ContentTaskUtils.isVisible(advPanel), "Advanced panel is now visible");
+
+ let advPanelContent = doc.getElementById("badCertTechnicalInfo");
+ ok(
+ ContentTaskUtils.isVisible(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.isVisible(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..9b89484ef1
--- /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.isVisible(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..9be8a3f431
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js
@@ -0,0 +1,230 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+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
+);
+const META_CANONICAL_CONTENT = `<meta http-equiv=\"refresh\" content=\"0;url=http://support.mozilla.org:8080/sumo\"/>`;
+
+const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
+ Ci.nsICaptivePortalService
+);
+
+let server;
+let loginPageShown = false;
+let showSumo = false;
+const SUMO_URL = "https://support.mozilla.org:8080/sumo";
+
+function redirectHandler(request, response) {
+ if (showSumo) {
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ META_CANONICAL_CONTENT,
+ META_CANONICAL_CONTENT.length
+ );
+ return;
+ }
+ if (loginPageShown) {
+ return;
+ }
+ response.setStatusLine(request.httpVersion, 302, "captive");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Location", LOGIN_URL);
+}
+
+function sumoHandler(request, response) {
+ response.setHeader("Content-Type", "text/html");
+ let content = `
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ Testing
+ </body>
+</html>
+`;
+ response.bodyOutputStream.write(content, content.length);
+}
+
+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.identity.add("http", "support.mozilla.org", 8080);
+ server.registerPathHandler("/success", redirectHandler);
+ server.registerPathHandler("/login", loginHandler);
+ server.registerPathHandler("/unlock", unlockHandler);
+ server.registerPathHandler("/sumo", sumoHandler);
+ server.start(8080);
+ registerCleanupFunction(async () => await server.stop());
+ info("Mock server is now set up for captive portal redirect");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["captivedetect.canonicalURL", CANONICAL_SUCCESS_URL],
+ ["captivedetect.canonicalContent", META_CANONICAL_CONTENT],
+ ["network.dns.native-is-localhost", true],
+ ],
+ });
+});
+
+// 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);
+});
+
+// Test that the captive portal page is closed after a successful login
+// even if it's self-refreshed to support.mozilla.org
+add_task(async function checkCaptivePortalTabCloseOnCanonicalURL_three() {
+ loginPageShown = false;
+ await portalDetected();
+ let errorTab = await openCaptivePortalErrorTab();
+ let tab = await openCaptivePortalLoginTab(errorTab, LOGIN_URL);
+ let browser = tab.linkedBrowser;
+
+ let errorPageReloaded = BrowserTestUtils.waitForErrorPage(
+ errorTab.linkedBrowser
+ );
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(tab);
+ showSumo = true;
+ 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);
+ });
+
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+
+ await TestUtils.waitForCondition(
+ () => CPS.state == CPS.UNLOCKED_PORTAL,
+ "Captive portal is released"
+ );
+
+ 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");
+ showSumo = false;
+ gBrowser.removeTab(errorTab);
+});
diff --git a/browser/base/content/test/captivePortal/head.js b/browser/base/content/test/captivePortal/head.js
new file mode 100644
index 0000000000..77105456a2
--- /dev/null
+++ b/browser/base/content/test/captivePortal/head.js
@@ -0,0 +1,270 @@
+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 = await ensurePortalNotification(win);
+
+ if (aLongRecheck) {
+ ensureNoPortalTab(win);
+ await 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."
+ );
+ await 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."
+ );
+}
+
+async function ensurePortalNotification(win) {
+ await BrowserTestUtils.waitForMutationCondition(
+ win.gNavToolbox,
+ { childList: true },
+ () =>
+ win.gNavToolbox
+ .querySelector("notification-message")
+ ?.getAttribute("value") == PORTAL_NOTIFICATION_VALUE
+ );
+
+ 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).
+async function testShowLoginPageButtonVisibility(notification, visibility) {
+ await notification.updateComplete;
+ 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.toml b/browser/base/content/test/chrome/chrome.toml
new file mode 100644
index 0000000000..9e0d1d6d93
--- /dev/null
+++ b/browser/base/content/test/chrome/chrome.toml
@@ -0,0 +1,5 @@
+[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.toml b/browser/base/content/test/contentTheme/browser.toml
new file mode 100644
index 0000000000..a8a3e6d502
--- /dev/null
+++ b/browser/base/content/test/contentTheme/browser.toml
@@ -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.toml b/browser/base/content/test/contextMenu/browser.toml
new file mode 100644
index 0000000000..3eb6a1d606
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser.toml
@@ -0,0 +1,101 @@
+[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..ebeb4bdb04
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu.js
@@ -0,0 +1,2034 @@
+"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 hasStripOnShare = Services.prefs.getBoolPref(
+ "privacy.query_stripping.strip_on_share.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 about_preferences_base = "about:preferences";
+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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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 exited = BrowserTestUtils.waitForEvent(window, "MozDOMFullscreen:Exited");
+
+ 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;
+ }
+ );
+ },
+ });
+ await exited;
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return !TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS");
+ });
+
+ if (AppConstants.platform == "macosx") {
+ // On macOS, the fullscreen transition takes some extra time
+ // to complete, and we don't receive events for it. We need to
+ // wait for it to complete or else input events in the next test
+ // might get eaten up. This is the best we can currently do.
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ }
+
+ await SimpleTest.promiseFocus(window);
+});
+
+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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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,
+ ...(hasStripOnShare ? ["context-stripOnShareLink", 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() {
+ lastElementSelector = null;
+ gBrowser.removeCurrentTab();
+});
+
+/*
+ * Testing that Copy without Site Tracking option does not
+ * appear on internal about: pages.
+ */
+add_task(async function test_strip_on_share_on_secure_about_page() {
+ let url = about_preferences_base;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url,
+ });
+
+ let browser2 = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser2, [], () => {
+ let link = content.document.createElement("a");
+ link.href = "https://mozilla.com";
+ link.textContent = "link with query param";
+ link.id = "link-test-strip";
+ content.document.body.appendChild(link);
+ });
+
+ // the Copy without Site Tracking option should not
+ // show up within internal about: pages
+ await test_contextmenu("#link-test-strip", [
+ "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
+ lastElementSelector = null;
+ 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..991a55af70
--- /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.startLoadingURIString(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..7547d93cfc
--- /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.isVisible(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..d8f349f7f1
--- /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.isVisible(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..ceabdfd313
--- /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.startLoadingURIString(
+ 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.startLoadingURIString(
+ 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_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..4df5ee6b21
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_share_win.js
@@ -0,0 +1,71 @@
+/* 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");
+
+ 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..2360cf6b22
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..8a04bd180c
--- /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
+add_setup(async function () {
+ let isWindows = AppConstants.platform == "win";
+ 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.startLoadingURIString(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..435b1aa0ff
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_strip_on_share_link.js
@@ -0,0 +1,189 @@
+/* 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 menu item does not show if the pref is disabled
+add_task(async function testPrefDisabled() {
+ let validUrl = "https://www.example.com/";
+ let shortenedUrl = "https://www.example.com/";
+ await testStripOnShare({
+ originalURI: validUrl,
+ strippedURI: shortenedUrl,
+ prefEnabled: false,
+ useTestList: false,
+ });
+});
+
+// Menu item should be visible, url should be stripped.
+add_task(async function testQueryParamIsStrippedSelectURL() {
+ let validUrl = "https://www.example.com/?stripParam=1234";
+ let shortenedUrl = "https://www.example.com/";
+ await testStripOnShare({
+ originalURI: validUrl,
+ strippedURI: shortenedUrl,
+ prefEnabled: true,
+ useTestList: false,
+ });
+});
+
+// Menu item should be visible, ensuring only parameters on the list are stripped
+add_task(async function testQueryParamIsStripped() {
+ let validUrl = "https://www.example.com/?stripParam=1234&otherParam=1234";
+ let shortenedUrl = "https://www.example.com/?otherParam=1234";
+ await testStripOnShare({
+ originalURI: validUrl,
+ strippedURI: shortenedUrl,
+ prefEnabled: true,
+ useTestList: false,
+ });
+});
+
+// Menu item should be visible, if there is nothing to strip, url should remain the same
+add_task(async function testURLIsCopiedWithNoParams() {
+ let validUrl = "https://www.example.com/";
+ let shortenedUrl = "https://www.example.com/";
+ await testStripOnShare({
+ originalURI: validUrl,
+ strippedURI: shortenedUrl,
+ prefEnabled: true,
+ useTestList: false,
+ });
+});
+
+// Testing site specific parameter stripping
+add_task(async function testQueryParamIsStrippedForSiteSpecific() {
+ let validUrl = "https://www.example.com/?test_2=1234";
+ let shortenedUrl = "https://www.example.com/";
+ await testStripOnShare({
+ originalURI: validUrl,
+ strippedURI: shortenedUrl,
+ prefEnabled: true,
+ useTestList: true,
+ });
+});
+
+// Ensuring site specific parameters are not stripped for other sites
+add_task(async function testQueryParamIsNotStrippedForWrongSiteSpecific() {
+ let validUrl = "https://www.example.com/?test_3=1234";
+ let shortenedUrl = "https://www.example.com/?test_3=1234";
+ await testStripOnShare({
+ originalURI: validUrl,
+ strippedURI: shortenedUrl,
+ prefEnabled: true,
+ useTestList: true,
+ });
+});
+
+/**
+ * Opens a new tab, opens the context menu and checks that the strip-on-share menu item is visible.
+ * Checks that the stripped version of the url is copied to the clipboard.
+ *
+ * @param {string} originalURI - The orginal url before the stripping occurs
+ * @param {string} strippedURI - The expected url after stripping occurs
+ * @param {boolean} prefEnabled - Whether StripOnShare pref is enabled
+ * @param {boolean} useTestList - Whether the StripOnShare or Test list should be used
+ */
+async function testStripOnShare({
+ originalURI,
+ strippedURI,
+ prefEnabled,
+ useTestList,
+}) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.strip_on_share.enabled", prefEnabled],
+ ["privacy.query_stripping.strip_on_share.enableTestMode", useTestList],
+ ],
+ });
+
+ if (useTestList) {
+ let testJson = {
+ global: {
+ queryParams: ["utm_ad"],
+ topLevelSites: ["*"],
+ },
+ example: {
+ queryParams: ["test_2", "test_1"],
+ topLevelSites: ["www.example.com"],
+ },
+ exampleNet: {
+ queryParams: ["test_3", "test_4"],
+ topLevelSites: ["www.example.net"],
+ },
+ };
+
+ await listService.testSetList(testJson);
+ }
+
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ // Prepare a link
+ await SpecialPowers.spawn(
+ browser,
+ [originalURI],
+ async function (startingURI) {
+ let link = content.document.createElement("a");
+ link.href = startingURI;
+ 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");
+ if (prefEnabled) {
+ Assert.ok(
+ BrowserTestUtils.isVisible(stripOnShare),
+ "Menu item is visible"
+ );
+ // Make sure the stripped link will be copied to the clipboard
+ await SimpleTest.promiseClipboardChange(strippedURI, () => {
+ contextMenu.activateItem(stripOnShare);
+ });
+ } else {
+ Assert.ok(
+ !BrowserTestUtils.isVisible(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.toml b/browser/base/content/test/favicons/browser.toml
new file mode 100644
index 0000000000..d8a9a7469a
--- /dev/null
+++ b/browser/base/content/test/favicons/browser.toml
@@ -0,0 +1,150 @@
+[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_empty_data.js"]
+support-files = [
+ "blank.html",
+ "file_favicon_empty.html",
+]
+
+["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..2614e8abd5
--- /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.startLoadingURIString(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..46f5a78552
--- /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.startLoadingURIString(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..72f6e69931
--- /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.startLoadingURIString(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..3a5f977eab
--- /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.startLoadingURIString(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..97fc01db1d
--- /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.startLoadingURIString(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..b7470334b8
--- /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.startLoadingURIString(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..b8215dcc3e
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js
@@ -0,0 +1,148 @@
+"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";
+
+// Runs the given task in the document of the browser.
+function runInDoc(browser, task) {
+ return ContentTask.spawn(browser, `(${task.toString()})();`, scriptStr => {
+ let script = content.document.createElement("script");
+ script.textContent = scriptStr;
+ content.document.body.appendChild(script);
+
+ // Link events are dispatched asynchronously so allow the event loop to run
+ // to ensure that any events are actually dispatched before returning.
+ return new Promise(resolve => content.setTimeout(resolve, 0));
+ });
+}
+
+/*
+ * 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 FaviconLoader.sys.mjs, 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.startLoadingURIString(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);
+});
+
+/*
+ * This verifies that creating and manipulating link elements inside document
+ * fragments doesn't trigger the link events.
+ */
+add_task(async function () {
+ let extraTab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ROOT
+ ));
+ let browser = extraTab.linkedBrowser;
+
+ let domLinkAddedFired = 0;
+ let domLinkChangedFired = 0;
+ const linkAddedHandler = event => domLinkAddedFired++;
+ const linkChangedhandler = event => domLinkChangedFired++;
+ BrowserTestUtils.addContentEventListener(
+ browser,
+ "DOMLinkAdded",
+ linkAddedHandler
+ );
+ BrowserTestUtils.addContentEventListener(
+ browser,
+ "DOMLinkChanged",
+ linkChangedhandler
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_ROOT + "blank.html");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ is(domLinkAddedFired, 0, "Should have been no link add events");
+ is(domLinkChangedFired, 0, "Should have been no link change events");
+
+ await runInDoc(browser, () => {
+ let fragment = document
+ .createRange()
+ .createContextualFragment(
+ '<link type="image/ico" href="file_generic_favicon.ico" rel="icon">'
+ );
+ fragment.firstElementChild.setAttribute("type", "image/png");
+ });
+
+ is(domLinkAddedFired, 0, "Should have been no link add events");
+ is(domLinkChangedFired, 0, "Should have been no link change events");
+
+ await runInDoc(browser, () => {
+ let fragment = document.createDocumentFragment();
+ let link = document.createElement("link");
+ link.setAttribute("href", "file_generic_favicon.ico");
+ link.setAttribute("rel", "icon");
+ link.setAttribute("type", "image/ico");
+
+ fragment.appendChild(link);
+ link.setAttribute("type", "image/png");
+ });
+
+ is(domLinkAddedFired, 0, "Should have been no link add events");
+ is(domLinkChangedFired, 0, "Should have been no link change events");
+
+ let expectedFavicon = TEST_ROOT + "file_generic_favicon.ico";
+ let faviconPromise = waitForFavicon(browser, expectedFavicon);
+
+ // Moving an element from the fragment into the DOM should trigger the add
+ // events and start favicon loading.
+ await runInDoc(browser, () => {
+ let fragment = document
+ .createRange()
+ .createContextualFragment(
+ '<link type="image/ico" href="file_generic_favicon.ico" rel="icon">'
+ );
+ document.head.appendChild(fragment);
+ });
+
+ is(domLinkAddedFired, 1, "Should have been one link add events");
+ is(domLinkChangedFired, 0, "Should have been no link change events");
+
+ await faviconPromise;
+
+ 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..7e04344db5
--- /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.startLoadingURIString(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..d9b5a41dbe
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_crossorigin.js
@@ -0,0 +1,64 @@
+/* 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.startLoadingURIString(
+ 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_empty_data.js b/browser/base/content/test/favicons/browser_favicon_empty_data.js
new file mode 100644
index 0000000000..5fb8b9b654
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_empty_data.js
@@ -0,0 +1,72 @@
+/* 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 PAGE_URL = TEST_ROOT + "blank.html";
+const ICON_URL = TEST_ROOT + "file_bug970276_favicon1.ico";
+const ICON_DATAURI_START = "data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAA";
+
+const EMPTY_PAGE_URL = TEST_ROOT + "file_favicon_empty.html";
+const EMPTY_ICON_URL = "data:image/x-icon";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async browser => {
+ let iconBox = gBrowser
+ .getTabForBrowser(browser)
+ .querySelector(".tab-icon-image");
+ await addContentLinkForIconUrl(ICON_URL, browser);
+ Assert.ok(
+ browser.mIconURL.startsWith(ICON_DATAURI_START),
+ "Favicon is correctly set."
+ );
+
+ // Give some time to ensure the icon is rendered.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 200));
+ let firstIconShotDataURL = TestUtils.screenshotArea(iconBox, window);
+
+ let browserLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ EMPTY_PAGE_URL
+ );
+ BrowserTestUtils.startLoadingURIString(browser, EMPTY_PAGE_URL);
+ let iconChanged = waitForFavicon(browser, EMPTY_ICON_URL);
+ await Promise.all([browserLoaded, iconChanged]);
+ Assert.equal(browser.mIconURL, EMPTY_ICON_URL, "Favicon was changed.");
+
+ // Give some time to ensure the icon is rendered.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 200));
+ let secondIconShotDataURL = TestUtils.screenshotArea(iconBox, window);
+
+ Assert.notEqual(
+ firstIconShotDataURL,
+ secondIconShotDataURL,
+ "Check the first icon didn't persist as the second one is invalid"
+ );
+ }
+ );
+});
+
+async function addContentLinkForIconUrl(url, browser) {
+ let iconChanged = waitForFavicon(browser, url);
+ info("Adding <link> to: " + url);
+ await ContentTask.spawn(browser, url, 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);
+ });
+ info("Awaiting icon change event for:" + url);
+ await iconChanged;
+}
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..10c2b8f24e
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_load.js
@@ -0,0 +1,167 @@
+/**
+ * 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",
+});
+
+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 = Promise.withResolvers();
+ 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..ed332e7413
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_referer.js
@@ -0,0 +1,65 @@
+/* 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.startLoadingURIString(
+ 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.startLoadingURIString(
+ 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..ab1fb13775
--- /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.startLoadingURIString(
+ 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..f619425909
--- /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.startLoadingURIString(
+ browser,
+ testPath + "file_with_favicon.html"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Got correct icon.");
+
+ BrowserTestUtils.startLoadingURIString(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..abccff52ac
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_oversized.js
@@ -0,0 +1,28 @@
+/* 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.startLoadingURIString(
+ 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..b574f5a86a
--- /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.startLoadingURIString(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..01a456abd1
--- /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.startLoadingURIString(
+ 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.startLoadingURIString(
+ 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_empty.html b/browser/base/content/test/favicons/file_favicon_empty.html
new file mode 100644
index 0000000000..28389f5927
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_empty.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="data:image/x-icon" type="image/ico" id="i">
+</head>
+
+<body>
+</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.toml b/browser/base/content/test/forms/browser.toml
new file mode 100644
index 0000000000..33d73ba8bf
--- /dev/null
+++ b/browser/base/content/test/forms/browser.toml
@@ -0,0 +1,34 @@
+[DEFAULT]
+prefs = ["gfx.font_loader.delay=0", "dom.select.showPicker.enabled=true"]
+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_hr.js"]
+
+["browser_selectpopup_large.js"]
+
+["browser_selectpopup_searchfocus.js"]
+fail-if = ["a11y_checks"] # Bug 1854233 input may not be labeled
+
+["browser_selectpopup_showPicker.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..abcdee486f
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -0,0 +1,914 @@
+/* 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.
+ Assert.greaterOrEqual(
+ selectPopup.getBoundingClientRect().height,
+ selectPopup.lastElementChild.getBoundingClientRect().height * 4,
+ "Height contains at least 4 items"
+ );
+ Assert.less(
+ 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);
+
+ Assert.less(
+ 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..63cece0ce5
--- /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.platform == "win") {
+ 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_hr.js b/browser/base/content/test/forms/browser_selectpopup_hr.js
new file mode 100644
index 0000000000..85a44be66c
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_hr.js
@@ -0,0 +1,55 @@
+add_task(async function test_hr() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.select.customstyling", true]],
+ });
+
+ const PAGE_CONTENT = `
+<!doctype html>
+<select>
+<option>One</option>
+<hr style="color: red; background-color: blue">
+<option>Two</option>
+</select>`;
+
+ const pageUrl = "data:text/html," + encodeURIComponent(PAGE_CONTENT);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ const selectPopup = await openSelectPopup("click");
+ const menulist = selectPopup.parentNode;
+
+ const optionOne = selectPopup.children[0];
+ const separator = selectPopup.children[1];
+ const optionTwo = selectPopup.children[2];
+
+ is(optionOne.textContent, "One", "First option has expected text content");
+
+ is(separator.tagName, "menuseparator", "Separator is menuseparator");
+
+ const separatorStyle = getComputedStyle(separator);
+
+ is(
+ separatorStyle.color,
+ "rgb(255, 0, 0)",
+ "Separator color is specified CSS color"
+ );
+
+ is(
+ separatorStyle.backgroundColor,
+ "rgba(0, 0, 0, 0)",
+ "Separator background-color is not set to specified CSS color"
+ );
+
+ is(optionTwo.textContent, "Two", "Second option has expected text content");
+
+ is(menulist.activeChild, optionOne, "First option is selected to start");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ is(
+ menulist.activeChild,
+ optionTwo,
+ "Second option is selected after arrow down"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
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..722e0d9588
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_large.js
@@ -0,0 +1,323 @@
+/* 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is sent on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ labelRule: false });
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 25,
+ { type: "mouseup" },
+ win
+ );
+ AccessibilityUtils.resetEnv();
+ 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);
+ Assert.greaterOrEqual(
+ rect.top - marginTop,
+ browserRect.top,
+ "Popup top position in within browser area"
+ );
+ Assert.lessOrEqual(
+ 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.
+ const fuzzFactor = 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;
+ }
+}
+
+// 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.startLoadingURIString(
+ 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_showPicker.js b/browser/base/content/test/forms/browser_selectpopup_showPicker.js
new file mode 100644
index 0000000000..9c978cb411
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_showPicker.js
@@ -0,0 +1,60 @@
+const PAGE = `
+<!doctype html>
+<select>
+ <option>ABC</option>
+ <option>DEFG</option>
+</select>
+`;
+
+add_task(async function test_showPicker() {
+ const url = "data:text/html," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.notifyUserGestureActivation();
+ content.document.querySelector("select").showPicker();
+ });
+
+ let selectPopup = await popupShownPromise;
+ is(
+ selectPopup.state,
+ "open",
+ "select popup is open after calling showPicker"
+ );
+ }
+ );
+});
+
+add_task(async function test_showPicker_alreadyOpen() {
+ const url = "data:text/html," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let selectPopup = await openSelectPopup("click");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.notifyUserGestureActivation();
+ content.document.querySelector("select").showPicker();
+ });
+
+ // Wait some time for potential (unwanted) closing.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ is(
+ selectPopup.state,
+ "open",
+ "select popup is still open after calling showPicker"
+ );
+ }
+ );
+});
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..9b4fcd860a
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_toplevel.js
@@ -0,0 +1,25 @@
+/* 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);
+ // We intentionally turn off this a11y check, because the following click
+ // is sent on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ labelRule: false });
+ EventUtils.synthesizeMouseAtCenter(select, {});
+ AccessibilityUtils.resetEnv();
+
+ 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..08c804a60d
--- /dev/null
+++ b/browser/base/content/test/fullscreen/FullscreenFrame.sys.mjs
@@ -0,0 +1,102 @@
+/* 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":
+ 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.toml b/browser/base/content/test/fullscreen/browser.toml
new file mode 100644
index 0000000000..b0985db527
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser.toml
@@ -0,0 +1,65 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "open_and_focus_helper.html",
+]
+
+["browser_bug1557041.js"]
+
+["browser_bug1620341.js"]
+support-files = [
+ "fullscreen.html",
+ "fullscreen_frame.html",
+]
+
+["browser_domFS_statuspanel.js"]
+
+["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'", # Bug 1818795
+ "os == 'win'", # 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
+ "os == 'mac' && !debug", # Bug 1861827
+]
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..7e4545af39
--- /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.startLoadingURIString(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..ced061fbf8
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_bug1620341.js
@@ -0,0 +1,102 @@
+/* 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")
+ );
+
+ ok(
+ !gBrowser.tabContainer.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 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_domFS_statuspanel.js b/browser/base/content/test/fullscreen/browser_domFS_statuspanel.js
new file mode 100644
index 0000000000..b6f6f9bc5e
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_domFS_statuspanel.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* Tests that the status panel gets cleared when entering DOM fullscreen
+ * (bug 1850993), and that we don't show network statuses in DOM fullscreen
+ * (bug 1853896). */
+
+// DOM FS tests 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);
+
+let statuspanel = document.getElementById("statuspanel");
+let statuspanelLabel = document.getElementById("statuspanel-label");
+
+async function withDomFsTab(beforeEnter, afterEnter) {
+ let url = "https://example.com/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ true,
+ true
+ );
+ let browser = tab.linkedBrowser;
+
+ await beforeEnter();
+ info("Entering DOM fullscreen");
+ await changeFullscreen(browser, true);
+ is(document.fullscreenElement, browser, "Entered DOM fullscreen");
+ await afterEnter();
+
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_overlink() {
+ const overlink = "https://example.com";
+ let setAndCheckOverLink = async info => {
+ XULBrowserWindow.setOverLink(overlink);
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.isVisible(statuspanel),
+ `statuspanel should become visible after setting overlink ${info}`
+ );
+ is(
+ statuspanelLabel.value,
+ BrowserUIUtils.trimURL(overlink),
+ `statuspanel has expected value after setting overlink ${info}`
+ );
+ };
+ await withDomFsTab(
+ async function () {
+ await setAndCheckOverLink("outside of DOM FS");
+ },
+ async function () {
+ await TestUtils.waitForCondition(
+ () => !BrowserTestUtils.isVisible(statuspanel),
+ "statuspanel with overlink should hide when entering DOM FS"
+ );
+ await setAndCheckOverLink("while in DOM FS");
+ }
+ );
+});
+
+add_task(async function test_networkstatus() {
+ await withDomFsTab(
+ async function () {
+ XULBrowserWindow.status = "test1";
+ XULBrowserWindow.busyUI = true;
+ StatusPanel.update();
+ ok(
+ BrowserTestUtils.isVisible(statuspanel),
+ "statuspanel is visible before entering DOM FS"
+ );
+ is(statuspanelLabel.value, "test1", "statuspanel has expected value");
+ },
+ async function () {
+ is(
+ XULBrowserWindow.busyUI,
+ true,
+ "browser window still considered busy (i.e. loading stuff) when entering DOM FS"
+ );
+ is(
+ XULBrowserWindow.status,
+ "",
+ "network status cleared when entering DOM FS"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(statuspanel),
+ "statuspanel with network status should should hide when entering DOM FS"
+ );
+ }
+ );
+});
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..9d9891acd2
--- /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.isVisible(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..48729a723a
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_permissions_prompt.js
@@ -0,0 +1,290 @@
+/* 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();
+});
+
+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, {});
+}
+
+add_task(
+ async function test_permission_prompt_closes_fullscreen_and_extends_security_delay() {
+ const TEST_SECURITY_DELAY = 500;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.webnotifications.requireuserinteraction", false],
+ ["permissions.fullscreen.allowed", false],
+ ["security.notification_enable_delay", TEST_SECURITY_DELAY],
+ // macOS is not affected by the sec delay bug because it uses the native
+ // macOS full screen API. Revert back to legacy behavior so we can also
+ // test on macOS. If this pref is removed in the future we can consider
+ // skipping the testcase for macOS altogether.
+ ["full-screen-api.macos-native-full-screen", 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.waitForPopupEvent(
+ window.PopupNotifications.panel,
+ "shown"
+ );
+ let fullScreenExit = waitForFullScreenState(browser, false);
+
+ info("Requesting notification permission");
+ requestNotificationPermission(browser).catch(() => {});
+ await popupShown;
+
+ let notificationHiddenPromise = BrowserTestUtils.waitForPopupEvent(
+ window.PopupNotifications.panel,
+ "hidden"
+ );
+
+ info("Waiting for full-screen exit");
+ await fullScreenExit;
+
+ info("Wait for original security delay to expire.");
+ SimpleTest.requestFlakyTimeout(
+ "Wait for original security delay to expire."
+ );
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, TEST_SECURITY_DELAY));
+
+ info(
+ "Trigger main action via button click during the extended security delay"
+ );
+ triggerMainCommand(PopupNotifications.panel);
+
+ let notification = PopupNotifications.getNotification(
+ "web-notifications",
+ gBrowser.selectedBrowser
+ );
+
+ // Linux in CI seems to skip the full screen animation, which means its not
+ // affected by the bug and we can't test extension of the sec delay here.
+ if (Services.appinfo.OS == "Linux") {
+ todo(
+ notification &&
+ !notification.dismissed &&
+ BrowserTestUtils.isVisible(PopupNotifications.panel.firstChild),
+ "Notification should still be open because we clicked during the security delay."
+ );
+ } else {
+ ok(
+ notification &&
+ !notification.dismissed &&
+ BrowserTestUtils.isVisible(PopupNotifications.panel.firstChild),
+ "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) {
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+ // Remove the granted notification permission.
+ Services.perms.removeAll();
+ return;
+ }
+
+ Assert.greater(
+ notification.timeShown,
+ performance.now(),
+ "Notification timeShown property should be in the future, because the security delay was extended."
+ );
+
+ // 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(
+ "web-notifications",
+ gBrowser.selectedBrowser
+ ),
+ "Should not longer see the notification."
+ );
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+ // Remove the granted notification permission.
+ Services.perms.removeAll();
+ }
+);
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..5dd71e1a92
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js
@@ -0,0 +1,136 @@
+/* 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);
+ },
+ () => {
+ // Async fullscreen transitions will swallow the repaint of the tab,
+ // preventing us from detecting that we've successfully changed
+ // fullscreen. Supply an action to switch back to the tab after the
+ // fullscreen event has been received, which will ensure that the
+ // tab is repainted when the DOMFullscreenChild is listening for it.
+ info("Calling switchTab()");
+ BrowserTestUtils.switchTab(gBrowser, tab);
+ }
+ );
+
+ // 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"
+ );
+ });
+ },
+ () => {
+ // Async fullscreen transitions will swallow the repaint of the tab,
+ // preventing us from detecting that we've successfully changed
+ // fullscreen. Supply an action to switch back to the tab after the
+ // fullscreen event has been received, which will ensure that the
+ // tab is repainted when the DOMFullscreenChild is listening for it.
+ info("Calling switchTab()");
+ BrowserTestUtils.switchTab(gBrowser, tab);
+ }
+ );
+
+ // 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..4d5543461e
--- /dev/null
+++ b/browser/base/content/test/fullscreen/head.js
@@ -0,0 +1,172 @@
+const TEST_URL =
+ "https://example.com/browser/browser/base/content/test/fullscreen/open_and_focus_helper.html";
+
+function waitForFullScreenState(browser, state, actionAfterFSEvent) {
+ 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;
+ if (actionAfterFSEvent) {
+ actionAfterFSEvent();
+ }
+ },
+ { 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,
+ actionAfterFSEvent
+) {
+ let fsPromise = waitForFullScreenState(browser, false, actionAfterFSEvent);
+ 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.toml b/browser/base/content/test/general/browser.toml
new file mode 100644
index 0000000000..6928ba2d4b
--- /dev/null
+++ b/browser/base/content/test/general/browser.toml
@@ -0,0 +1,536 @@
+###############################################################################
+# 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_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_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_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
+# 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"]
+skip-if = ["a11y_checks"] # Bugs 1858041 and 1835079 for causing intermittent crashes
+# 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",
+ "win11_2009 && 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"]
+# 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 = ["win11_2009 && 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..0809553404
--- /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("aria-label", "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..c7829d16fe
--- /dev/null
+++ b/browser/base/content/test/general/browser_alltabslistener.js
@@ -0,0 +1,332 @@
+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) {
+ Assert.equal(
+ 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.startLoadingURIString(gBackgroundBrowser, kBasePage);
+ BrowserTestUtils.startLoadingURIString(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.startLoadingURIString(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..dbd8b272fd
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1297539.js
@@ -0,0 +1,126 @@
+/* -*- 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,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+ 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..aa3569c93d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug356571.js
@@ -0,0 +1,101 @@
+// 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
+ Assert.equal(
+ 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..f8189fb268
--- /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:robots",
+ "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..68e2e99511
--- /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.startLoadingURIString(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..f90de6c530
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug431826.js
@@ -0,0 +1,59 @@
+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.startLoadingURIString(
+ 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_bug477014.js b/browser/base/content/test/general/browser_bug477014.js
new file mode 100644
index 0000000000..e51f7b48e6
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug477014.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/. */
+
+// That's a gecko!
+const iconURLSpec =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+var testPage = "data:text/plain,test bug 477014";
+
+add_task(async function () {
+ let tabToDetach = BrowserTestUtils.addTab(gBrowser, testPage);
+ await BrowserTestUtils.browserStopped(tabToDetach.linkedBrowser);
+
+ gBrowser.setIcon(
+ tabToDetach,
+ iconURLSpec,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ tabToDetach.setAttribute("busy", "true");
+
+ // detach and set the listener on the new window
+ let newWindow = gBrowser.replaceTabWithWindow(tabToDetach);
+ await BrowserTestUtils.waitForEvent(
+ tabToDetach.linkedBrowser,
+ "SwapDocShells"
+ );
+
+ is(
+ newWindow.gBrowser.selectedTab.hasAttribute("busy"),
+ true,
+ "Busy attribute should be correct"
+ );
+ is(newWindow.gBrowser.getIcon(), iconURLSpec, "Icon should be correct");
+
+ newWindow.close();
+});
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..c1c848aae3
--- /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.startLoadingURIString(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..ccb467a234
--- /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.startLoadingURIString(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..b9446e3d34
--- /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.startLoadingURIString(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..79cd10c591
--- /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.startLoadingURIString(
+ browser,
+ "data:text/plain;charset=utf-8,2"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.startLoadingURIString(
+ 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..df2dcb4b0d
--- /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.startLoadingURIString(
+ 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..8aeb5f6221
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..bd86f8e2b3
--- /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.startLoadingURIString(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..ea3c39222e
--- /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 = Promise.withResolvers();
+
+ 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..9a6b781f91
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug832435.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/. */
+
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+add_task(async function test() {
+ gBrowser.selectedBrowser.focus();
+ await UrlbarTestUtils.inputIntoURLBar(
+ window,
+ "javascript: var foo = '11111111'; "
+ );
+ ok(gURLBar.focused, "Address bar is focused");
+ EventUtils.synthesizeKey("VK_RETURN");
+
+ // javscript: URIs are evaluated async.
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ ok(true, "Evaluated without crashing");
+});
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_contentAreaClick.js b/browser/base/content/test/general/browser_contentAreaClick.js
new file mode 100644
index 0000000000..1a788e823f
--- /dev/null
+++ b/browser/base/content/test/general/browser_contentAreaClick.js
@@ -0,0 +1,329 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 549340.
+ * Test for browser.js::contentAreaClick() util.
+ *
+ * The test opens a new browser window, then replaces browser.js methods invoked
+ * by contentAreaClick with a mock function that tracks which methods have been
+ * called.
+ * Each sub-test synthesizes a mouse click event on links injected in content,
+ * the event is collected by a click handler that ensures that contentAreaClick
+ * correctly prevent default events, and follows the correct code path.
+ */
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+var gTests = [
+ {
+ desc: "Simple left click",
+ setup() {},
+ clean() {},
+ event: {},
+ targets: ["commonlink", "mathlink", "svgxlink", "maplink"],
+ expectedInvokedMethods: [],
+ preventDefault: false,
+ },
+
+ {
+ desc: "Ctrl/Cmd left click",
+ setup() {},
+ clean() {},
+ event: { ctrlKey: true, metaKey: true },
+ targets: ["commonlink", "mathlink", "svgxlink", "maplink"],
+ expectedInvokedMethods: ["urlSecurityCheck", "openLinkIn"],
+ preventDefault: true,
+ },
+
+ // The next test should just be like Alt click.
+ {
+ desc: "Shift+Alt left click",
+ setup() {
+ Services.prefs.setBoolPref("browser.altClickSave", true);
+ },
+ clean() {
+ Services.prefs.clearUserPref("browser.altClickSave");
+ },
+ event: { shiftKey: true, altKey: true },
+ targets: ["commonlink", "maplink"],
+ expectedInvokedMethods: ["gatherTextUnder", "saveURL"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Shift+Alt left click on XLinks",
+ setup() {
+ Services.prefs.setBoolPref("browser.altClickSave", true);
+ },
+ clean() {
+ Services.prefs.clearUserPref("browser.altClickSave");
+ },
+ event: { shiftKey: true, altKey: true },
+ targets: ["mathlink", "svgxlink"],
+ expectedInvokedMethods: ["saveURL"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Shift click",
+ setup() {},
+ clean() {},
+ event: { shiftKey: true },
+ targets: ["commonlink", "mathlink", "svgxlink", "maplink"],
+ expectedInvokedMethods: ["urlSecurityCheck", "openLinkIn"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Alt click",
+ setup() {
+ Services.prefs.setBoolPref("browser.altClickSave", true);
+ },
+ clean() {
+ Services.prefs.clearUserPref("browser.altClickSave");
+ },
+ event: { altKey: true },
+ targets: ["commonlink", "maplink"],
+ expectedInvokedMethods: ["gatherTextUnder", "saveURL"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Alt click on XLinks",
+ setup() {
+ Services.prefs.setBoolPref("browser.altClickSave", true);
+ },
+ clean() {
+ Services.prefs.clearUserPref("browser.altClickSave");
+ },
+ event: { altKey: true },
+ targets: ["mathlink", "svgxlink"],
+ expectedInvokedMethods: ["saveURL"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Panel click",
+ setup() {},
+ clean() {},
+ event: {},
+ targets: ["panellink"],
+ expectedInvokedMethods: ["urlSecurityCheck", "loadURI"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Simple middle click opentab",
+ setup() {},
+ clean() {},
+ event: { button: 1 },
+ wantedEvent: "auxclick",
+ targets: ["commonlink", "mathlink", "svgxlink", "maplink"],
+ expectedInvokedMethods: ["urlSecurityCheck", "openLinkIn"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Simple middle click openwin",
+ setup() {
+ Services.prefs.setBoolPref("browser.tabs.opentabfor.middleclick", false);
+ },
+ clean() {
+ Services.prefs.clearUserPref("browser.tabs.opentabfor.middleclick");
+ },
+ event: { button: 1 },
+ wantedEvent: "auxclick",
+ targets: ["commonlink", "mathlink", "svgxlink", "maplink"],
+ expectedInvokedMethods: ["urlSecurityCheck", "openLinkIn"],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Middle mouse paste",
+ setup() {
+ Services.prefs.setBoolPref("middlemouse.contentLoadURL", true);
+ Services.prefs.setBoolPref("general.autoScroll", false);
+ },
+ clean() {
+ Services.prefs.clearUserPref("middlemouse.contentLoadURL");
+ Services.prefs.clearUserPref("general.autoScroll");
+ },
+ event: { button: 1 },
+ wantedEvent: "auxclick",
+ targets: ["emptylink"],
+ expectedInvokedMethods: ["middleMousePaste"],
+ preventDefault: true,
+ },
+];
+
+// Array of method names that will be replaced in the new window.
+var gReplacedMethods = [
+ "middleMousePaste",
+ "urlSecurityCheck",
+ "loadURI",
+ "gatherTextUnder",
+ "saveURL",
+ "openLinkIn",
+ "getShortcutOrURIAndPostData",
+];
+
+// Returns the target object for the replaced method.
+function getStub(replacedMethod) {
+ let targetObj =
+ replacedMethod == "getShortcutOrURIAndPostData" ? UrlbarUtils : gTestWin;
+ return targetObj[replacedMethod];
+}
+
+// Reference to the new window.
+var gTestWin = null;
+
+// The test currently running.
+var gCurrentTest = null;
+
+function test() {
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ sinon.restore();
+ });
+
+ gTestWin = openDialog(location, "", "chrome,all,dialog=no", "about:blank");
+ whenDelayedStartupFinished(gTestWin, function () {
+ info("Browser window opened");
+ waitForFocus(function () {
+ info("Browser window focused");
+ waitForFocus(
+ function () {
+ info("Setting up browser...");
+ setupTestBrowserWindow();
+ info("Running tests...");
+ executeSoon(runNextTest);
+ },
+ gTestWin.content,
+ true
+ );
+ }, gTestWin);
+ });
+}
+
+// Click handler used to steal click events.
+var gClickHandler = {
+ handleEvent(event) {
+ if (event.type == "click" && event.button != 0) {
+ return;
+ }
+ let linkId = event.target.id || event.target.localName;
+ let wantedEvent = gCurrentTest.wantedEvent || "click";
+ is(
+ event.type,
+ wantedEvent,
+ `${gCurrentTest.desc}:Handler received a ${wantedEvent} event on ${linkId}`
+ );
+
+ let isPanelClick = linkId == "panellink";
+ gTestWin.contentAreaClick(event, isPanelClick);
+ let prevent = event.defaultPrevented;
+ is(
+ prevent,
+ gCurrentTest.preventDefault,
+ gCurrentTest.desc +
+ ": event.defaultPrevented is correct (" +
+ prevent +
+ ")"
+ );
+
+ // Check that all required methods have been called.
+ for (let expectedMethod of gCurrentTest.expectedInvokedMethods) {
+ ok(
+ getStub(expectedMethod).called,
+ `${gCurrentTest.desc}:${expectedMethod} should have been invoked`
+ );
+ }
+
+ for (let method of gReplacedMethods) {
+ if (
+ getStub(method).called &&
+ !gCurrentTest.expectedInvokedMethods.includes(method)
+ ) {
+ ok(false, `Should have not called ${method}`);
+ }
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ executeSoon(runNextTest);
+ },
+};
+
+function setupTestBrowserWindow() {
+ // Steal click events and don't propagate them.
+ gTestWin.addEventListener("click", gClickHandler, true);
+ gTestWin.addEventListener("auxclick", gClickHandler, true);
+
+ // Replace methods.
+ gReplacedMethods.forEach(function (methodName) {
+ let targetObj =
+ methodName == "getShortcutOrURIAndPostData" ? UrlbarUtils : gTestWin;
+ sinon.stub(targetObj, methodName).returnsArg(0);
+ });
+
+ // Inject links in content.
+ let doc = gTestWin.content.document;
+ let mainDiv = doc.createElement("div");
+ mainDiv.innerHTML =
+ '<p><a id="commonlink" href="http://mochi.test/moz/">Common link</a></p>' +
+ '<p><a id="panellink" href="http://mochi.test/moz/">Panel link</a></p>' +
+ '<p><a id="emptylink">Empty 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>' +
+ '<p><map name="map" id="map"><area href="http://mochi.test/moz/" shape="rect" coords="0,0,128,128" /></map><img id="maplink" usemap="#map" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAABGdBTUEAALGPC%2FxhBQAAAOtJREFUeF7t0IEAAAAAgKD9qRcphAoDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGBgwIAAAT0N51AAAAAASUVORK5CYII%3D"/></p>';
+ doc.body.appendChild(mainDiv);
+}
+
+function runNextTest() {
+ if (!gCurrentTest) {
+ gCurrentTest = gTests.shift();
+ gCurrentTest.setup();
+ }
+
+ if (!gCurrentTest.targets.length) {
+ info(gCurrentTest.desc + ": cleaning up...");
+ gCurrentTest.clean();
+
+ if (gTests.length) {
+ gCurrentTest = gTests.shift();
+ gCurrentTest.setup();
+ } else {
+ finishTest();
+ return;
+ }
+ }
+
+ // Move to next target.
+ sinon.resetHistory();
+ let target = gCurrentTest.targets.shift();
+
+ info(gCurrentTest.desc + ": testing " + target);
+
+ // Fire (aux)click event.
+ let targetElt = gTestWin.content.document.getElementById(target);
+ ok(targetElt, gCurrentTest.desc + ": target is valid (" + targetElt.id + ")");
+ EventUtils.synthesizeMouseAtCenter(
+ targetElt,
+ gCurrentTest.event,
+ gTestWin.content
+ );
+}
+
+function finishTest() {
+ info("Restoring browser...");
+ gTestWin.removeEventListener("click", gClickHandler, true);
+ gTestWin.removeEventListener("auxclick", gClickHandler, true);
+ gTestWin.close();
+ finish();
+}
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..490b2aea10
--- /dev/null
+++ b/browser/base/content/test/general/browser_datachoices_notification.js
@@ -0,0 +1,293 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"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 = Promise.withResolvers();
+ 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 = Promise.withResolvers();
+ 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];
+
+ let openPrefsPromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+
+ // Click on the button.
+ button.click();
+
+ // Wait for the preferences panel to open.
+ await openPrefsPromise;
+};
+
+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;
+ await promiseNextTick();
+
+ 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..880db6110f
--- /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.startLoadingURIString(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..a8dcf9d995
--- /dev/null
+++ b/browser/base/content/test/general/browser_drag.js
@@ -0,0 +1,58 @@
+async function test() {
+ waitForExplicitFinish();
+
+ // ---- 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
+ );
+ Assert.strictEqual(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..996f01e0b9
--- /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.startLoadingURIString(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..90978a547e
--- /dev/null
+++ b/browser/base/content/test/general/browser_gestureSupport.js
@@ -0,0 +1,1150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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) =>
+ Assert.equal(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
+ );
+ Assert.equal(cmdInc.callCount, 0, "Increasing command was triggered");
+ Assert.equal(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
+ );
+ Assert.equal(cmdInc.callCount, 1, "Increasing command was not triggered");
+ Assert.equal(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
+ );
+ Assert.equal(cmdInc.callCount, 0, "Increasing command was triggered");
+ Assert.equal(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
+ );
+ Assert.equal(cmdInc.callCount, 0, "Increasing command was triggered");
+ Assert.equal(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
+ );
+ Assert.equal(cmdInc.callCount, 0, "Increasing command was triggered");
+ Assert.equal(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
+ );
+ Assert.equal(cmdUp.callCount, 1, "Step 1: Up command was not triggered");
+ Assert.equal(cmdDown.callCount, 0, "Step 1: Down command was triggered");
+ Assert.equal(cmdLeft.callCount, 0, "Step 1: Left command was triggered");
+ Assert.equal(cmdRight.callCount, 0, "Step 1: Right command was triggered");
+
+ // DOWN
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ down,
+ 0,
+ 0,
+ 0
+ );
+ Assert.equal(cmdUp.callCount, 0, "Step 2: Up command was triggered");
+ Assert.equal(cmdDown.callCount, 1, "Step 2: Down command was not triggered");
+ Assert.equal(cmdLeft.callCount, 0, "Step 2: Left command was triggered");
+ Assert.equal(cmdRight.callCount, 0, "Step 2: Right command was triggered");
+
+ // LEFT
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ left,
+ 0,
+ 0,
+ 0
+ );
+ Assert.equal(cmdUp.callCount, 0, "Step 3: Up command was triggered");
+ Assert.equal(cmdDown.callCount, 0, "Step 3: Down command was triggered");
+ Assert.equal(cmdLeft.callCount, 1, "Step 3: Left command was not triggered");
+ Assert.equal(cmdRight.callCount, 0, "Step 3: Right command was triggered");
+
+ // RIGHT
+ resetCounts();
+ await synthesizeSimpleGestureEvent(
+ test_normalTab.linkedBrowser,
+ "MozSwipeGesture",
+ 10,
+ 10,
+ right,
+ 0,
+ 0,
+ 0
+ );
+ Assert.equal(cmdUp.callCount, 0, "Step 4: Up command was triggered");
+ Assert.equal(cmdDown.callCount, 0, "Step 4: Down command was triggered");
+ Assert.equal(cmdLeft.callCount, 0, "Step 4: Left command was triggered");
+ Assert.equal(
+ 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
+ );
+ Assert.equal(
+ cmdUp.callCount,
+ 0,
+ "Step 5-" + i + ": Up command was triggered"
+ );
+ Assert.equal(
+ cmdDown.callCount,
+ 0,
+ "Step 5-" + i + ": Down command was triggered"
+ );
+ Assert.equal(
+ cmdLeft.callCount,
+ 0,
+ "Step 5-" + i + ": Left command was triggered"
+ );
+ Assert.equal(
+ 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..4362f3456e
--- /dev/null
+++ b/browser/base/content/test/general/browser_homeDrop.js
@@ -0,0 +1,111 @@
+/* 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"]);
+
+ // 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..95b7898369
--- /dev/null
+++ b/browser/base/content/test/general/browser_lastAccessedTab.js
@@ -0,0 +1,71 @@
+/* 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);
+ Assert.lessOrEqual(
+ 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() {
+ Assert.less(
+ 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() {
+ Assert.less(
+ 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..ea4bb569a4
--- /dev/null
+++ b/browser/base/content/test/general/browser_newTabDrop.js
@@ -0,0 +1,218 @@
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+
+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("example.com/first", ["http://example.com/first"]);
+});
+add_task(async function single_url2() {
+ await dropText("example.com/second", ["http://example.com/second"]);
+});
+add_task(async function single_url3() {
+ await dropText("example.com/third", ["http://example.com/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("www.example.com/1\nexample.com/2", [
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.example.com/1",
+ "http://example.com/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: "example.com/5" }],
+ [{ type: "text/plain", data: "example.com/6\nexample.com/7" }],
+ ],
+ ["http://example.com/5", "http://example.com/6", "http://example.com/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: "example.com/8\nTITLE8\nexample.com/9\nTITLE9",
+ },
+ ],
+ ],
+ ["http://example.com/8", "http://example.com/9"]
+ );
+});
+
+// Single item with multiple types.
+add_task(async function single_item_multiple_types() {
+ await drop(
+ [
+ [
+ { type: "text/plain", data: "example.com/10" },
+ { type: "text/x-moz-url", data: "example.com/11\nTITLE11" },
+ ],
+ ],
+ ["http://example.com/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("example.com/multi" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://example.com/multi0",
+ "http://example.com/multi1",
+ "http://example.com/multi2",
+ "http://example.com/multi3",
+ "http://example.com/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("example.com/accept" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://example.com/accept0",
+ "http://example.com/accept1",
+ "http://example.com/accept2",
+ "http://example.com/accept3",
+ "http://example.com/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("example.com/cancel" + i);
+ }
+ await dropText(urls.join("\n"), []);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+
+// Open URLs ignoring non-URL.
+add_task(async function multiple_urls() {
+ await dropText(
+ `
+ example.com/urls0
+ example.com/urls1
+ example.com/urls2
+ non url0
+ example.com/urls3
+ non url1
+ non url2
+`,
+ [
+ "http://example.com/urls0",
+ "http://example.com/urls1",
+ "http://example.com/urls2",
+ "http://example.com/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}`
+ );
+
+ // 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, true, 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..3e41b0d6ac
--- /dev/null
+++ b/browser/base/content/test/general/browser_newWindowDrop.js
@@ -0,0 +1,225 @@
+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}`
+ );
+
+ // 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..332d7fa029
--- /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.isVisible(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..637f47a545
--- /dev/null
+++ b/browser/base/content/test/general/browser_refreshBlocker.js
@@ -0,0 +1,211 @@
+"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.startLoadingURIString(
+ 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.startLoadingURIString(
+ 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 [redirectLabel, refreshLabel] = await document.l10n.formatValues([
+ { id: "refresh-blocked-redirect-label" },
+ { id: "refresh-blocked-refresh-label" },
+ ]);
+
+ is(
+ notification.messageText.textContent.trim(),
+ redirectLabel,
+ "Should be showing the redirect message"
+ );
+
+ // Next, attempt a refresh
+ await attemptFakeRefresh(browser, false);
+
+ is(
+ notification.messageText.textContent.trim(),
+ refreshLabel,
+ "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..17b0eb7cbe
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+ );
+ 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..d46f53db7d
--- /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.startLoadingURIString(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.startLoadingURIString(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..b018212280
--- /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.startLoadingURIString(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..e7507fcbb0
--- /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.startLoadingURIString(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..276088fbb1
--- /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.startLoadingURIString(
+ 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..92c1dcdb8b
--- /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.startLoadingURIString(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..88c4a175b1
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabDrop.js
@@ -0,0 +1,204 @@
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+
+// 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("example.com/first", ["http://example.com/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("example.com/second", ["http://example.com/second"]);
+});
+add_task(async function single_data_url() {
+ await dropText("data:text/html,bad", []);
+});
+add_task(async function single_url3() {
+ await dropText("example.com/third", ["http://example.com/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("example.com/1\nexample.com/2", [
+ "http://example.com/1",
+ "http://example.com/2",
+ ]);
+});
+add_task(async function multiple_urls_javascript() {
+ await dropText("javascript:'bad1'\nexample.com/3", []);
+});
+add_task(async function multiple_urls_data() {
+ await dropText("example.com/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: "example.com/5" }],
+ [{ type: "text/plain", data: "example.com/6\nexample.com/7" }],
+ ],
+ ["http://example.com/5", "http://example.com/6", "http://example.com/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: "example.com/8\nTITLE8\nexample.com/9\nTITLE9",
+ },
+ ],
+ ],
+ ["http://example.com/8", "http://example.com/9"]
+ );
+});
+
+// Single item with multiple types.
+add_task(async function single_item_multiple_types() {
+ await drop(
+ [
+ [
+ { type: "text/plain", data: "example.com/10" },
+ { type: "text/x-moz-url", data: "example.com/11\nTITLE11" },
+ ],
+ ],
+ ["http://example.com/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("example.com/multi" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://example.com/multi0",
+ "http://example.com/multi1",
+ "http://example.com/multi2",
+ "http://example.com/multi3",
+ "http://example.com/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("example.com/accept" + i);
+ }
+ await dropText(urls.join("\n"), [
+ "http://example.com/accept0",
+ "http://example.com/accept1",
+ "http://example.com/accept2",
+ "http://example.com/accept3",
+ "http://example.com/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("example.com/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 awaitDrop = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "drop");
+
+ let loadedPromises = expectedURLs.map(url =>
+ BrowserTestUtils.waitForNewTab(gBrowser, url, true, 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..405e4509b4
--- /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.startLoadingURIString(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..01519a6142
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
@@ -0,0 +1,417 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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);
+
+/**
+ * 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..33b0a5c238
--- /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.startLoadingURIString(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_tabkeynavigation.js b/browser/base/content/test/general/browser_tabkeynavigation.js
new file mode 100644
index 0000000000..765bf5c21d
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabkeynavigation.js
@@ -0,0 +1,223 @@
+/*
+ * This test checks that keyboard navigation for tabs isn't blocked by content
+ */
+add_task(async function test() {
+ let testPage1 =
+ "data:text/html,<html id='tab1'><body><button id='button1'>Tab 1</button></body></html>";
+ let testPage2 =
+ "data:text/html,<html id='tab2'><body><button id='button2'>Tab 2</button><script>function preventDefault(event) { event.preventDefault(); event.stopImmediatePropagation(); } window.addEventListener('keydown', preventDefault, true); window.addEventListener('keypress', preventDefault, true);</script></body></html>";
+ let testPage3 =
+ "data:text/html,<html id='tab3'><body><button id='button3'>Tab 3</button></body></html>";
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage1);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage2);
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage3);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.ctrlTab.sortByRecentlyUsed", false]],
+ });
+
+ // Disable tab animations
+ gReduceMotionOverride = true;
+
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ is(gBrowser.selectedTab, tab1, "Tab1 should be activated");
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+Tab on Tab1"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab3,
+ "Tab3 should be activated by pressing Ctrl+Tab on Tab2"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+Shift+Tab on Tab3"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab1,
+ "Tab1 should be activated by pressing Ctrl+Shift+Tab on Tab2"
+ );
+
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ is(gBrowser.selectedTab, tab1, "Tab1 should be activated");
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+PageDown on Tab1"
+ );
+
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab3,
+ "Tab3 should be activated by pressing Ctrl+PageDown on Tab2"
+ );
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+PageUp on Tab3"
+ );
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab1,
+ "Tab1 should be activated by pressing Ctrl+PageUp on Tab2"
+ );
+
+ if (gBrowser.tabbox._handleMetaAltArrows) {
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ let ltr = window.getComputedStyle(gBrowser.tabbox).direction == "ltr";
+ let advanceKey = ltr ? "VK_RIGHT" : "VK_LEFT";
+ let reverseKey = ltr ? "VK_LEFT" : "VK_RIGHT";
+
+ is(gBrowser.selectedTab, tab1, "Tab1 should be activated");
+ EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+" + advanceKey + " on Tab1"
+ );
+
+ EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab3,
+ "Tab3 should be activated by pressing Ctrl+" + advanceKey + " on Tab2"
+ );
+
+ EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+" + reverseKey + " on Tab3"
+ );
+
+ EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab1,
+ "Tab1 should be activated by pressing Ctrl+" + reverseKey + " on Tab2"
+ );
+ }
+
+ gBrowser.selectedTab = tab2;
+ is(gBrowser.selectedTab, tab2, "Tab2 should be activated");
+ is(gBrowser.tabContainer.selectedIndex, 2, "Tab2 index should be 2");
+
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true, shiftKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated after Ctrl+Shift+PageDown"
+ );
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ 3,
+ "Tab2 index should be 1 after Ctrl+Shift+PageDown"
+ );
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true, shiftKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated after Ctrl+Shift+PageUp"
+ );
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ 2,
+ "Tab2 index should be 2 after Ctrl+Shift+PageUp"
+ );
+
+ if (navigator.platform.indexOf("Mac") == 0) {
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ // XXX Currently, Command + "{" and "}" don't work if keydown event is
+ // consumed because following keypress event isn't fired.
+
+ let ltr = window.getComputedStyle(gBrowser.tabbox).direction == "ltr";
+ let advanceKey = ltr ? "}" : "{";
+ let reverseKey = ltr ? "{" : "}";
+
+ is(gBrowser.selectedTab, tab1, "Tab1 should be activated");
+
+ EventUtils.synthesizeKey(advanceKey, { metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+" + advanceKey + " on Tab1"
+ );
+
+ EventUtils.synthesizeKey(advanceKey, { metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab3,
+ "Tab3 should be activated by pressing Ctrl+" + advanceKey + " on Tab2"
+ );
+
+ EventUtils.synthesizeKey(reverseKey, { metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be activated by pressing Ctrl+" + reverseKey + " on Tab3"
+ );
+
+ EventUtils.synthesizeKey(reverseKey, { metaKey: true });
+ is(
+ gBrowser.selectedTab,
+ tab1,
+ "Tab1 should be activated by pressing Ctrl+" + reverseKey + " on Tab2"
+ );
+ } else {
+ gBrowser.selectedTab = tab2;
+ EventUtils.synthesizeKey("VK_F4", { type: "keydown", ctrlKey: true });
+
+ isnot(
+ gBrowser.selectedTab,
+ tab2,
+ "Tab2 should be closed by pressing Ctrl+F4 on Tab2"
+ );
+ is(
+ gBrowser.tabs.length,
+ 3,
+ "The count of tabs should be 3 since tab2 should be closed"
+ );
+
+ // NOTE: keypress event shouldn't be fired since the keydown event should
+ // be consumed by tab2.
+ EventUtils.synthesizeKey("VK_F4", { type: "keyup", ctrlKey: true });
+ is(
+ gBrowser.tabs.length,
+ 3,
+ "The count of tabs should be 3 since renaming key events shouldn't close other tabs"
+ );
+ }
+
+ gBrowser.selectedTab = tab3;
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
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..49f8629a25
--- /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.startLoadingURIString(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..eee3775249
--- /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.startLoadingURIString(
+ 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..ea0b99ebf1
--- /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.startLoadingURIString(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.startLoadingURIString(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..f7b4a0d93b
--- /dev/null
+++ b/browser/base/content/test/general/head.js
@@ -0,0 +1,339 @@
+ChromeUtils.defineESModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
+});
+
+/**
+ * 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.startLoadingURIString(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.isVisible(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.startLoadingURIString(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..6fb26605fc
--- /dev/null
+++ b/browser/base/content/test/general/refresh_header.sjs
@@ -0,0 +1,23 @@
+/**
+ * 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) {
+ 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..02beb17ac2
--- /dev/null
+++ b/browser/base/content/test/general/refresh_meta.sjs
@@ -0,0 +1,35 @@
+/**
+ * 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) {
+ 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.toml b/browser/base/content/test/gesture/browser.toml
new file mode 100644
index 0000000000..c438a5ff78
--- /dev/null
+++ b/browser/base/content/test/gesture/browser.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["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..78da1373b8
--- /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.startLoadingURIString(
+ newWindow.gBrowser.selectedBrowser,
+ "about:mozilla"
+ );
+ await BrowserTestUtils.browserLoaded(
+ newWindow.gBrowser.selectedBrowser,
+ false,
+ "about:mozilla"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ 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.startLoadingURIString(
+ newWindow.gBrowser.selectedBrowser,
+ "about:mozilla"
+ );
+ await BrowserTestUtils.browserLoaded(
+ newWindow.gBrowser.selectedBrowser,
+ false,
+ "about:mozilla"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ 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.toml b/browser/base/content/test/historySwipeAnimation/browser.toml
new file mode 100644
index 0000000000..5e9dff0b07
--- /dev/null
+++ b/browser/base/content/test/historySwipeAnimation/browser.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["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.toml b/browser/base/content/test/keyboard/browser.toml
new file mode 100644
index 0000000000..770e1bb39f
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser.toml
@@ -0,0 +1,22 @@
+[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' && (asan || tsan || debug)", # Bug 1775712
+ "os == 'mac' && debug", # Bug 1775712
+ "os == 'win'", # Bug 1775712
+]
+
+["browser_toolbarKeyNav.js"]
+support-files = ["!/browser/base/content/test/permissions/permissions.html"]
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..2b381dda4b
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_popup_keyNav.js
@@ -0,0 +1,51 @@
+/* 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");
+ await focusAndActivateElement(hamburgerButton, () =>
+ 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..8640716bab
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js
@@ -0,0 +1,334 @@
+/* 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");
+ let focused = BrowserTestUtils.waitForEvent(
+ window.PanelUI.mainView,
+ "focus",
+ true
+ );
+ await focusAndActivateElement(button, () => 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");
+ await focusAndActivateElement(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");
+ await focusAndActivateElement(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");
+ await focusAndActivateElement(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");
+ await focusAndActivateElement(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");
+ await focusAndActivateElement(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.startLoadingURIString(aBrowser, "https://example.com/2");
+
+ await BrowserTestUtils.browserLoaded(aBrowser);
+ let backButton = document.getElementById("back-button");
+ let onLocationChange = waitForLocationChange();
+ await focusAndActivateElement(backButton, () =>
+ EventUtils.synthesizeKey(" ")
+ );
+ await onLocationChange;
+ ok(true, "Location changed after back button pressed");
+
+ let forwardButton = document.getElementById("forward-button");
+ onLocationChange = waitForLocationChange();
+ await focusAndActivateElement(forwardButton, () =>
+ 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) {
+ info("Waiting for button to be enabled.");
+ let button = document.getElementById("reload-button");
+ await TestUtils.waitForCondition(() => !button.disabled);
+ let loaded = BrowserTestUtils.browserLoaded(aBrowser);
+ info("Focusing button");
+ await focusAndActivateElement(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");
+ await focusAndActivateElement(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);
+ await focusAndActivateElement(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");
+ 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
+ );
+ await focusAndActivateElement(button, () =>
+ 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");
+ let menu = document.getElementById("BMB_bookmarksPopup");
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ await focusAndActivateElement(button, () => 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");
+ let view = document.getElementById("widget-overflow-mainView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focusAndActivateElement(button, () => 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");
+ let panel = document.getElementById("downloadsPanel");
+ let focused = BrowserTestUtils.waitForEvent(panel, "focus", true);
+ await focusAndActivateElement(button, () => 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");
+ // The panel is created on the fly, so we can't simply wait for focus
+ // inside it.
+ let showing = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshowing",
+ true
+ );
+ await focusAndActivateElement(button, () =>
+ 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..6cd2eee35c
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_toolbarKeyNav.js
@@ -0,0 +1,643 @@
+/* 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 - 3
+ );
+ CustomizableUI.addWidgetToArea(
+ "sidebar-button",
+ "nav-bar",
+ CustomizableUI.getWidgetIdsInArea("nav-bar").length - 3
+ );
+ CustomizableUI.addWidgetToArea(
+ "unified-extensions-button",
+ "nav-bar",
+ CustomizableUI.getWidgetIdsInArea("nav-bar").length - 3
+ );
+}
+
+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}y/` };
+ }
+ 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.startLoadingURIString(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");
+ await focusAndActivateElement(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;
+ }
+ await focusAndActivateElement(lastVisible, () =>
+ 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");
+ let view = document.getElementById("appMenu-libraryView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focusAndActivateElement(button, () => 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 expectFocusAfterKey("ArrowLeft", afterUrlBarButton);
+ await expectFocusAfterKey("Shift+Tab", "searchbar", true);
+ await expectFocusAfterKey("Shift+Tab", gURLBar.inputField);
+ });
+ 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..4f87109b32
--- /dev/null
+++ b/browser/base/content/test/keyboard/head.js
@@ -0,0 +1,61 @@
+/* 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.
+ * It then runs the `activateMethod` passed in, and restores usual focus state
+ * afterwards. `activateMethod` can be async.
+ */
+async function focusAndActivateElement(elem, activateMethod) {
+ elem.setAttribute("tabindex", "-1");
+ elem.focus();
+ try {
+ await activateMethod(elem);
+ } finally {
+ elem.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.toml b/browser/base/content/test/menubar/browser.toml
new file mode 100644
index 0000000000..4ba2101891
--- /dev/null
+++ b/browser/base/content/test/menubar/browser.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+prefs = [
+ "browser.sessionstore.closedTabsFromAllWindows=true",
+ "browser.sessionstore.closedTabsFromClosedWindows=true",
+]
+
+["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
+
+["browser_history_recently_closed_tabs.js"]
+skip-if = ["os == 'mac'"] # No menubar on macOS.
+
+support-files = ["file_shareurl.html"]
+
+["browser_search_bookmarks.js"]
+
+["browser_search_history.js"]
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..9fb969b777
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_file_menu_import_wizard.js
@@ -0,0 +1,23 @@
+/* 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 when the
+ // about:preferences hosting the migration wizard opens, we'll load
+ // the about:preferences page in a new tab rather than overtaking the
+ // initial one. This makes it easier to be more explicit when cleaning
+ // up because we can just remove the opened tab.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, "https://example.com");
+ await BrowserTestUtils.browserLoaded(browser);
+});
+
+add_task(async function file_menu_import_wizard() {
+ let wizardTabPromise = BrowserTestUtils.waitForMigrationWizard(window);
+ document.getElementById("menu_importFromAnotherBrowser").doCommand();
+ let wizardTab = await wizardTabPromise;
+ ok(wizardTab, "Migration wizard tab opened");
+ BrowserTestUtils.removeTab(wizardTab);
+});
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/browser_history_recently_closed_tabs.js b/browser/base/content/test/menubar/browser_history_recently_closed_tabs.js
new file mode 100644
index 0000000000..246dce4db0
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_history_recently_closed_tabs.js
@@ -0,0 +1,397 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/**
+ * This test verifies behavior from bug 1819675:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1819675
+ *
+ * The recently closed tabs menu item should be enabled when there are tabs
+ * closed from any window that is in the same private/non-private bucket as
+ * the current window.
+ */
+
+const { SessionStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SessionStoreTestUtils.sys.mjs"
+);
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+SessionStoreTestUtils.init(this, window);
+
+async function checkMenu(window, expected) {
+ await SimpleTest.promiseFocus(window);
+ const historyMenubarItem = window.document.getElementById("history-menu");
+ const historyMenu = window.document.getElementById("historyMenuPopup");
+ const recentlyClosedTabsItem = historyMenu.querySelector("#historyUndoMenu");
+
+ const menuShown = BrowserTestUtils.waitForEvent(historyMenu, "popupshown");
+ historyMenubarItem.openMenu(true);
+ info("checkMenu:, waiting for menuShown");
+ await menuShown;
+
+ Assert.equal(
+ recentlyClosedTabsItem.disabled,
+ expected.menuItemDisabled,
+ `Recently closed tabs menu item is ${
+ expected.menuItemDisabled ? "disabled" : "not disabled"
+ }`
+ );
+ const menuHidden = BrowserTestUtils.waitForEvent(historyMenu, "popuphidden");
+ historyMenu.hidePopup();
+ info("checkMenu:, waiting for menuHidden");
+ await menuHidden;
+ info("checkMenu:, menuHidden, returning");
+}
+
+function resetClosedTabsAndWindows() {
+ // Clear the lists of closed windows and tabs.
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(SessionStore.getClosedWindowCount(), 0, "Expect 0 closed windows");
+ for (const win of BrowserWindowTracker.orderedWindows) {
+ is(
+ SessionStore.getClosedTabCountForWindow(win),
+ 0,
+ "Expect 0 closed tabs for this window"
+ );
+ }
+}
+
+add_task(async function test_recently_closed_tabs_nonprivate() {
+ await resetClosedTabsAndWindows();
+
+ const win1 = window;
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(
+ win1.gBrowser,
+ "https://example.com"
+ );
+ // we're going to close a tab and don't want to accidentally close the window when it has 0 tabs
+ await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, "about:about");
+ await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ "https://example.org"
+ );
+
+ info("Checking the menuitem is initially disabled in both windows");
+ for (let win of [win1, win2]) {
+ await checkMenu(win, {
+ menuItemDisabled: true,
+ });
+ }
+
+ await SessionStoreTestUtils.closeTab(win2.gBrowser.selectedTab);
+ is(
+ SessionStore.getClosedTabCount(),
+ 1,
+ "Expect closed tab count of 1 after closing a tab"
+ );
+
+ for (let win of [win1, win2]) {
+ await checkMenu(win, {
+ menuItemDisabled: false,
+ });
+ }
+
+ // clean up
+ info("clean up opened window");
+ const sessionStoreChanged = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ await BrowserTestUtils.closeWindow(win2);
+ await sessionStoreChanged;
+
+ info("starting tab cleanup");
+ while (gBrowser.tabs.length > 1) {
+ await SessionStoreTestUtils.closeTab(
+ gBrowser.tabs[gBrowser.tabs.length - 1]
+ );
+ }
+ info("finished tab cleanup");
+});
+
+add_task(async function test_recently_closed_tabs_nonprivate_pref_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.closedTabsFromAllWindows", false]],
+ });
+ await resetClosedTabsAndWindows();
+
+ const win1 = window;
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(
+ win1.gBrowser,
+ "https://example.com"
+ );
+ // we're going to close a tab and don't want to accidentally close the window when it has 0 tabs
+ await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, "about:about");
+ await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ "https://example.org"
+ );
+
+ info("Checking the menuitem is initially disabled in both windows");
+ for (let win of [win1, win2]) {
+ await checkMenu(win, {
+ menuItemDisabled: true,
+ });
+ }
+ await SimpleTest.promiseFocus(win2);
+ await SessionStoreTestUtils.closeTab(win2.gBrowser.selectedTab);
+ is(
+ SessionStore.getClosedTabCount(),
+ 1,
+ "Expect closed tab count of 1 after closing a tab"
+ );
+
+ await checkMenu(win1, {
+ menuItemDisabled: true,
+ });
+ await checkMenu(win2, {
+ menuItemDisabled: false,
+ });
+
+ // clean up
+ info("clean up opened window");
+ const sessionStoreChanged = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ await BrowserTestUtils.closeWindow(win2);
+ await sessionStoreChanged;
+
+ info("starting tab cleanup");
+ while (gBrowser.tabs.length > 1) {
+ await SessionStoreTestUtils.closeTab(
+ gBrowser.tabs[gBrowser.tabs.length - 1]
+ );
+ }
+ info("finished tab cleanup");
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_recently_closed_tabs_mixed_private() {
+ await resetClosedTabsAndWindows();
+ is(
+ SessionStore.getClosedTabCount(),
+ 0,
+ "Expect closed tab count of 0 after reset"
+ );
+
+ await BrowserTestUtils.openNewForegroundTab(window.gBrowser, "about:robots");
+ await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.com"
+ );
+
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "about:about"
+ );
+ await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "https://example.org"
+ );
+
+ for (let win of [window, privateWin]) {
+ await checkMenu(win, {
+ menuItemDisabled: true,
+ });
+ }
+
+ await SessionStoreTestUtils.closeTab(privateWin.gBrowser.selectedTab);
+ is(
+ SessionStore.getClosedTabCount(privateWin),
+ 1,
+ "Expect closed tab count of 1 for private windows"
+ );
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Expect closed tab count of 0 for non-private windows"
+ );
+
+ // the menu should be enabled only for the private window
+ await checkMenu(window, {
+ menuItemDisabled: true,
+ });
+ await checkMenu(privateWin, {
+ menuItemDisabled: false,
+ });
+
+ await resetClosedTabsAndWindows();
+ await SimpleTest.promiseFocus(window);
+
+ info("closing tab in non-private window");
+ await SessionStoreTestUtils.closeTab(window.gBrowser.selectedTab);
+ is(
+ SessionStore.getClosedTabCount(window),
+ 1,
+ "Expect 1 closed tab count after closing the a tab in the non-private window"
+ );
+
+ // the menu should be enabled only for the non-private window
+ await checkMenu(window, {
+ menuItemDisabled: false,
+ });
+ await checkMenu(privateWin, {
+ menuItemDisabled: true,
+ });
+
+ // clean up
+ info("closing private window");
+ await BrowserTestUtils.closeWindow(privateWin);
+ await TestUtils.waitForTick();
+
+ info("starting tab cleanup");
+ while (gBrowser.tabs.length > 1) {
+ await SessionStoreTestUtils.closeTab(
+ gBrowser.tabs[gBrowser.tabs.length - 1]
+ );
+ }
+ info("finished tab cleanup");
+});
+
+add_task(async function test_recently_closed_tabs_mixed_private_pref_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.closedTabsFromAllWindows", false]],
+ });
+ await resetClosedTabsAndWindows();
+
+ await BrowserTestUtils.openNewForegroundTab(window.gBrowser, "about:robots");
+ await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.com"
+ );
+
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "about:about"
+ );
+ await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "https://example.org"
+ );
+
+ for (let win of [window, privateWin]) {
+ await checkMenu(win, {
+ menuItemDisabled: true,
+ });
+ }
+
+ await SimpleTest.promiseFocus(privateWin);
+ await SessionStoreTestUtils.closeTab(privateWin.gBrowser.selectedTab);
+ is(
+ SessionStore.getClosedTabCount(privateWin),
+ 1,
+ "Expect closed tab count of 1 for private windows"
+ );
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Expect closed tab count of 0 for non-private windows"
+ );
+
+ // the menu should be enabled only for the private window
+ await checkMenu(window, {
+ menuItemDisabled: true,
+ });
+ await checkMenu(privateWin, {
+ menuItemDisabled: false,
+ });
+
+ await resetClosedTabsAndWindows();
+ is(
+ SessionStore.getClosedTabCount(privateWin),
+ 0,
+ "Expect 0 closed tab count after reset"
+ );
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Expect 0 closed tab count after reset"
+ );
+
+ info("closing tab in non-private window");
+ await SimpleTest.promiseFocus(window);
+ await SessionStoreTestUtils.closeTab(window.gBrowser.selectedTab);
+
+ // the menu should be enabled only for the non-private window
+ await checkMenu(window, {
+ menuItemDisabled: false,
+ });
+ await checkMenu(privateWin, {
+ menuItemDisabled: true,
+ });
+
+ // clean up
+ info("closing private window");
+ await BrowserTestUtils.closeWindow(privateWin);
+ await TestUtils.waitForTick();
+
+ info("starting tab cleanup");
+ while (gBrowser.tabs.length > 1) {
+ await SessionStoreTestUtils.closeTab(
+ gBrowser.tabs[gBrowser.tabs.length - 1]
+ );
+ }
+ info("finished tab cleanup");
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_recently_closed_tabs_closed_windows() {
+ // prepare window state with closed tabs from closed windows
+ await SpecialPowers.pushPrefEnv({
+ set: [["sessionstore.closedTabsFromClosedWindows", true]],
+ });
+ const closedTabUrls = ["about:robots"];
+ const closedWindowState = {
+ tabs: [
+ {
+ entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ },
+ ],
+ _closedTabs: closedTabUrls.map(url => {
+ return {
+ state: {
+ entries: [
+ {
+ url,
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ };
+ }),
+ };
+ await SessionStoreTestUtils.promiseBrowserState({
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ },
+ ],
+ },
+ ],
+ _closedWindows: [closedWindowState],
+ });
+
+ // verify the recently-closed-tabs menu item is enabled
+ await checkMenu(window, {
+ menuItemDisabled: false,
+ });
+
+ // flip the pref
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.closedTabsFromClosedWindows", false]],
+ });
+
+ // verify the recently-closed-tabs menu item is disabled
+ await checkMenu(window, {
+ menuItemDisabled: true,
+ });
+
+ SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/menubar/browser_search_bookmarks.js b/browser/base/content/test/menubar/browser_search_bookmarks.js
new file mode 100644
index 0000000000..1c0da6071e
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_search_bookmarks.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the Search Bookmarks option from the menubar starts Address Bar search
+ * mode for bookmarks.
+ */
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+add_task(async function test_menu_search_bookmarks_with_window_open() {
+ info("Opening bookmarks menu");
+ let searchBookmarksMenuEntry = document.getElementById(
+ "menu_searchBookmarks"
+ );
+
+ searchBookmarksMenuEntry.doCommand();
+
+ await isUrlbarInBookmarksSearchMode(window);
+});
+
+add_task(async function test_menu_search_bookmarks_opens_new_window() {
+ let newWindowPromise = TestUtils.topicObserved(
+ "browser-delayed-startup-finished"
+ );
+
+ info(
+ "Executing command in untracked browser window (simulating non-browser window)."
+ );
+ BrowserWindowTracker.untrackForTestsOnly(window);
+ let searchBookmarksMenuEntry = document.getElementById(
+ "menu_searchBookmarks"
+ );
+ searchBookmarksMenuEntry.doCommand();
+ BrowserWindowTracker.track(window);
+
+ info("Waiting for new window to open.");
+ let [newWindow] = await newWindowPromise;
+ await isUrlbarInBookmarksSearchMode(newWindow);
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+async function isUrlbarInBookmarksSearchMode(targetWin) {
+ is(
+ targetWin,
+ BrowserWindowTracker.getTopWindow(),
+ "Target window is top window."
+ );
+ await UrlbarTestUtils.promisePopupOpen(targetWin, () => {});
+
+ // Verify URLBar is in search mode with correct restriction
+ let searchMode = UrlbarUtils.searchModeForToken("*");
+ searchMode.entry = "bookmarkmenu";
+ await UrlbarTestUtils.assertSearchMode(targetWin, searchMode);
+}
diff --git a/browser/base/content/test/menubar/browser_search_history.js b/browser/base/content/test/menubar/browser_search_history.js
new file mode 100644
index 0000000000..06c1402362
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_search_history.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the Search History option from the menubar starts Address Bar search
+ * mode for history.
+ */
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+add_task(async function test_menu_search_history_with_window_open() {
+ info("Opening history menu");
+ let searchHistoryMenuEntry = document.getElementById("menu_searchHistory");
+
+ searchHistoryMenuEntry.doCommand();
+
+ await isUrlbarInHistorySearchMode(window);
+});
+
+add_task(async function test_menu_search_history_opens_new_window() {
+ let newWindowPromise = TestUtils.topicObserved(
+ "browser-delayed-startup-finished"
+ );
+
+ info(
+ "Executing command in untracked browser window (simulating non-browser window)."
+ );
+ BrowserWindowTracker.untrackForTestsOnly(window);
+ let searchHistoryMenuEntry = document.getElementById("menu_searchHistory");
+ searchHistoryMenuEntry.doCommand();
+ BrowserWindowTracker.track(window);
+
+ info("Waiting for new window to open.");
+ let [newWindow] = await newWindowPromise;
+ await isUrlbarInHistorySearchMode(newWindow);
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+async function isUrlbarInHistorySearchMode(targetWin) {
+ is(
+ targetWin,
+ BrowserWindowTracker.getTopWindow(),
+ "Target window is top window."
+ );
+ await UrlbarTestUtils.promisePopupOpen(targetWin, () => {});
+
+ // Verify URLBar is in search mode with correct restriction
+ let searchMode = UrlbarUtils.searchModeForToken("^");
+ searchMode.entry = "historymenu";
+ await UrlbarTestUtils.assertSearchMode(targetWin, searchMode);
+}
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.toml b/browser/base/content/test/metaTags/browser.toml
new file mode 100644
index 0000000000..4baee55b98
--- /dev/null
+++ b/browser/base/content/test/metaTags/browser.toml
@@ -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..7213de0e8e
--- /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 FaviconLoader.sys.mjs 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.toml b/browser/base/content/test/notificationbox/browser.toml
new file mode 100644
index 0000000000..9aaebc2b59
--- /dev/null
+++ b/browser/base/content/test/notificationbox/browser.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+
+["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..c775f2f9e9
--- /dev/null
+++ b/browser/base/content/test/notificationbox/browser_notificationbar_telemetry.js
@@ -0,0 +1,224 @@
+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 = await box3.appendNotification("infobar-testtwo-value", {
+ label: "Message for tab 3",
+ priority: box3.PRIORITY_INFO_HIGH,
+ telemetry: TELEMETRY_BASE + "testtwo",
+ });
+ await notif3.updateComplete;
+
+ verifyTelemetry("first notification", 0, 0, 0, 0, 0, 1);
+
+ let notif1 = await 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",
+ },
+ ]
+ );
+ await notif1.updateComplete;
+ 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 = await box1.appendNotification(
+ "infobar-testtwo-value",
+ {
+ label: "Additional message for tab 1",
+ priority: box1.PRIORITY_INFO_HIGH,
+ telemetry: TELEMETRY_BASE + "testone",
+ telemetryFilter: ["shown"],
+ },
+ [
+ {
+ label: "Button1",
+ },
+ ]
+ );
+ await notif4.updateComplete;
+ 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 = await 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",
+ },
+ ]
+ );
+ await notif5.updateComplete;
+ 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 = await 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",
+ },
+ ]
+ );
+ await notif6.updateComplete;
+ 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..9287294d7d
--- /dev/null
+++ b/browser/base/content/test/notificationbox/browser_tabnotificationbox_switch_tabs.js
@@ -0,0 +1,143 @@
+/* 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;
+ Assert.notEqual(
+ 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}`);
+}
+
+async function createNotification({ browser, label, value, priority }) {
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = await 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");
+
+ await 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");
+
+ await 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);
+
+ await 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);
+
+ await 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.toml b/browser/base/content/test/outOfProcess/browser.toml
new file mode 100644
index 0000000000..0163f6d2a7
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser.toml
@@ -0,0 +1,19 @@
+[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..5a5fd85809
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/head.js
@@ -0,0 +1,84 @@
+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) {
+ 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.toml b/browser/base/content/test/pageActions/browser.toml
new file mode 100644
index 0000000000..72e2f11216
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser.toml
@@ -0,0 +1,8 @@
+[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..4cc0519c8b
--- /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.startLoadingURIString(
+ 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.isVisible(addonButton));
+ let starButton =
+ win.BrowserPageActions.urlbarButtonNodeForActionID("bookmark");
+ Assert.ok(BrowserTestUtils.isVisible(starButton));
+ let meatballButton = win.document.getElementById("pageActionButton");
+ Assert.ok(!BrowserTestUtils.isVisible(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.isVisible(addonButton));
+ Assert.ok(!BrowserTestUtils.isVisible(starButton));
+ Assert.ok(BrowserTestUtils.isVisible(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.isVisible(starButton));
+ Assert.ok(!BrowserTestUtils.isVisible(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.startLoadingURIString(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.isVisible(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.isVisible(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..e64bffa032
--- /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.startLoadingURIString(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.isVisible(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.startLoadingURIString(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.isVisible(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..cd269bf9b5
--- /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 = Promise.withResolvers();
+ 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 = Promise.withResolvers();
+ 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.toml b/browser/base/content/test/pageStyle/browser.toml
new file mode 100644
index 0000000000..27ab96fcc2
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser.toml
@@ -0,0 +1,15 @@
+[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..8ae829817d
--- /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.docViewer.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..ac543f48f5
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser_page_style_menu.js
@@ -0,0 +1,177 @@
+/* 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.startLoadingURIString(
+ 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..e406bdcc0b
--- /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.startLoadingURIString(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.toml b/browser/base/content/test/pageinfo/browser.toml
new file mode 100644
index 0000000000..ae70eb68ff
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser.toml
@@ -0,0 +1,34 @@
+[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..354e85a241
--- /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.startLoadingURIString(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..7550379ad1
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js
@@ -0,0 +1,32 @@
+/* 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)");
+ Assert.equal(
+ 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..e1f71204d0
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_images.js
@@ -0,0 +1,113 @@
+/* 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)");
+
+ Assert.equal(
+ 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);
+ }
+ );
+});
+
+add_task(async function test_image_size() {
+ 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 imageSize = pageInfo.document.getElementById("imagesizetext");
+
+ Assert.notEqual("media-unknown-not-cached", imageSize.value);
+
+ pageInfo.close();
+ }
+ );
+});
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..ebf027811d
--- /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.isVisible(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.isVisible(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..47df97db06
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_security.js
@@ -0,0 +1,356 @@
+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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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 =>
+ subj.QueryInterface(Ci.nsICookieNotification).action ==
+ Ci.nsICookieNotification.COOKIE_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..3934cd2aea
--- /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.startLoadingURIString(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..caa832c2e5
--- /dev/null
+++ b/browser/base/content/test/performance/PerfTestHelpers.sys.mjs
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+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.toml b/browser/base/content/test/performance/browser.toml
new file mode 100644
index 0000000000..bd7d56e762
--- /dev/null
+++ b/browser/base/content/test/performance/browser.toml
@@ -0,0 +1,115 @@
+[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'",
+ "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_hidden_browser_vsync.js"]
+
+["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",
+ "win11_2009 && bits == 32",
+]
+
+["browser_tabswitch.js"]
+skip-if = ["os == 'win'"] #Bug 1455054
+
+["browser_toolbariconcolor_restyles.js"]
+
+["browser_urlbar_keyed_search.js"]
+skip-if = ["win11_2009 && 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.
+ "win11_2009 && 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..37e7482c51
--- /dev/null
+++ b/browser/base/content/test/performance/browser_appmenu.js
@@ -0,0 +1,130 @@
+"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 ensureAnimationsFinished();
+ 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_hidden_browser_vsync.js b/browser/base/content/test/performance/browser_hidden_browser_vsync.js
new file mode 100644
index 0000000000..a2e5cc990c
--- /dev/null
+++ b/browser/base/content/test/performance/browser_hidden_browser_vsync.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_hidden_sidebar() {
+ let b = document.createXULElement("browser");
+ for (let [k, v] of Object.entries({
+ type: "content",
+ disablefullscreen: "true",
+ disablehistory: "true",
+ flex: "1",
+ style: "min-width: 300px",
+ message: "true",
+ remote: "true",
+ maychangeremoteness: "true",
+ })) {
+ b.setAttribute(k, v);
+ }
+ let mainBrowser = gBrowser.selectedBrowser;
+ let panel = gBrowser.getPanel(mainBrowser);
+ panel.append(b);
+ let loaded = BrowserTestUtils.browserLoaded(b);
+ BrowserTestUtils.startLoadingURIString(
+ b,
+ `data:text/html,<!doctype html><style>
+ @keyframes fade-in {
+ from {
+ opacity: .25;
+ }
+ to {
+ opacity: 1;
+ }
+ </style>
+ <div style="
+ animation-name: fade-in;
+ animation-direction: alternate;
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-timing-function: ease-in-out;
+ background-color: red;
+ height: 500px;
+ width: 100%;
+ "></div>`
+ );
+ await loaded;
+ ok(b, "Browser was created.");
+ await SpecialPowers.spawn(b, [], async () => {
+ await new Promise(r =>
+ content.requestAnimationFrame(() => content.requestAnimationFrame(r))
+ );
+ });
+ b.hidden = true;
+ ok(b.hidden, "Browser should be hidden.");
+ // Now the framework will test to see vsync goes away
+});
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..6bc623a360
--- /dev/null
+++ b/browser/base/content/test/performance/browser_preferences_usage.js
@@ -0,0 +1,272 @@
+/* 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 (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.startLoadingURIString(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..9d75c14e0f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -0,0 +1,246 @@
+/* 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.sys.mjs",
+ "resource:///modules/BrowserUsageTelemetry.sys.mjs",
+ "resource:///modules/ContentCrashHandlers.sys.mjs",
+ "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",
+ // Sqlite.sys.mjs commented out because of bug 1828735.
+ // "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.sys.mjs is intermittently used.
+ // "resource:///modules/BrowserWindowTracker.sys.mjs",
+ "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..b0f861e47f
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -0,0 +1,186 @@
+/* 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
+ // eslint-disable-next-line mozilla/use-console-createInstance
+ "resource://gre/modules/Log.sys.mjs",
+
+ // Browser front-end
+ "resource:///actors/AboutReaderChild.sys.mjs",
+ "resource:///actors/InteractionsChild.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"
+ );
+}
+
+// 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/translation/LanguageDetector.sys.mjs",
+ "resource://gre/modules/ConsoleAPIStorage.sys.mjs", // Logging related.
+
+ // Session store.
+ "resource://gre/modules/sessionstore/SessionHistory.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://specialpowers/AppTestDelegateChild.sys.mjs",
+ "resource://testing-common/SpecialPowersChild.sys.mjs",
+ "resource://testing-common/WrapPrivileged.sys.mjs",
+ ]),
+ frameScripts: new Set([]),
+ 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();
+
+ 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..e60b95bb3c
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js
@@ -0,0 +1,465 @@
+/* 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";
+const FORK_SERVER = Services.prefs.getBoolPref(
+ "dom.ipc.forkserver.enable",
+ false
+);
+
+/* 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",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ 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",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+ WebExtensions: [
+ {
+ path: "GreD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ // Visible on Windows with an open marker.
+ // The fork server preloads the omnijars.
+ condition: !WIN && !FORK_SERVER,
+ 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`;
+ }
+ Assert.greaterOrEqual(
+ 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 helpString;
+ if (path) {
+ let profilePath = PathUtils.join(path, filename);
+ await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
+ helpString = `open the ${filename} artifact in the Firefox Profiler to see what happened`;
+ } else {
+ helpString =
+ "set the MOZ_UPLOAD_DIR environment variable to record a profile";
+ }
+ ok(
+ false,
+ "Unexpected main thread I/O behavior during child process startup; " +
+ helpString
+ );
+ }
+});
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..3d1ee6352d
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_subframe.js
@@ -0,0 +1,151 @@
+/* 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
+ // eslint-disable-next-line mozilla/use-console-createInstance
+ "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..16300e1525
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_flicker.js
@@ -0,0 +1,72 @@
+/* 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 && isLikelyFocusChange(rects, frame)) {
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ continue;
+ }
+ alreadyFocused = true;
+
+ 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..27ff837dc4
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_hiddenwindow.js
@@ -0,0 +1,47 @@
+/* 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": AppConstants.platform === "macosx",
+ // 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..b65ede26d5
--- /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`;
+ }
+ Assert.greaterOrEqual(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..32c9450b0e
--- /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: 2,
+ },
+ {
+ 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;
+ }
+ Assert.lessOrEqual(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..b529eec040
--- /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}.x/` })),
+ });
+
+ 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..7d11779acc
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowclose.js
@@ -0,0 +1,67 @@
+"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,
+ frames: {
+ filter(rects, frame, previousFrame) {
+ // Ignore the focus-out animation.
+ if (isLikelyFocusChange(rects, frame)) {
+ return [];
+ }
+ return rects;
+ },
+ },
+ },
+ 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..02c6172948
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowopen.js
@@ -0,0 +1,164 @@
+/* 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 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, frame)) {
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ return [];
+ }
+ alreadyFocused = true;
+ 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..29722e6bbe
--- /dev/null
+++ b/browser/base/content/test/performance/head.js
@@ -0,0 +1,1001 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
+ 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",
+});
+
+/**
+ * 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.importESModule(
+ "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs"
+ );
+ 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 ensureAnimationsFinished(win = window) {
+ let animations = win.document.getAnimations();
+ info(`Waiting for ${animations.length} animations`);
+ await Promise.allSettled(animations.map(a => a.finished));
+}
+
+async function prepareSettledWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureNoPreloadedBrowser(win);
+ await ensureAnimationsFinished(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);
+
+ let rectText = r => `${r.toSource()}, window width: ${frame.width}`;
+
+ rects = rects.filter(rect => {
+ for (let e of expectations.exceptions || []) {
+ if (e.condition(rect)) {
+ todo(false, e.name + ", " + rectText(rect));
+ return false;
+ }
+ }
+ return true;
+ });
+
+ if (expectations.filter) {
+ rects = expectations.filter(rects, frame, previousFrame);
+ }
+
+ if (!rects.length) {
+ continue;
+ }
+
+ ok(
+ false,
+ `unexpected ${rects.length} changed rects: ${rects
+ .map(rectText)
+ .join(", ")}`
+ );
+
+ // 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");
+ }
+}
+
+// The first screenshot we get in OSX / Windows shows an unfocused browser
+// window for some reason. See bug 1445161. This function allows to deal with
+// that in a central place.
+function isLikelyFocusChange(rects, frame) {
+ if (rects.length > 3 && rects.every(r => r.y2 < 100)) {
+ // There are at least 4 areas that changed near the top of the screen.
+ // Note that we need a bit more leeway than the titlebar height, because on
+ // OSX other toolbarbuttons in the navigation toolbar also get disabled
+ // state.
+ return true;
+ }
+ if (
+ rects.every(r => r.y1 == 0 && r.x1 == 0 && r.w == frame.width && r.y2 < 100)
+ ) {
+ // Full-width rect in the top of the titlebar.
+ return true;
+ }
+ return false;
+}
diff --git a/browser/base/content/test/performance/hidpi/browser.toml b/browser/base/content/test/performance/hidpi/browser.toml
new file mode 100644
index 0000000000..afcc961963
--- /dev/null
+++ b/browser/base/content/test/performance/hidpi/browser.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs = [
+ "browser.startup.recordImages=true",
+ "layout.css.devPixelsPerPx='2'",
+]
+
+["../browser_startup_images.js"]
+skip-if = ["!debug"]
diff --git a/browser/base/content/test/performance/io/browser.toml b/browser/base/content/test/performance/io/browser.toml
new file mode 100644
index 0000000000..e581849028
--- /dev/null
+++ b/browser/base/content/test/performance/io/browser.toml
@@ -0,0 +1,38 @@
+[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",
+ "extensions.screenshots.disabled=false", # The Screenshots extension is disabled by default in Mochitests. We re-enable it here, since it's a more realistic configuration.
+]
+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",
+ "win11_2009 && bits == 32",
+ "os == 'win' && msix", # Bug 1833639
+]
+
+["../browser_startup_syncIPC.js"]
diff --git a/browser/base/content/test/performance/lowdpi/browser.toml b/browser/base/content/test/performance/lowdpi/browser.toml
new file mode 100644
index 0000000000..391bff58af
--- /dev/null
+++ b/browser/base/content/test/performance/lowdpi/browser.toml
@@ -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..dce33b938d
--- /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.toml",
+ "hidpi/browser.toml",
+ "io/browser.toml",
+ "lowdpi/browser.toml",
+]
+
+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..e5a8051bd2
--- /dev/null
+++ b/browser/base/content/test/performance/triage.json
@@ -0,0 +1,70 @@
+{
+ "triagers": {
+ "Gijs": {
+ "bzmail": "gijskruitbosch+bugs@gmail.com"
+ },
+ "Mike Conley": {
+ "bzmail": "mconley@mozilla.com"
+ },
+ "Florian Quèze": {
+ "bzmail": "florian@mozilla.com"
+ },
+ "Alex Thayer": {
+ "bzmail": "dothayer@mozilla.com"
+ }
+ },
+ "duty-start-dates": {
+ "2023-12-07": "Mike Conley",
+ "2023-12-14": "Florian Quèze",
+ "2023-12-21": "Alex Thayer",
+ "2023-12-28": "Gijs Kruitbosch",
+ "2024-01-04": "Mike Conley",
+ "2024-01-11": "Florian Quèze",
+ "2024-01-18": "Alex Thayer",
+ "2024-01-25": "Gijs Kruitbosch",
+ "2024-02-01": "Mike Conley",
+ "2024-02-08": "Florian Quèze",
+ "2024-02-15": "Alex Thayer",
+ "2024-02-22": "Gijs Kruitbosch",
+ "2024-02-29": "Mike Conley",
+ "2024-03-07": "Florian Quèze",
+ "2024-03-14": "Alex Thayer",
+ "2024-03-21": "Gijs Kruitbosch",
+ "2024-03-28": "Mike Conley",
+ "2024-04-04": "Florian Quèze",
+ "2024-04-11": "Alex Thayer",
+ "2024-04-18": "Gijs Kruitbosch",
+ "2024-04-25": "Mike Conley",
+ "2024-05-02": "Florian Quèze",
+ "2024-05-09": "Alex Thayer",
+ "2024-05-16": "Gijs Kruitbosch",
+ "2024-05-23": "Mike Conley",
+ "2024-05-30": "Florian Quèze",
+ "2024-06-06": "Alex Thayer",
+ "2024-06-13": "Gijs Kruitbosch",
+ "2024-06-20": "Mike Conley",
+ "2024-06-27": "Florian Quèze",
+ "2024-07-04": "Alex Thayer",
+ "2024-07-11": "Gijs Kruitbosch",
+ "2024-07-18": "Mike Conley",
+ "2024-07-25": "Florian Quèze",
+ "2024-08-01": "Alex Thayer",
+ "2024-08-08": "Gijs Kruitbosch",
+ "2024-08-15": "Mike Conley",
+ "2024-08-22": "Florian Quèze",
+ "2024-08-29": "Alex Thayer",
+ "2024-09-05": "Gijs Kruitbosch",
+ "2024-09-12": "Mike Conley",
+ "2024-09-19": "Florian Quèze",
+ "2024-09-26": "Alex Thayer",
+ "2024-10-03": "Gijs Kruitbosch",
+ "2024-10-10": "Mike Conley",
+ "2024-10-17": "Florian Quèze",
+ "2024-10-24": "Alex Thayer",
+ "2024-10-31": "Gijs Kruitbosch",
+ "2024-11-07": "Mike Conley",
+ "2024-11-14": "Florian Quèze",
+ "2024-11-21": "Gijs Kruitbosch",
+ "2024-11-28": "Alex Thayer"
+ }
+}
diff --git a/browser/base/content/test/perftest.toml b/browser/base/content/test/perftest.toml
new file mode 100644
index 0000000000..715b392c00
--- /dev/null
+++ b/browser/base/content/test/perftest.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["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.toml b/browser/base/content/test/permissions/browser.toml
new file mode 100644
index 0000000000..31892b0358
--- /dev/null
+++ b/browser/base/content/test/permissions/browser.toml
@@ -0,0 +1,51 @@
+[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
+]
+
+["browser_canvas_rfp_exclusion.js"]
+
+["browser_permission_delegate_geo.js"]
+
+["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..d81481d6a5
--- /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.isVisible(autoplayBlockedIcon());
+ }, "Blocked icon is shown");
+}
+
+async function blockedIconHidden() {
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.isHidden(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.isHidden(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.startLoadingURIString(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.startLoadingURIString(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.isHidden(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.startLoadingURIString(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.startLoadingURIString(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..4dbbe1ea97
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permission_delegate_geo.js
@@ -0,0 +1,278 @@
+/* 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],
+ // 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..5f5ab006fb
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions.js
@@ -0,0 +1,698 @@
+/*
+ * 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");
+
+ // We intentionally turn off a11y_checks, because the following function
+ // is expected to click a toolbar button that may be already hidden
+ // with "display:none;". The permissions panel anchor is hidden because
+ // the last permission was removed, however we force opening the panel
+ // anyways in order to test that the list has been properly emptied:
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ await openPermissionPopup();
+ AccessibilityUtils.resetEnv();
+
+ 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.isHidden(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.isHidden(reloadHint), "Reload hint is hidden");
+
+ let cancelButtons = permissionsList.querySelectorAll(
+ ".permission-popup-permission-remove-button"
+ );
+ PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+
+ cancelButtons[0].click();
+ ok(!BrowserTestUtils.isHidden(reloadHint), "Reload hint is visible");
+
+ cancelButtons[1].click();
+ ok(!BrowserTestUtils.isHidden(reloadHint), "Reload hint is visible");
+
+ await closePermissionPopup();
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, PERMISSIONS_PAGE);
+ await loaded;
+ await openPermissionPopup();
+
+ ok(
+ BrowserTestUtils.isHidden(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");
+ Assert.equal(
+ menulist,
+ null,
+ "The popup permission menulist is not visible"
+ );
+
+ let removeButton = permissionsList.querySelector(
+ ".permission-popup-permission-remove-button"
+ );
+ Assert.equal(
+ 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.isHidden(gPermissionPanel._permissionPopup),
+ "Popup is hidden"
+ );
+
+ await openPermissionPopup();
+
+ ok(
+ !BrowserTestUtils.isHidden(gPermissionPanel._permissionPopup),
+ "Popup is shown"
+ );
+
+ let reloaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ PERMISSIONS_PAGE
+ );
+ EventUtils.synthesizeKey("VK_F5", {}, browser.ownerGlobal);
+ await reloaded;
+
+ ok(
+ BrowserTestUtils.isHidden(gPermissionPanel._permissionPopup),
+ "Popup is hidden"
+ );
+ });
+});
+
+async function helper3rdPartyStoragePermissionTest(permissionID) {
+ // 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.isHidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ await closePermissionPopup();
+
+ let storagePermissionID = `${permissionID}^https://example2.com`;
+ PermissionTestUtils.add(
+ browser.currentURI,
+ storagePermissionID,
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+ ok(
+ BrowserTestUtils.isVisible(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.isVisible(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.isHidden(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.isHidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ await closePermissionPopup();
+ });
+}
+
+add_task(async function test3rdPartyStoragePermission() {
+ await helper3rdPartyStoragePermissionTest("3rdPartyStorage");
+});
+
+add_task(async function test3rdPartyFrameStoragePermission() {
+ await helper3rdPartyStoragePermissionTest("3rdPartyFrameStorage");
+});
+
+add_task(async function test3rdPartyBothStoragePermission() {
+ // Test the handling of both types of 3rdParty(Frame)?Storage permissions together
+
+ 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.isHidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ await closePermissionPopup();
+
+ let storagePermissionID = "3rdPartyFrameStorage^https://example2.com";
+ PermissionTestUtils.add(
+ browser.currentURI,
+ storagePermissionID,
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+ ok(
+ BrowserTestUtils.isVisible(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,
+ "3rdPartyStorage^https://www.example2.com",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPermissionPopup();
+
+ testPermListHasEntries(true);
+ ok(
+ BrowserTestUtils.isVisible(storagePermissionAnchor.firstElementChild),
+ "Anchor header is visible"
+ );
+
+ labels = permissionsList.querySelectorAll(
+ ".permission-popup-permission-label"
+ );
+ is(labels.length, 1, "One 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"
+ );
+ is(
+ PermissionTestUtils.testPermission(
+ browser.currentURI,
+ "3rdPartyStorage^https://www.example2.com"
+ ),
+ SitePermissions.UNKNOWN,
+ "3rdPartyStorage permission removed from permission manager"
+ );
+
+ 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..9f0066f8e1
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js
@@ -0,0 +1,45 @@
+/**
+ * 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: [
+ ["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..e306cb1058
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_postPrompt.js
@@ -0,0 +1,82 @@
+/* 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.isVisible(icon),
+ "notifications icon is not visible at first"
+ );
+
+ await SpecialPowers.spawn(browser, [], task);
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.isVisible(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
+ );
+
+ // 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..7a8953de47
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_site_scoped_permissions.js
@@ -0,0 +1,124 @@
+/* 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);
+
+ // We intentionally turn off a11y_checks, because the following function
+ // is expected to click a toolbar button that may be already hidden
+ // with "display:none;". The permissions panel anchor is hidden because
+ // the last permission was removed, however we force opening the panel
+ // anyways in order to test that the list has been properly emptied:
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ await openPermissionPopup();
+ AccessibilityUtils.resetEnv();
+
+ 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);
+
+ // We intentionally turn off a11y_checks, because the following function
+ // is expected to click a toolbar button that may be already hidden
+ // with "display:none;". The permissions panel anchor is hidden because
+ // the last permission was removed, however we force opening the panel
+ // anyways in order to test that the list has been properly emptied:
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ await openPermissionPopup();
+ AccessibilityUtils.resetEnv();
+
+ 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.toml b/browser/base/content/test/plugins/browser.toml
new file mode 100644
index 0000000000..b941e7278d
--- /dev/null
+++ b/browser/base/content/test/plugins/browser.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files = [
+ "empty_file.html",
+ "head.js",
+ "plugin_bug797677.html",
+]
+
+["browser_bug797677.js"]
+
+["browser_enable_DRM_prompt.js"]
+
+["browser_globalplugin_crashinfobar.js"]
+run-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..60a59ea98b
--- /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_FALLBACK,
+ "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..3dfa26f021
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_enable_DRM_prompt.js
@@ -0,0 +1,298 @@
+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);
+ let notificationShownPromise = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "drmContentDisabled"
+ );
+
+ // 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);
+ await notificationShownPromise;
+ let notification = box.currentNotification;
+ await notification.updateComplete;
+
+ 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);
+ let notificationShownPromise = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "drmContentDisabled"
+ );
+
+ // 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);
+ await notificationShownPromise;
+ let notification = box.currentNotification;
+ await notification.updateComplete;
+
+ 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_drm_prompt_only_shows_one_notification() {
+ 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);
+ let notificationShownPromise = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "drmContentDisabled"
+ );
+
+ // Send three EME requests to ensure only one instance of the
+ // "Enable DRM" notification appears in the chrome
+ for (let i = 0; i < 3; i++) {
+ 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 };
+ });
+ }
+
+ // Verify the UI prompt showed.
+ let box = gBrowser.getNotificationBox(browser);
+ await notificationShownPromise;
+ let notification = box.currentNotification;
+
+ ok(notification, "Notification should be visible");
+ is(
+ notification.getAttribute("value"),
+ "drmContentDisabled",
+ "Should be showing the right notification"
+ );
+ is(
+ box.allNotifications.length,
+ 1,
+ "There should only be one notification shown"
+ );
+ });
+});
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..38ac3864c0
--- /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.trim(),
+ "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..4f6c25b92a
--- /dev/null
+++ b/browser/base/content/test/plugins/head.js
@@ -0,0 +1,204 @@
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.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.startLoadingURIString(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/popupNotifications/browser.toml b/browser/base/content/test/popupNotifications/browser.toml
new file mode 100644
index 0000000000..6a7ff4e14e
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser.toml
@@ -0,0 +1,98 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["browser_displayURI.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification_2.js"]
+https_first_disabled = true
+skip-if = [
+ "os == 'linux' && debug", # bug 1251135
+ "os == 'linux' && asan", # bug 1251135
+ "os == 'linux' && bits == 64 && os_version == '18.04'", # bug 1251135
+]
+
+["browser_popupNotification_3.js"]
+https_first_disabled = true
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+ "verify",
+]
+
+["browser_popupNotification_4.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification_5.js"]
+skip-if = ["true"] # bug 1332646
+
+["browser_popupNotification_accesskey.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+ "os == 'mac'",
+]
+
+["browser_popupNotification_checkbox.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+ "a11y_checks", # Bug 1858037 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland)
+]
+
+["browser_popupNotification_hide_after_identity_panel.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification_hide_after_protections_panel.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification_keyboard.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification_learnmore.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification_no_anchors.js"]
+https_first_disabled = true
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_popupNotification_security_delay.js"]
+
+["browser_popupNotification_selection_required.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && asan",
+]
+
+["browser_reshow_in_background.js"]
+skip-if = [
+ "os == 'linux' && debug",
+ "os == 'linux' && 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..d25e343452
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_displayURI.js
@@ -0,0 +1,149 @@
+/*
+ * 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");
+ ok(
+ body.innerHTML.includes("local file"),
+ `file:// URIs should be displayed as local file.`
+ );
+ }
+ );
+
+ 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..1b7626c660
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
@@ -0,0 +1,378 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* 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
+ );
+ Assert.notEqual(
+ 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..0ec5de0c3a
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
@@ -0,0 +1,502 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+ );
+ Assert.greater(
+ 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..4a9276512c
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js
@@ -0,0 +1,402 @@
+/* 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");
+
+ // Wait for a tick of the event loop to ensure the button we're clicking
+ // has been slotted into moz-button-group
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ 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."
+ );
+});
+
+/**
+ * Tests that the security delay gets reset when a window is repositioned and
+ * the PopupNotifications panel position is updated.
+ */
+add_task(async function test_notificationWindowMove() {
+ await ensureSecurityDelayReady();
+
+ info("Open a notification.");
+ let popupShownPromise = waitForNotificationPanel();
+ showNotification();
+ await popupShownPromise;
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be open after show call."
+ );
+
+ // Test that the initial security delay works.
+ 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.");
+ let 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;
+ }
+
+ info("Wait for security delay to expire.");
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, TEST_SECURITY_DELAY + 500)
+ );
+
+ info("Reposition the window");
+ // Remember original window position.
+ let { screenX, screenY } = window;
+
+ let promisePopupPositioned = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuppositioned"
+ );
+
+ // Move the window.
+ window.moveTo(200, 200);
+
+ // Wait for the panel to reposition and the PopupNotifications listener to run.
+ await promisePopupPositioned;
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ 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");
+ let notificationHiddenPromise = waitForNotificationPanelHidden();
+ triggerMainCommand(PopupNotifications.panel);
+
+ info("Wait for panel to be hidden.");
+ await notificationHiddenPromise;
+
+ ok(
+ !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
+ "Should not longer see the notification."
+ );
+
+ // Reset window position
+ window.moveTo(screenX, screenY);
+});
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..8dd7e65de0
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js
@@ -0,0 +1,70 @@
+"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.
+ let tabB = BrowserTestUtils.addTab(gBrowser, "https://example.com/");
+ await BrowserTestUtils.browserLoaded(tabB.linkedBrowser);
+
+ let tabC = BrowserTestUtils.addTab(gBrowser, "https://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",
+ "",
+ "geo-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..57c393e2a2
--- /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.startLoadingURIString(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"
+ );
+ });
+}
+
+ChromeUtils.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.toml b/browser/base/content/test/popups/browser.toml
new file mode 100644
index 0000000000..f3ecb82b5b
--- /dev/null
+++ b/browser/base/content/test/popups/browser.toml
@@ -0,0 +1,93 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "popup_blocker_a.html", # used as dummy file
+]
+
+["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'", # Frequent bug 1081925 and bug 1125520 failures
+ "debug",
+]
+
+["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.
+]
+
+["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..ec1002befd
--- /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(5);
+
+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()}`
+ );
+
+ Assert.equal(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..9e7ca68e66
--- /dev/null
+++ b/browser/base/content/test/popups/head.js
@@ -0,0 +1,568 @@
+/* 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;
+ }
+
+ Assert.greaterOrEqual(
+ 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);
+
+ 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 (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/privateBrowsing/browser.toml b/browser/base/content/test/privateBrowsing/browser.toml
new file mode 100644
index 0000000000..4750e8de69
--- /dev/null
+++ b/browser/base/content/test/privateBrowsing/browser.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["browser_private_browsing_simplified_ui.js"]
diff --git a/browser/base/content/test/privateBrowsing/browser_private_browsing_simplified_ui.js b/browser/base/content/test/privateBrowsing/browser_private_browsing_simplified_ui.js
new file mode 100644
index 0000000000..162bf7ab5a
--- /dev/null
+++ b/browser/base/content/test/privateBrowsing/browser_private_browsing_simplified_ui.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.felt-privacy-v1", true]],
+ });
+});
+
+add_task(async function check_for_simplified_pbm_ui() {
+ let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let pocketButton = pbmWindow.document.getElementById("save-to-pocket-button");
+ ok(!pocketButton, "Pocket button is removed from PBM window");
+ let bookmarksBar = pbmWindow.document.getElementById("PersonalToolbar");
+ ok(
+ bookmarksBar.getAttribute("collapsed"),
+ "Bookmarks bar is hidden in PBM window"
+ );
+
+ await BrowserTestUtils.openNewForegroundTab(pbmWindow, "about:blank", true);
+ ok(
+ bookmarksBar.getAttribute("collapsed"),
+ "Bookmarks bar is hidden in PBM window after loading a new tab"
+ );
+
+ await BrowserTestUtils.closeWindow(pbmWindow);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.showInPrivateBrowsing", true]],
+ });
+ pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ bookmarksBar = pbmWindow.document.getElementById("PersonalToolbar");
+ console.info(bookmarksBar.getAttribute("collapsed"));
+ Assert.equal(
+ bookmarksBar.getAttribute("collapsed").toString(),
+ "false",
+ "Bookmarks bar is visible in PBM window when showInPrivateBrowsing pref is true"
+ );
+
+ await BrowserTestUtils.closeWindow(pbmWindow);
+});
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.toml b/browser/base/content/test/protectionsUI/browser.toml
new file mode 100644
index 0000000000..10611cbe8c
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser.toml
@@ -0,0 +1,97 @@
+[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
+
+["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
+skip-if = ["a11y_checks"] # Bugs 1858041 and 1849179 for causing intermittent crashes
+
+["browser_protectionsUI_icon_state.js"]
+https_first_disabled = true
+
+["browser_protectionsUI_info_message.js"]
+
+["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", # Bug 1546797
+ "asan",
+]
+
+["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_suspicious_fingerprinters_subview.js"]
+support-files = [
+ "canvas-fingerprinter.html",
+ "font-fingerprinter.html",
+]
+
+["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..98698e087e
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI.js
@@ -0,0 +1,738 @@
+/* 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"
+);
+
+requestLongerTimeout(3);
+
+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();
+ });
+});
+
+async function clickToggle(toggle) {
+ let changed = BrowserTestUtils.waitForEvent(toggle, "toggle");
+ await EventUtils.synthesizeMouseAtCenter(toggle.buttonEl, {});
+ await changed;
+}
+
+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"
+ );
+ console.log(buttonEvents);
+ is(buttonEvents.length, 1, "recorded telemetry for opening the popup");
+
+ // Check the visibility of the "Site not working?" link.
+ ok(
+ BrowserTestUtils.isVisible(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be visible."
+ );
+
+ // The 'Site Fixed?' link should be hidden.
+ ok(
+ BrowserTestUtils.isHidden(
+ 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("pressed"),
+ "TP Switch should be on"
+ );
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await clickToggle(gProtectionsHandler._protectionsPopupTPSwitch);
+
+ // The 'Site not working?' link should be hidden after clicking the TP switch.
+ ok(
+ BrowserTestUtils.isHidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be hidden after TP switch turns to off."
+ );
+ // Same for the 'Site Fixed?' link
+ ok(
+ BrowserTestUtils.isHidden(
+ 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 toastShown = waitForProtectionsPanelToast();
+
+ await browserLoadedPromise;
+
+ // Wait until the ETP state confirmation toast is shown and hides itself.
+ await toastShown;
+
+ await openProtectionsPanel();
+ ok(
+ !gProtectionsHandler._protectionsPopupTPSwitch.hasAttribute("pressed"),
+ "TP Switch should be off"
+ );
+
+ // The 'Site not working?' link should be hidden if the TP is off.
+ ok(
+ BrowserTestUtils.isHidden(
+ 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.isVisible(
+ 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);
+
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+
+ await clickToggle(gProtectionsHandler._protectionsPopupTPSwitch);
+
+ ok(
+ BrowserTestUtils.isHidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ `The 'Site not working?' link should be still hidden after toggling TP
+ switch to on from off.`
+ );
+ ok(
+ BrowserTestUtils.isHidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be hidden."
+ );
+
+ // Wait for the protections panel to be hidden as the result of the ETP toggle
+ // on action.
+ await popuphiddenPromise;
+
+ toastShown = waitForProtectionsPanelToast();
+
+ await browserLoadedPromise;
+
+ // Wait until the ETP state confirmation toast is shown and hides itself.
+ await toastShown;
+
+ 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.isVisible(item),
+ `The section '${item.id}' is hidden in the toast.`
+ );
+ } else {
+ ok(
+ BrowserTestUtils.isVisible(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.
+ await clickToggle(gProtectionsHandler._protectionsPopupTPSwitch);
+
+ // 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"
+ );
+ // We intentionally turn off a11y_checks, because the following click
+ // is targeting static toast message that's not meant to be interactive and
+ // is not expected to be accessible:
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ document.getElementById("protections-popup-mainView-panel-header").click();
+ AccessibilityUtils.resetEnv();
+ 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);
+ await clickToggle(gProtectionsHandler._protectionsPopupTPSwitch);
+
+ // 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.isHidden(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.isVisible(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.isVisible(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.
+ await clickToggle(gProtectionsHandler._protectionsPopupTPSwitch);
+ 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..6fe3db3a70
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_categories.js
@@ -0,0 +1,279 @@
+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 FPP_PREF = "privacy.fingerprintingProtection";
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+const l10n = new Localization([
+ "browser/siteProtections.ftl",
+ "branding/brand.ftl",
+]);
+
+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"));
+ const blockAllMsg = await l10n.formatValue(
+ "content-blocking-cookies-blocking-all-label"
+ );
+ await TestUtils.waitForCondition(
+ () => categoryLabel.textContent == blockAllMsg,
+ "The category label has updated correctly"
+ );
+
+ 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"));
+ const block3rdMsg = await l10n.formatValue(
+ "content-blocking-cookies-blocking-third-party-label"
+ );
+ await TestUtils.waitForCondition(
+ () => categoryLabel.textContent == block3rdMsg,
+ "The category label has updated correctly"
+ );
+
+ 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"));
+ const blockTrackersMsg = await l10n.formatValue(
+ "content-blocking-cookies-blocking-trackers-label"
+ );
+ await TestUtils.waitForCondition(
+ () => categoryLabel.textContent == blockTrackersMsg,
+ "The category label has updated correctly"
+ );
+
+ 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 == blockTrackersMsg,
+ "The category label has updated correctly"
+ );
+
+ 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}`);
+
+ Assert.equal(
+ 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);
+ Services.prefs.setBoolPref(FPP_PREF, false);
+
+ 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() {
+ requestLongerTimeout(3);
+
+ 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..688bc9d7c8
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookie_banner.js
@@ -0,0 +1,489 @@
+/* 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.isVisible(el.section),
+ expectVisible,
+ `Cookie banner section should be ${
+ expectVisible ? "visible" : "not visible"
+ }.`
+ );
+ is(
+ BrowserTestUtils.isVisible(el.sectionSeparator),
+ expectVisible,
+ `Cookie banner section separator should be ${
+ expectVisible ? "visible" : "not visible"
+ }.`
+ );
+ is(
+ BrowserTestUtils.isVisible(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") {
+ Assert.equal(
+ el.section.dataset.state,
+ "detected",
+ "CBH switch is set to ON"
+ );
+
+ ok(BrowserTestUtils.isVisible(el.labelON), "ON label should be visible");
+ ok(
+ !BrowserTestUtils.isVisible(el.labelOFF),
+ "OFF label should not be visible"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(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") {
+ Assert.equal(
+ el.section.dataset.state,
+ "site-disabled",
+ "CBH switch is set to OFF"
+ );
+
+ ok(
+ !BrowserTestUtils.isVisible(el.labelON),
+ "ON label should not be visible"
+ );
+ ok(BrowserTestUtils.isVisible(el.labelOFF), "OFF label should be visible");
+ ok(
+ !BrowserTestUtils.isVisible(el.labelUNDETECTED),
+ "UNDETECTED label should not be visible"
+ );
+
+ is(
+ pref,
+ MODE_DISABLED,
+ `There should be a per-site exception for ${currentURI.spec}.`
+ );
+ } else {
+ Assert.equal(
+ el.section.dataset.state,
+ "undetected",
+ "CBH not supported for site"
+ );
+
+ ok(
+ !BrowserTestUtils.isVisible(el.labelON),
+ "ON label should not be visible"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(el.labelOFF),
+ "OFF label should not be visible"
+ );
+ ok(
+ BrowserTestUtils.isVisible(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.isVisible(enableButton), "Enable button is visible");
+ enableButton.click();
+ } else {
+ ok(BrowserTestUtils.isVisible(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() {
+ requestLongerTimeout(3);
+
+ // 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..00281ac415
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js
@@ -0,0 +1,533 @@
+/* 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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(listHeaders[0]),
+ "Only one header, should be hidden"
+ );
+ } else {
+ for (let header of listHeaders) {
+ ok(
+ BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(stateLabel), "State label is visible");
+ is(
+ stateLabel.getAttribute("data-l10n-id"),
+ "content-blocking-cookies-view-allowed-label",
+ "State label has correct text"
+ );
+
+ let button = listItem.querySelector(
+ ".permission-popup-permission-remove-button"
+ );
+ ok(BrowserTestUtils.isVisible(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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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() {
+ requestLongerTimeout(2);
+
+ 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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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..30a836d352
--- /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.isVisible(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.isVisible(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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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..1beda6c6e2
--- /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.isVisible(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.isVisible(categoryItem)
+ );
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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.isVisible(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..26b131d4eb
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js
@@ -0,0 +1,43 @@
+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) {
+ const win = newTabBrowser.ownerGlobal;
+ await openProtectionsPanel(false, win);
+ 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;
+
+ const gProtectionsHandler = win.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.getAttribute(
+ "data-l10n-id"
+ ),
+ "tracking-protection-icon-active",
+ "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..9d2a2f1da6
--- /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.isHidden(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.isVisible(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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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_info_message.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_info_message.js
new file mode 100644
index 0000000000..fadfaaab98
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_info_message.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests the info message that apears in the protections panel
+ * on first render, and afterward by clicking the "info" icon */
+
+"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";
+
+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],
+ // Set the infomessage pref to ensure the message is displayed
+ // every time
+ ["browser.protections_panel.infoMessage.seen", false],
+ ],
+ });
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.telemetry.clearEvents();
+
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.clearEvents();
+ });
+});
+
+add_task(async function testPanelInfoMessage() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TRACKING_PAGE
+ );
+
+ await openProtectionsPanel();
+
+ await TestUtils.waitForCondition(() => {
+ return gProtectionsHandler._protectionsPopup.hasAttribute(
+ "infoMessageShowing"
+ );
+ });
+
+ // Test that the info message is displayed when the panel opens
+ let container = document.getElementById("messaging-system-message-container");
+ let message = document.getElementById("protections-popup-message");
+ let learnMoreLink = document.querySelector(
+ "#messaging-system-message-container .text-link"
+ );
+
+ // Check the visibility of the info message.
+ ok(
+ BrowserTestUtils.isVisible(container),
+ "The message container should exist."
+ );
+
+ ok(BrowserTestUtils.isVisible(message), "The message should be visible.");
+
+ ok(BrowserTestUtils.isVisible(learnMoreLink), "The link should be visible.");
+
+ // Check telemetry for the info message
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ let messageEvents = events.filter(
+ e =>
+ e[1] == "security.ui.protectionspopup" &&
+ e[2] == "open" &&
+ e[3] == "protectionspopup_cfr" &&
+ e[4] == "impression"
+ );
+ is(
+ messageEvents.length,
+ 1,
+ "recorded telemetry for showing the info message"
+ );
+ //Clear telemetry from this test so that the next one doesn't fall over
+ Services.telemetry.clearEvents();
+ BrowserTestUtils.removeTab(tab);
+});
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..713d13c30c
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js
@@ -0,0 +1,104 @@
+/* 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() {
+ requestLongerTimeout(3);
+
+ // The protections panel needs to be openend at least once,
+ // or the milestone-achieved pref observer is not triggered.
+ await BrowserTestUtils.withNewTab("https://example.com", async () => {
+ await openProtectionsPanel();
+ await closeProtectionsPanel();
+ });
+
+ // 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.isVisible(
+ gProtectionsHandler._protectionsPopupMilestonesText
+ ),
+ "Milestones section should be visible in the panel."
+ );
+
+ await closeProtectionsPanel();
+ await openProtectionsPanel();
+
+ ok(
+ BrowserTestUtils.isVisible(
+ 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.isVisible(
+ 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..5790ebe1e0
--- /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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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..ee393801bc
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js
@@ -0,0 +1,179 @@
+/* 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.isVisible(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.getAttribute(
+ "data-l10n-id"
+ ),
+ "tracking-protection-icon-active",
+ "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.getAttribute(
+ "data-l10n-id"
+ ),
+ "tracking-protection-icon-disabled",
+ "correct tooltip"
+ );
+
+ ok(
+ BrowserTestUtils.isVisible(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..2ae0d5c9d9
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js
@@ -0,0 +1,415 @@
+/* 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 GPC_PREF = "privacy.globalprivacycontrol.enabled";
+
+const PREF_REPORT_BREAKAGE_URL = "browser.contentblocking.reportBreakage.url";
+
+let { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+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();
+
+ // Disable Report Broken Site, as it hides "Site not working?" when enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.new-webcompat-reporter.enabled", false]],
+ });
+
+ 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(GPC_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",
+ ],
+ ["privacy.globalprivacycontrol.enabled", true],
+ ],
+ });
+});
+
+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.isVisible(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.isVisible(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.isHidden(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.isVisible(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",
+ "privacy.globalprivacycontrol.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.isVisible(errorMessage)
+ );
+ is(
+ comments.value,
+ "This is a comment",
+ "Comment not cleared in case of an error"
+ );
+ gProtectionsHandler._protectionsPopup.hidePopup();
+ } else {
+ ok(BrowserTestUtils.isHidden(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..d4addd380d
--- /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.isHidden(
+ 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..3f579b0280
--- /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.isVisible(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.isVisible(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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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.isVisible(categoryItem), "Item should be hidden");
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+ ok(
+ BrowserTestUtils.isVisible(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.isVisible(categoryItem), "Item should not be visible");
+ ok(
+ BrowserTestUtils.isVisible(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.isVisible(categoryItem), "Item should be visible");
+ ok(
+ !BrowserTestUtils.isVisible(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..126328914e
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_state.js
@@ -0,0 +1,403 @@
+/*
+ * 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;
+
+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.getAttribute(
+ "data-l10n-id"
+ ),
+ "tracking-protection-icon-no-trackers-detected",
+ "correct tooltip"
+ );
+ ok(
+ BrowserTestUtils.isVisible(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.getAttribute(
+ "data-l10n-id"
+ ),
+ "tracking-protection-icon-disabled",
+ "correct tooltip"
+ );
+
+ ok(
+ !BrowserTestUtils.isHidden(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.isVisible(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.getAttribute(
+ "data-l10n-id"
+ ),
+ blockedByTP
+ ? "tracking-protection-icon-active"
+ : "tracking-protection-icon-no-trackers-detected",
+ "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.getAttribute(
+ "data-l10n-id"
+ ),
+ "tracking-protection-icon-disabled",
+ "correct tooltip"
+ );
+
+ ok(
+ BrowserTestUtils.isVisible(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() {
+ requestLongerTimeout(3);
+
+ 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..186c3b36ce
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_subview_shim.js
@@ -0,0 +1,400 @@
+/* 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.isVisible(
+ 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.isVisible(categoryItem);
+ });
+
+ ok(
+ BrowserTestUtils.isVisible(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.isVisible(shimAllowSection, "Shim allow hint is visible.");
+ } else {
+ BrowserTestUtils.isHidden(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_suspicious_fingerprinters_subview.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_suspicious_fingerprinters_subview.js
new file mode 100644
index 0000000000..220f985223
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_suspicious_fingerprinters_subview.js
@@ -0,0 +1,427 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+/**
+ * Bug 1863280 - Testing the fingerprinting category of the protection panel
+ * shows the suspicious fingerpinter domain if the fingerprinting
+ * protection is enabled
+ */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ ""
+);
+
+const TEST_DOMAIN = "https://www.example.com";
+const TEST_3RD_DOMAIN = "https://example.org";
+
+const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "benignPage.html";
+const TEST_3RD_CANVAS_FP_PAGE =
+ TEST_3RD_DOMAIN + TEST_PATH + "canvas-fingerprinter.html";
+const TEST_3RD_FONT_FP_PAGE =
+ TEST_3RD_DOMAIN + TEST_PATH + "font-fingerprinter.html";
+
+const FINGERPRINT_BLOCKING_PREF =
+ "privacy.trackingprotection.fingerprinting.enabled";
+const FINGERPRINT_PROTECTION_PREF = "privacy.fingerprintingProtection";
+const FINGERPRINT_PROTECTION_PBM_PREF =
+ "privacy.fingerprintingProtection.pbmode";
+
+/**
+ * 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 wait until the suspicious fingerprinting content
+ * blocking event is dispatched.
+ *
+ * @param {Window} win The window to listen content blocking events.
+ * @returns {Promise} A promise that resolves when the event files.
+ */
+async function waitForSuspiciousFingerprintingEvent(win) {
+ return new Promise(resolve => {
+ let listener = {
+ onContentBlockingEvent(webProgress, request, event) {
+ info(`Received onContentBlockingEvent event: ${event}`);
+ if (
+ event &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING
+ ) {
+ win.gBrowser.removeProgressListener(listener);
+ resolve();
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(listener);
+ });
+}
+
+/**
+ * A helper function that opens a tests page that embeds iframes with given
+ * urls.
+ *
+ * @param {string[]} urls An array of urls that will be embedded in the test page.
+ * @param {boolean} usePrivateWin true to indicate testing in a private window.
+ * @param {Function} testFn The test function that will be called after the
+ * iframes have been loaded.
+ */
+async function openTestPage(urls, usePrivateWin, testFn) {
+ let win = window;
+
+ if (usePrivateWin) {
+ win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ }
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: TEST_PAGE },
+ async browser => {
+ info("Loading all iframes");
+
+ for (const url of urls) {
+ await SpecialPowers.spawn(browser, [url], async testUrl => {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = resolve;
+
+ content.document.body.appendChild(ifr);
+ ifr.src = testUrl;
+ });
+ });
+ }
+
+ info("Running the test");
+ await testFn(win, browser);
+ }
+ );
+
+ if (usePrivateWin) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+}
+
+/**
+ * A testing function verifies that fingerprinting category is not shown.
+ *
+ * @param {*} win The window to run the test.
+ */
+async function testCategoryNotShown(win) {
+ await openProtectionsPanel(false, win);
+
+ let categoryItem = win.document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ // The fingerprinting category should have the 'notFound' class to indicate
+ // that no fingerprinter was found in the page.
+ ok(
+ notFound("protections-popup-category-fingerprinters"),
+ "Fingerprinting category is not found"
+ );
+
+ ok(
+ !BrowserTestUtils.isVisible(categoryItem),
+ "Fingerprinting category item is not visible"
+ );
+
+ await closeProtectionsPanel(win);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FINGERPRINT_BLOCKING_PREF, true]],
+ });
+});
+
+// Verify that fingerprinting category is not shown if fingerprinting protection
+// is disabled.
+add_task(async function testFPPDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FINGERPRINT_PROTECTION_PREF, false]],
+ });
+
+ await openTestPage(
+ [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
+ false,
+ testCategoryNotShown
+ );
+});
+
+// Verify that fingerprinting category is not shown if no fingerprinting
+// activity is detected.
+add_task(async function testFPPEnabledWithoutFingerprintingActivity() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FINGERPRINT_PROTECTION_PREF, true]],
+ });
+
+ // Test the case where the page doesn't load any fingerprinter.
+ await openTestPage([], false, testCategoryNotShown);
+
+ // Test the case where the page loads only one fingerprinter. We don't treat
+ // this case as suspicious fingerprinting.
+ await openTestPage([TEST_3RD_FONT_FP_PAGE], false, testCategoryNotShown);
+
+ // Test the case where the page loads the same fingerprinter multiple times.
+ // We don't treat this case as suspicious fingerprinting.
+ await openTestPage(
+ [TEST_3RD_FONT_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
+ false,
+ testCategoryNotShown
+ );
+});
+
+// Verify that fingerprinting category is not shown if no fingerprinting
+// activity is detected.
+add_task(
+ async function testFPPEnabledWithoutSuspiciousFingerprintingActivity() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FINGERPRINT_PROTECTION_PREF, true]],
+ });
+
+ // Test the case where the page doesn't load any fingerprinter.
+ await openTestPage([], false, testCategoryNotShown);
+
+ // Test the case where the page loads only one fingerprinter. We don't treat
+ // this case as suspicious fingerprinting.
+ await openTestPage([TEST_3RD_FONT_FP_PAGE], false, testCategoryNotShown);
+
+ // Test the case where the page loads the same fingerprinter multiple times.
+ // We don't treat this case as suspicious fingerprinting.
+ await openTestPage(
+ [TEST_3RD_FONT_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
+ false,
+ testCategoryNotShown
+ );
+ }
+);
+
+// Verify that fingerprinting category is properly shown and the fingerprinting
+// subview displays the origin of the suspicious fingerprinter.
+add_task(async function testFingerprintingSubview() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FINGERPRINT_PROTECTION_PREF, true]],
+ });
+
+ await openTestPage(
+ [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
+ false,
+ async (win, _) => {
+ await openProtectionsPanel(false, win);
+
+ let categoryItem = win.document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await BrowserTestUtils.waitForMutationCondition(categoryItem, {}, () =>
+ BrowserTestUtils.isVisible(categoryItem)
+ );
+
+ ok(
+ BrowserTestUtils.isVisible(categoryItem),
+ "Fingerprinting category item is visible"
+ );
+
+ // Click the fingerprinting category and wait until the fingerprinter view is
+ // shown.
+ let fingerprintersView = win.document.getElementById(
+ "protections-popup-fingerprintersView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ fingerprintersView,
+ "ViewShown"
+ );
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Fingerprinter view was shown");
+
+ // Ensure the fingerprinter is listed on the tracker list.
+ let listItems = Array.from(
+ fingerprintersView.querySelectorAll(".protections-popup-list-item")
+ );
+ is(listItems.length, 1, "We have 1 fingerprinter in the list");
+
+ let listItem = listItems.find(
+ item => item.querySelector("label").value == "https://example.org"
+ );
+ ok(listItem, "Has an item for example.org");
+ ok(BrowserTestUtils.isVisible(listItem), "List item is visible");
+
+ // Back to the popup main view.
+ let mainView = win.document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = fingerprintersView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ await closeProtectionsPanel(win);
+ }
+ );
+});
+
+// Verify the case where the fingerprinting protection is only enabled in PBM.
+add_task(async function testFingerprintingSubviewInPBM() {
+ // Only enabled fingerprinting protection in PBM.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FINGERPRINT_PROTECTION_PREF, false],
+ [FINGERPRINT_PROTECTION_PBM_PREF, true],
+ ],
+ });
+
+ // Verify that fingerprinting category isn't shown on a normal window.
+ await openTestPage(
+ [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
+ false,
+ testCategoryNotShown
+ );
+
+ // Verify that fingerprinting category is shown on a private window.
+ await openTestPage(
+ [TEST_3RD_CANVAS_FP_PAGE, TEST_3RD_FONT_FP_PAGE],
+ true,
+ async (win, _) => {
+ await openProtectionsPanel(false, win);
+
+ let categoryItem = win.document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await BrowserTestUtils.waitForMutationCondition(categoryItem, {}, () =>
+ BrowserTestUtils.isVisible(categoryItem)
+ );
+
+ ok(
+ BrowserTestUtils.isVisible(categoryItem),
+ "Fingerprinting category item is visible"
+ );
+
+ // Click the fingerprinting category and wait until the fingerprinter view is
+ // shown.
+ let fingerprintersView = win.document.getElementById(
+ "protections-popup-fingerprintersView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ fingerprintersView,
+ "ViewShown"
+ );
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Fingerprinter view was shown");
+
+ // Ensure the fingerprinter is listed on the tracker list.
+ let listItems = Array.from(
+ fingerprintersView.querySelectorAll(".protections-popup-list-item")
+ );
+ is(listItems.length, 1, "We have 1 fingerprinter in the list");
+
+ let listItem = listItems.find(
+ item => item.querySelector("label").value == "https://example.org"
+ );
+ ok(listItem, "Has an item for example.org");
+ ok(BrowserTestUtils.isVisible(listItem), "List item is visible");
+
+ // Back to the popup main view.
+ let mainView = win.document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = fingerprintersView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ await closeProtectionsPanel(win);
+ }
+ );
+});
+
+// Verify that fingerprinting category will be shown after loading
+// fingerprinters.
+add_task(async function testDynamicallyLoadFingerprinter() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FINGERPRINT_PROTECTION_PREF, true]],
+ });
+
+ await openTestPage([TEST_3RD_FONT_FP_PAGE], false, async (win, browser) => {
+ await openProtectionsPanel(false, win);
+
+ let categoryItem = win.document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ // The fingerprinting category should have the 'notFound' class to indicate
+ // that no suspicious fingerprinter was found in the page.
+ ok(
+ notFound("protections-popup-category-fingerprinters"),
+ "Fingerprinting category is not found"
+ );
+
+ ok(
+ !BrowserTestUtils.isVisible(categoryItem),
+ "Fingerprinting category item is not visible"
+ );
+
+ // Add an iframe that triggers suspicious fingerprinting and wait until the
+ // content event files.
+
+ let contentBlockingEventPromise = waitForSuspiciousFingerprintingEvent(win);
+ await SpecialPowers.spawn(browser, [TEST_3RD_CANVAS_FP_PAGE], test_url => {
+ let ifr = content.document.createElement("iframe");
+
+ content.document.body.appendChild(ifr);
+ ifr.src = test_url;
+ });
+ await contentBlockingEventPromise;
+
+ // Click the fingerprinting category and wait until the fingerprinter view
+ // is shown.
+ let fingerprintersView = win.document.getElementById(
+ "protections-popup-fingerprintersView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ fingerprintersView,
+ "ViewShown"
+ );
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Fingerprinter view was shown");
+
+ // Ensure the fingerprinter is listed on the tracker list.
+ let listItems = Array.from(
+ fingerprintersView.querySelectorAll(".protections-popup-list-item")
+ );
+ is(listItems.length, 1, "We have 1 fingerprinter in the list");
+
+ let listItem = listItems.find(
+ item => item.querySelector("label").value == "https://example.org"
+ );
+ ok(listItem, "Has an item for example.org");
+ ok(BrowserTestUtils.isVisible(listItem), "List item is visible");
+
+ // Back to the popup main view.
+ let mainView = win.document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = fingerprintersView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ await closeProtectionsPanel(win);
+ });
+});
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..22decca636
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js
@@ -0,0 +1,144 @@
+/* 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 l10n = new Localization(["browser/siteProtections.ftl"]);
+
+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.isVisible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.isVisible(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");
+
+ const header = trackersView.querySelector(".panel-header > h1 > span");
+ const headerL10nId = blocked
+ ? "protections-blocking-tracking-content"
+ : "protections-not-blocking-tracking-content";
+ const [headerMsg] = await l10n.formatMessages([headerL10nId]);
+ const expHeader = headerMsg.attributes.find(a => a.name === "title").value;
+ is(header.textContent, expHeader, "Trackers view header is correct");
+
+ 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.isVisible(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.isVisible(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/canvas-fingerprinter.html b/browser/base/content/test/protectionsUI/canvas-fingerprinter.html
new file mode 100644
index 0000000000..d48d289529
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/canvas-fingerprinter.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<head>
+ <title>A page containing a canvas fingerprinter</title>
+</head>
+<body>
+ <canvas width=200 height=200>
+ </canvas>
+ <script>
+ var canvas = document.querySelector("canvas");
+ var context = canvas.getContext("2d");
+
+ context.fillStyle = "rgb(100, 210, 0)";
+ context.fillRect(100, 10, 50, 50);
+ context.fillStyle = "#f65";
+ context.font = "16pt Arial";
+ context.fillText("<@nv45. F1n63r,Pr1n71n6!", 20, 40);
+
+ var data = canvas.toDataURL();
+ </script>
+</body>
+</html>
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/font-fingerprinter.html b/browser/base/content/test/protectionsUI/font-fingerprinter.html
new file mode 100644
index 0000000000..efc71aa999
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/font-fingerprinter.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<html>
+<head>
+ <title>A page containing a font fingerprinter</title>
+</head>
+<body>
+<script>
+/*
+* Based on https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js
+* (Archived: https://web.archive.org/web/20150706050408/https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js#L233)
+*
+* Fingerprintjs2 0.1.4 - Modern & flexible browser fingerprint library v2
+* https://github.com/Valve/fingerprintjs2
+* Copyright (c) 2015 Valentin Vasilyev (valentin.vasilyev@outlook.com)
+* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+*/
+// kudos to http://www.lalit.org/lab/javascript-css-font-detect/
+function getFonts() {
+ // a font will be compared against all the three default fonts.
+ // and if it doesn't match all 3 then that font is not available.
+ var baseFonts = ["monospace", "sans-serif", "serif"];
+
+ //we use m or w because these two characters take up the maximum width.
+ // And we use a LLi so that the same matching fonts can get separated
+ var testString = "mmmmmmmmmmlli";
+
+ //we test using 72px font size, we may use any size. I guess larger the better.
+ var testSize = "72px";
+
+ var h = document.getElementsByTagName("body")[0];
+
+ // create a SPAN in the document to get the width of the text we use to test
+ var s = document.createElement("span");
+ s.style.fontSize = testSize;
+ s.innerHTML = testString;
+ var defaultWidth = {};
+ var defaultHeight = {};
+ for (var index in baseFonts) {
+ //get the default width for the three base fonts
+ s.style.fontFamily = baseFonts[index];
+ h.appendChild(s);
+ defaultWidth[baseFonts[index]] = s.offsetWidth; //width for the default font
+ defaultHeight[baseFonts[index]] = s.offsetHeight; //height for the defualt font
+ h.removeChild(s);
+ }
+ var detect = function (font) {
+ var detected = false;
+ for (var idx in baseFonts) {
+ s.style.fontFamily = font + "," + baseFonts[idx]; // name of the font along with the base font for fallback.
+ h.appendChild(s);
+ var matched = (s.offsetWidth !== defaultWidth[baseFonts[idx]] || s.offsetHeight !== defaultHeight[baseFonts[idx]]);
+ h.removeChild(s);
+ detected = detected || matched;
+ }
+ return detected;
+ };
+ var fontList = [
+ "Abadi MT Condensed Light", "Academy Engraved LET", "ADOBE CASLON PRO", "Adobe Garamond", "ADOBE GARAMOND PRO", "Agency FB", "Aharoni", "Albertus Extra Bold", "Albertus Medium", "Algerian", "Amazone BT", "American Typewriter",
+ "American Typewriter Condensed", "AmerType Md BT", "Andale Mono", "Andalus", "Angsana New", "AngsanaUPC", "Antique Olive", "Aparajita", "Apple Chancery", "Apple Color Emoji", "Apple SD Gothic Neo", "Arabic Typesetting", "ARCHER", "Arial", "Arial Black", "Arial Hebrew",
+ "Arial MT", "Arial Narrow", "Arial Rounded MT Bold", "Arial Unicode MS", "ARNO PRO", "Arrus BT", "Aurora Cn BT", "AvantGarde Bk BT", "AvantGarde Md BT", "AVENIR", "Ayuthaya", "Bandy", "Bangla Sangam MN", "Bank Gothic", "BankGothic Md BT", "Baskerville",
+ "Baskerville Old Face", "Batang", "BatangChe", "Bauer Bodoni", "Bauhaus 93", "Bazooka", "Bell MT", "Bembo", "Benguiat Bk BT", "Berlin Sans FB", "Berlin Sans FB Demi", "Bernard MT Condensed", "BernhardFashion BT", "BernhardMod BT", "Big Caslon", "BinnerD",
+ "Bitstream Vera Sans Mono", "Blackadder ITC", "BlairMdITC TT", "Bodoni 72", "Bodoni 72 Oldstyle", "Bodoni 72 Smallcaps", "Bodoni MT", "Bodoni MT Black", "Bodoni MT Condensed", "Bodoni MT Poster Compressed", "Book Antiqua", "Bookman Old Style",
+ "Bookshelf Symbol 7", "Boulder", "Bradley Hand", "Bradley Hand ITC", "Bremen Bd BT", "Britannic Bold", "Broadway", "Browallia New", "BrowalliaUPC", "Brush Script MT", "Calibri", "Californian FB", "Calisto MT", "Calligrapher", "Cambria", "Cambria Math", "Candara",
+ "CaslonOpnface BT", "Castellar", "Centaur", "Century", "Century Gothic", "Century Schoolbook", "Cezanne", "CG Omega", "CG Times", "Chalkboard", "Chalkboard SE", "Chalkduster", "Charlesworth", "Charter Bd BT", "Charter BT", "Chaucer",
+ "ChelthmITC Bk BT", "Chiller", "Clarendon", "Clarendon Condensed", "CloisterBlack BT", "Cochin", "Colonna MT", "Comic Sans", "Comic Sans MS", "Consolas", "Constantia", "Cooper Black", "Copperplate", "Copperplate Gothic", "Copperplate Gothic Bold",
+ "Copperplate Gothic Light", "CopperplGoth Bd BT", "Corbel", "Cordia New", "CordiaUPC", "Cornerstone", "Coronet", "Courier", "Courier New", "Cuckoo", "Curlz MT", "DaunPenh", "Dauphin", "David", "DB LCD Temp", "DELICIOUS", "Denmark", "Devanagari Sangam MN",
+ "DFKai-SB", "Didot", "DilleniaUPC", "DIN", "DokChampa", "Dotum", "DotumChe", "Ebrima", "Edwardian Script ITC", "Elephant", "English 111 Vivace BT", "Engravers MT", "EngraversGothic BT", "Eras Bold ITC", "Eras Demi ITC", "Eras Light ITC", "Eras Medium ITC",
+ "Estrangelo Edessa", "EucrosiaUPC", "Euphemia", "Euphemia UCAS", "EUROSTILE", "Exotc350 Bd BT", "FangSong", "Felix Titling", "Fixedsys", "FONTIN", "Footlight MT Light", "Forte", "Franklin Gothic", "Franklin Gothic Book", "Franklin Gothic Demi",
+ "Franklin Gothic Demi Cond", "Franklin Gothic Heavy", "Franklin Gothic Medium", "Franklin Gothic Medium Cond", "FrankRuehl", "Fransiscan", "Freefrm721 Blk BT", "FreesiaUPC", "Freestyle Script", "French Script MT", "FrnkGothITC Bk BT", "Fruitger", "FRUTIGER",
+ "Futura", "Futura Bk BT", "Futura Lt BT", "Futura Md BT", "Futura ZBlk BT", "FuturaBlack BT", "Gabriola", "Galliard BT", "Garamond", "Gautami", "Geeza Pro", "Geneva", "Geometr231 BT", "Geometr231 Hv BT", "Geometr231 Lt BT", "Georgia", "GeoSlab 703 Lt BT",
+ "GeoSlab 703 XBd BT", "Gigi", "Gill Sans", "Gill Sans MT", "Gill Sans MT Condensed", "Gill Sans MT Ext Condensed Bold", "Gill Sans Ultra Bold", "Gill Sans Ultra Bold Condensed", "Gisha", "Gloucester MT Extra Condensed", "GOTHAM", "GOTHAM BOLD",
+ "Goudy Old Style", "Goudy Stout", "GoudyHandtooled BT", "GoudyOLSt BT", "Gujarati Sangam MN", "Gulim", "GulimChe", "Gungsuh", "GungsuhChe", "Gurmukhi MN", "Haettenschweiler", "Harlow Solid Italic", "Harrington", "Heather", "Heiti SC", "Heiti TC", "HELV", "Helvetica",
+ "Helvetica Neue", "Herald", "High Tower Text", "Hiragino Kaku Gothic ProN", "Hiragino Mincho ProN", "Hoefler Text", "Humanst 521 Cn BT", "Humanst521 BT", "Humanst521 Lt BT", "Impact", "Imprint MT Shadow", "Incised901 Bd BT", "Incised901 BT",
+ "Incised901 Lt BT", "INCONSOLATA", "Informal Roman", "Informal011 BT", "INTERSTATE", "IrisUPC", "Iskoola Pota", "JasmineUPC", "Jazz LET", "Jenson", "Jester", "Jokerman", "Juice ITC", "Kabel Bk BT", "Kabel Ult BT", "Kailasa", "KaiTi", "Kalinga", "Kannada Sangam MN",
+ "Kartika", "Kaufmann Bd BT", "Kaufmann BT", "Khmer UI", "KodchiangUPC", "Kokila", "Korinna BT", "Kristen ITC", "Krungthep", "Kunstler Script", "Lao UI", "Latha", "Leelawadee", "Letter Gothic", "Levenim MT", "LilyUPC", "Lithograph", "Lithograph Light", "Long Island",
+ "Lucida Bright", "Lucida Calligraphy", "Lucida Console", "Lucida Fax", "LUCIDA GRANDE", "Lucida Handwriting", "Lucida Sans", "Lucida Sans Typewriter", "Lucida Sans Unicode", "Lydian BT", "Magneto", "Maiandra GD", "Malayalam Sangam MN", "Malgun Gothic",
+ "Mangal", "Marigold", "Marion", "Marker Felt", "Market", "Marlett", "Matisse ITC", "Matura MT Script Capitals", "Meiryo", "Meiryo UI", "Microsoft Himalaya", "Microsoft JhengHei", "Microsoft New Tai Lue", "Microsoft PhagsPa", "Microsoft Sans Serif", "Microsoft Tai Le",
+ "Microsoft Uighur", "Microsoft YaHei", "Microsoft Yi Baiti", "MingLiU", "MingLiU_HKSCS", "MingLiU_HKSCS-ExtB", "MingLiU-ExtB", "Minion", "Minion Pro", "Miriam", "Miriam Fixed", "Mistral", "Modern", "Modern No. 20", "Mona Lisa Solid ITC TT", "Monaco", "Mongolian Baiti",
+ "MONO", "Monotype Corsiva", "MoolBoran", "Mrs Eaves", "MS Gothic", "MS LineDraw", "MS Mincho", "MS Outlook", "MS PGothic", "MS PMincho", "MS Reference Sans Serif", "MS Reference Specialty", "MS Sans Serif", "MS Serif", "MS UI Gothic", "MT Extra", "MUSEO", "MV Boli", "MYRIAD",
+ "MYRIAD PRO", "Nadeem", "Narkisim", "NEVIS", "News Gothic", "News GothicMT", "NewsGoth BT", "Niagara Engraved", "Niagara Solid", "Noteworthy", "NSimSun", "Nyala", "OCR A Extended", "Old Century", "Old English Text MT", "Onyx", "Onyx BT", "OPTIMA", "Oriya Sangam MN",
+ "OSAKA", "OzHandicraft BT", "Palace Script MT", "Palatino", "Palatino Linotype", "Papyrus", "Parchment", "Party LET", "Pegasus", "Perpetua", "Perpetua Titling MT", "PetitaBold", "Pickwick", "Plantagenet Cherokee", "Playbill", "PMingLiU", "PMingLiU-ExtB",
+ "Poor Richard", "Poster", "PosterBodoni BT", "PRINCETOWN LET", "Pristina", "PTBarnum BT", "Pythagoras", "Raavi", "Rage Italic", "Ravie", "Ribbon131 Bd BT", "Rockwell", "Rockwell Condensed", "Rockwell Extra Bold", "Rod", "Roman", "Sakkal Majalla",
+ "Santa Fe LET", "Savoye LET", "Sceptre", "Script", "Script MT Bold", "SCRIPTINA", "Segoe Print", "Segoe Script", "Segoe UI", "Segoe UI Light", "Segoe UI Semibold", "Segoe UI Symbol", "Serifa", "Serifa BT", "Serifa Th BT", "ShelleyVolante BT", "Sherwood",
+ "Shonar Bangla", "Showcard Gothic", "Shruti", "Signboard", "SILKSCREEN", "SimHei", "Simplified Arabic", "Simplified Arabic Fixed", "SimSun", "SimSun-ExtB", "Sinhala Sangam MN", "Sketch Rockwell", "Skia", "Small Fonts", "Snap ITC", "Snell Roundhand", "Socket",
+ "Souvenir Lt BT", "Staccato222 BT", "Steamer", "Stencil", "Storybook", "Styllo", "Subway", "Swis721 BlkEx BT", "Swiss911 XCm BT", "Sylfaen", "Synchro LET", "System", "Tahoma", "Tamil Sangam MN", "Technical", "Teletype", "Telugu Sangam MN", "Tempus Sans ITC",
+ "Terminal", "Thonburi", "Times", "Times New Roman", "Times New Roman PS", "Traditional Arabic", "Trajan", "TRAJAN PRO", "Trebuchet MS", "Tristan", "Tubular", "Tunga", "Tw Cen MT", "Tw Cen MT Condensed", "Tw Cen MT Condensed Extra Bold",
+ "TypoUpright BT", "Unicorn", "Univers", "Univers CE 55 Medium", "Univers Condensed", "Utsaah", "Vagabond", "Vani", "Verdana", "Vijaya", "Viner Hand ITC", "VisualUI", "Vivaldi", "Vladimir Script", "Vrinda", "Westminster", "WHITNEY", "Wide Latin", "Wingdings",
+ "Wingdings 2", "Wingdings 3", "ZapfEllipt BT", "ZapfHumnst BT", "ZapfHumnst Dm BT", "Zapfino", "Zurich BlkEx BT", "Zurich Ex BT", "ZWAdobeF"];
+ var available = [];
+ for (var i = 0, l = fontList.length; i < l; i++) {
+ if(detect(fontList[i])) {
+ available.push(fontList[i]);
+ }
+ }
+ return available;
+}
+
+let fonts = getFonts();
+console.log("detected fonts:", fonts);
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/head.js b/browser/base/content/test/protectionsUI/head.js
new file mode 100644
index 0000000000..98e5063129
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/head.js
@@ -0,0 +1,247 @@
+/* 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"
+);
+
+ChromeUtils.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 waitForProtectionsPanelToast() {
+ await BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ Assert.ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+ "Protections panel toast is shown."
+ );
+
+ await BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+}
+
+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"
+ );
+
+ // Register a promise to wait for the tooltip to be shown.
+ let tooltip = win.document.getElementById("tracking-protection-icon-tooltip");
+ let tooltipShownPromise = BrowserTestUtils.waitForPopupEvent(
+ tooltip,
+ "shown"
+ );
+
+ // 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
+ );
+
+ // Wait for the tooltip to be shown.
+ await tooltipShownPromise;
+
+ 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.startLoadingURIString(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.toml b/browser/base/content/test/referrer/browser.toml
new file mode 100644
index 0000000000..ecf28158a6
--- /dev/null
+++ b/browser/base/content/test/referrer/browser.toml
@@ -0,0 +1,44 @@
+[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..cabbc19151
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver.sjs
@@ -0,0 +1,40 @@
+/**
+ * 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) {
+ 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..6c5c2c7db2
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs
@@ -0,0 +1,40 @@
+/**
+ * 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) {
+ 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.toml b/browser/base/content/test/sanitize/browser.toml
new file mode 100644
index 0000000000..814dc54c3d
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser.toml
@@ -0,0 +1,39 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "dummy.js",
+ "dummy_page.html",
+ "site_data_test.html",
+]
+
+["browser_cookiePermission.js"]
+
+["browser_cookiePermission_aboutURL.js"]
+
+["browser_cookiePermission_containers.js"]
+
+["browser_cookiePermission_subDomains.js"]
+
+["browser_cookiePermission_subDomains_v2.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"]
+
+["browser_sanitizeDialog_v2.js"]
+
+["browser_sanitizeOnShutdown_migration.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..ada8286437
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js
@@ -0,0 +1,111 @@
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+// We will be removing the ["cookies","offlineApps"] option once we remove the
+// old clear history dialog in Bug 1856418 - Remove all old clear data dialog boxes
+let prefs = [["cookiesAndStorage"], ["cookies", "offlineApps"]];
+
+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);
+ };
+ });
+}
+
+for (let itemsToClear of prefs) {
+ 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(itemsToClear);
+
+ 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(itemsToClear);
+
+ 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_cookiePermission_subDomains_v2.js b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains_v2.js
new file mode 100644
index 0000000000..d1887db91b
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains_v2.js
@@ -0,0 +1,288 @@
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ["privacy.clearOnShutdown.cookiesAndStorage", true],
+ ["privacy.clearOnShutdown.cache", false],
+ ["privacy.clearOnShutdown.historyAndFormData", false],
+ ["privacy.clearOnShutdown.downloads", false],
+ ["privacy.clearOnShutdown.siteSettings", false],
+ ["browser.sanitizer.loglevel", "All"],
+ ["privacy.sanitize.useOldClearHistoryDialog", false],
+ ],
+ });
+});
+// 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..5ad7b78d69
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js
@@ -0,0 +1,86 @@
+/* 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";
+
+// We will be removing the ["history"] option once we remove the
+// old clear history dialog in Bug 1856418 - Remove all old clear data dialog boxes
+let prefs = [["history"], ["historyFormDataAndDownloads"]];
+
+for (let itemsToClear of prefs) {
+ 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(itemsToClear);
+
+ 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..ae043dbd62
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-formhistory.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/. */
+
+add_task(async function test() {
+ let prefs = ["history", "historyFormDataAndDownloads"];
+
+ for (let pref of prefs) {
+ // 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([pref]);
+ 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..e003762f35
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-history.js
@@ -0,0 +1,136 @@
+/* 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() {
+ let categories = ["history", "historyFormDataAndDownloads"];
+
+ for (let pref of categories) {
+ 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([pref], {
+ // 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([pref]);
+
+ 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([pref, "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..2dfc62c01f
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
@@ -0,0 +1,249 @@
+/* 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;
+
+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);
+ });
+}
+
+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;
+}
+
+// We will be removing the ["cookies","offlineApps"] option once we remove the
+// old clear history dialog in Bug 1856418 - Remove all old clear data dialog boxes
+let prefs = [["cookiesAndStorage"], ["cookies", "offlineApps"]];
+
+for (let itemsToClear of prefs) {
+ 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 createDummyDataForHost("example.org");
+ await createDummyDataForHost("example.com");
+
+ endDate = Date.now() * 1000;
+ principals = sas.getActiveOrigins(endDate - oneHour, endDate);
+ ok(!!principals, "We have an active origin.");
+ Assert.greaterOrEqual(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 createDummyDataForHost("example.org");
+ await createDummyDataForHost("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_sanitize-timespans_v2.js b/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js
new file mode 100644
index 0000000000..c732262a1a
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js
@@ -0,0 +1,1190 @@
+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
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.sanitize.useOldClearHistoryDialog", false]],
+ });
+
+ let itemsToClear = ["historyAndFormData", "downloads"];
+
+ 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(itemsToClear, { 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(itemsToClear, { 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(itemsToClear, { 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(itemsToClear, { 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(itemsToClear, { 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(itemsToClear, { 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(itemsToClear, { 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(itemsToClear, {
+ ignoreTimespan: false,
+ });
+ Assert.deepEqual(progress, {
+ historyAndFormData: "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(itemsToClear, { 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..2df7d83c6e
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitizeDialog.js
@@ -0,0 +1,837 @@
+/* -*- 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();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.sanitize.useOldClearHistoryDialog", true]],
+ });
+});
+
+/**
+ * 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]");
+ Assert.greater(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/browser_sanitizeDialog_v2.js b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js
new file mode 100644
index 0000000000..29f760f57f
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js
@@ -0,0 +1,1429 @@
+/* -*- 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",
+ PermissionTestUtils: "resource://testing-common/PermissionTestUtils.sys.mjs",
+ FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+});
+
+const kMsecPerMin = 60 * 1000;
+const kUsecPerMin = 60 * 1000000;
+let today = Date.now() - new Date().setHours(0, 0, 0, 0);
+let nowMSec = Date.now();
+let nowUSec = nowMSec * 1000;
+let fileURL;
+
+const TEST_TARGET_FILE_NAME = "test-download.txt";
+const TEST_QUOTA_USAGE_HOST = "example.com";
+const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+const TEST_QUOTA_USAGE_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_QUOTA_USAGE_ORIGIN
+ ) + "site_data_test.html";
+
+const siteOrigins = [
+ "https://www.example.com",
+ "https://example.org",
+ "http://localhost:8000",
+ "http://localhost:3000",
+];
+
+/**
+ * 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`
+ );
+ }
+}
+
+/**
+ * Ensures that the given pref is the expected value.
+ *
+ * @param {String} aPrefName
+ * The pref's sub-branch under the privacy branch
+ * @param {Boolean} aExpectedVal
+ * The pref's expected value
+ * @param {String} 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"
+ );
+ }
+}
+
+/**
+ * Checks if a form entry exists.
+ */
+async function formNameExists(name) {
+ return !!(await FormHistory.count({ fieldname: 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 = nowUSec - aMinutesAgo * kUsecPerMin;
+
+ return FormHistory.update({
+ op: "add",
+ fieldname: name,
+ value: "dummy",
+ firstUsed: timestamp,
+ });
+}
+
+/**
+ * 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(nowMSec - 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 multiple downloads to the PUBLIC download list
+ */
+async function addToDownloadList() {
+ const url = createFileURL();
+ const downloadsList = await Downloads.getList(Downloads.PUBLIC);
+ let timeOptions = [1, 2, 4, 24, 128, 128];
+ let buffer = 100000;
+
+ for (let i = 0; i < timeOptions.length; i++) {
+ let timeDownloaded = 60 * kMsecPerMin * timeOptions[i];
+ if (timeOptions[i] === 24) {
+ timeDownloaded = today;
+ }
+
+ let download = await Downloads.createDownload({
+ source: { url: url.spec, isPrivate: false },
+ target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path },
+ startTime: {
+ getTime: _ => {
+ return nowMSec - timeDownloaded + buffer;
+ },
+ },
+ });
+
+ Assert.ok(!!download);
+ downloadsList.add(download);
+ }
+ let items = await downloadsList.getAll();
+ Assert.equal(items.length, 6, "Items were added to the list");
+}
+
+async function addToSiteUsage() {
+ // Fill indexedDB with test data.
+ // Don't wait for the page to load, to register the content event handler as quickly as possible.
+ // If this test goes intermittent, we might have to tell the page to wait longer before
+ // firing the event.
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false);
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "test-indexedDB-done",
+ false,
+ null,
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ let siteLastAccessed = [1, 2, 4, 24];
+
+ let staticUsage = 4096 * 6;
+ // Add a time buffer so the site access falls within the time range
+ const buffer = 10000;
+
+ // Change lastAccessed of sites
+ for (let index = 0; index < siteLastAccessed.length; index++) {
+ let lastAccessedTime = 60 * kMsecPerMin * siteLastAccessed[index];
+ if (siteLastAccessed[index] === 24) {
+ lastAccessedTime = today;
+ }
+
+ let site = SiteDataManager._testInsertSite(siteOrigins[index], {
+ quotaUsage: staticUsage,
+ lastAccessed: (nowMSec - lastAccessedTime + buffer) * 1000,
+ });
+ Assert.ok(site, "Site added successfully");
+ }
+}
+
+/**
+ * Helper function to create file URL to open
+ *
+ * @returns {Object} a file URL
+ */
+function createFileURL() {
+ if (!fileURL) {
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("foo.txt");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ fileURL = Services.io.newFileURI(file);
+ }
+ return fileURL;
+}
+
+add_setup(async function () {
+ requestLongerTimeout(3);
+ await blankSlate();
+ registerCleanupFunction(async function () {
+ await blankSlate();
+ await PlacesTestUtils.promiseAsyncUpdates();
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.sanitize.useOldClearHistoryDialog", false]],
+ });
+
+ // open preferences to trigger an updateSites()
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * 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();
+}
+
+/**
+ * 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.
+ * @param mode (optional)
+ * One of
+ * clear on shutdown settings context ("clearOnShutdown"),
+ * clear site data settings context ("clearSiteData"),
+ * clear history context ("clearHistory"),
+ * browser context ("browser")
+ * "browser" by default
+ */
+function DialogHelper(openContext = "browser") {
+ this._browserWin = window;
+ this.win = null;
+ this._mode = openContext;
+ 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 cb = this.win.document.querySelectorAll(
+ "checkbox[id='" + aPrefName + "']"
+ );
+ is(cb.length, 1, "found checkbox for " + aPrefName + " id");
+ if (cb[0].checked != aCheckState) {
+ cb[0].click();
+ }
+ },
+
+ /**
+ * @param {String} aCheckboxId
+ * The checkbox id name
+ * @param {Boolean} aCheckState
+ * True if the checkbox should be checked, false otherwise
+ */
+ validateCheckbox(aCheckboxId, aCheckState) {
+ let cb = this.win.document.querySelectorAll(
+ "checkbox[id='" + aCheckboxId + "']"
+ );
+ is(cb.length, 1, `found checkbox for id=${aCheckboxId}`);
+ is(
+ cb[0].checked,
+ aCheckState,
+ `checkbox for ${aCheckboxId} is ${aCheckState}`
+ );
+ },
+
+ /**
+ * Makes sure all the checkboxes are checked.
+ */
+ _checkAllCheckboxesCustom(check) {
+ var cb = this.win.document.querySelectorAll(".clearingItemCheckbox");
+ ok(cb.length, "found checkboxes for ids");
+ for (var i = 0; i < cb.length; ++i) {
+ if (cb[i].checked != 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_v2.xhtml",
+ {
+ isSubDialog: true,
+ }
+ );
+
+ // We want to simulate opening the dialog inside preferences for clear history
+ // and clear site data
+ if (this._mode != "browser") {
+ await openPreferencesViaOpenPreferencesAPI("privacy", {
+ leaveOpen: true,
+ });
+ let tabWindow = gBrowser.selectedBrowser.contentWindow;
+ let clearDialogOpenButtonId = this._mode + "Button";
+ // the id for clear on shutdown is of a different format
+ if (this._mode == "clearOnShutdown") {
+ // set always clear to true to enable the clear on shutdown dialog
+ let enableSettingsCheckbox =
+ tabWindow.document.getElementById("alwaysClear");
+ if (!enableSettingsCheckbox.checked) {
+ enableSettingsCheckbox.click();
+ }
+ clearDialogOpenButtonId = "clearDataSettings";
+ }
+ // open dialog
+ tabWindow.document.getElementById(clearDialogOpenButtonId).click();
+ }
+ // We open the dialog in the chrome context in other cases
+ else {
+ executeSoon(() => {
+ Sanitizer.showUI(this._browserWin, this._mode);
+ });
+ }
+
+ this.win = await dialogPromise;
+ this.win.addEventListener(
+ "load",
+ () => {
+ // Run onload on next tick so that gSanitizePromptDialog.init can run first.
+ executeSoon(async () => {
+ await this.win.gSanitizePromptDialog.dataSizesFinishedUpdatingPromise;
+ 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();
+ }
+ if (this._mode != "browser") {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ 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"
+ );
+ }
+ },
+};
+
+/**
+ * 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 nowUSec - aMinutesAgo * kUsecPerMin;
+}
+
+function promiseSanitizationComplete() {
+ return TestUtils.topicObserved("sanitizer-sanitization-complete");
+}
+
+/**
+ * Helper function to validate the data sizes shown for each time selection
+ *
+ * @param {DialogHelper} dh - dialog object to access window and timespan
+ */
+async function validateDataSizes(dialogHelper) {
+ let timespans = [
+ "TIMESPAN_HOUR",
+ "TIMESPAN_2HOURS",
+ "TIMESPAN_4HOURS",
+ "TIMESPAN_TODAY",
+ "TIMESPAN_EVERYTHING",
+ ];
+
+ // get current data sizes from siteDataManager
+ let cacheUsage = await SiteDataManager.getCacheSize();
+ let quotaUsage = await SiteDataManager.getQuotaUsageForTimeRanges(timespans);
+
+ for (let i = 0; i < timespans.length; i++) {
+ // select timespan to check
+ dialogHelper.selectDuration(Sanitizer[timespans[i]]);
+
+ // get the elements
+ let clearCookiesAndSiteDataCheckbox =
+ dialogHelper.win.document.getElementById("cookiesAndStorage");
+ let clearCacheCheckbox = dialogHelper.win.document.getElementById("cache");
+
+ let [convertedQuotaUsage] = DownloadUtils.convertByteUnits(
+ quotaUsage[timespans[i]]
+ );
+ let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage);
+
+ // Ensure l10n is finished before inspecting the category labels.
+ await dialogHelper.win.document.l10n.translateElements([
+ clearCookiesAndSiteDataCheckbox,
+ clearCacheCheckbox,
+ ]);
+ ok(
+ clearCacheCheckbox.label.includes(convertedCacheUnit),
+ "Should show the cache usage"
+ );
+ ok(
+ clearCookiesAndSiteDataCheckbox.label.includes(convertedQuotaUsage),
+ `Should show the quota usage as ${convertedQuotaUsage}`
+ );
+ }
+}
+
+/**
+ *
+ * Opens dialog in the provided context and selects the checkboxes
+ * as sent in the parameters
+ *
+ * @param {Object} context the dialog is opened in, timespan to select,
+ * if historyFormDataAndDownloads, cookiesAndStorage, cache or siteSettings
+ * are checked
+ */
+async function performActionsOnDialog({
+ context = "browser",
+ timespan = Sanitizer.TIMESPAN_HOUR,
+ historyFormDataAndDownloads = true,
+ cookiesAndStorage = true,
+ cache = false,
+ siteSettings = false,
+}) {
+ let dh = new DialogHelper(context);
+ dh.onload = function () {
+ this.selectDuration(timespan);
+ this.checkPrefCheckbox(
+ "historyFormDataAndDownloads",
+ historyFormDataAndDownloads
+ );
+ this.checkPrefCheckbox("cookiesAndStorage", cookiesAndStorage);
+ this.checkPrefCheckbox("cache", cache);
+ this.checkPrefCheckbox("siteSettings", siteSettings);
+ this.acceptDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+}
+
+/**
+ * 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("historyFormDataAndDownloads", 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 "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("historyFormDataAndDownloads", 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("historyFormDataAndDownloads", 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;
+});
+
+/**
+ * Tests that the clearing button gets disabled if no checkboxes are checked
+ * and enabled when at least one checkbox is checked
+ */
+add_task(async function testAcceptButtonDisabled() {
+ let dh = new DialogHelper();
+ dh.onload = async function () {
+ let clearButton = this.win.document
+ .querySelector("dialog")
+ .getButton("accept");
+ this.uncheckAllCheckboxes();
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ is(clearButton.disabled, true, "Clear button should be disabled");
+ // await BrowserTestUtils.waitForMutationCondition(
+ // clearButton,
+ // { attributes: true },
+ // () => clearButton.disabled,
+ // "Clear button should be disabled"
+ // );
+
+ this.checkPrefCheckbox("cache", true);
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ is(clearButton.disabled, false, "Clear button should not be disabled");
+
+ this.cancelDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Tests to see if the warning box is hidden when opened in the clear on shutdown context
+ */
+add_task(async function testWarningBoxInClearOnShutdown() {
+ let dh = new DialogHelper("clearSiteData");
+ dh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ is(
+ BrowserTestUtils.isVisible(this.getWarningPanel()),
+ true,
+ `warning panel should be visible`
+ );
+ this.acceptDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+
+ dh = new DialogHelper("clearOnShutdown");
+ dh.onload = function () {
+ is(
+ BrowserTestUtils.isVisible(this.getWarningPanel()),
+ false,
+ `warning panel should not be visible`
+ );
+
+ this.cancelDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Checks if clearing history and downloads for the simple timespan
+ * behaves as expected
+ */
+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("historyFormDataAndDownloads", true);
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ intPrefIs(
+ "sanitize.timeSpan",
+ Sanitizer.TIMESPAN_HOUR,
+ "timeSpan pref should be hour after accepting dialog with " +
+ "hour selected"
+ );
+
+ 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;
+});
+
+/**
+ * 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 () {
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[id='historyFormDataAndDownloads']"
+ );
+ 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 () {
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[id='historyFormDataAndDownloads']"
+ );
+ 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 () {
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[id='historyFormDataAndDownloads']"
+ );
+ is(cb.length, 1, "There is only one checkbox for history and 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;
+});
+
+add_task(async function test_cookie_sizes() {
+ await clearAndValidateDataSizes({
+ clearCookies: true,
+ clearCache: false,
+ clearDownloads: false,
+ timespan: Sanitizer.TIMESPAN_HOUR,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: true,
+ clearCache: false,
+ clearDownloads: false,
+ timespan: Sanitizer.TIMESPAN_4HOURS,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: true,
+ clearCache: false,
+ clearDownloads: false,
+ timespan: Sanitizer.TIMESPAN_EVERYTHING,
+ });
+});
+
+add_task(async function test_cache_sizes() {
+ await clearAndValidateDataSizes({
+ clearCookies: false,
+ clearCache: true,
+ clearDownloads: false,
+ timespan: Sanitizer.TIMESPAN_HOUR,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: false,
+ clearCache: true,
+ clearDownloads: false,
+ timespan: Sanitizer.TIMESPAN_4HOURS,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: false,
+ clearCache: true,
+ clearDownloads: false,
+ timespan: Sanitizer.TIMESPAN_EVERYTHING,
+ });
+});
+
+add_task(async function test_downloads_sizes() {
+ await clearAndValidateDataSizes({
+ clearCookies: false,
+ clearCache: false,
+ clearDownloads: true,
+ timespan: Sanitizer.TIMESPAN_HOUR,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: false,
+ clearCache: false,
+ clearDownloads: true,
+ timespan: Sanitizer.TIMESPAN_4HOURS,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: false,
+ clearCache: false,
+ clearDownloads: true,
+ timespan: Sanitizer.TIMESPAN_EVERYTHING,
+ });
+});
+
+add_task(async function test_all_data_sizes() {
+ await clearAndValidateDataSizes({
+ clearCookies: true,
+ clearCache: true,
+ clearDownloads: true,
+ timespan: Sanitizer.TIMESPAN_HOUR,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: true,
+ clearCache: true,
+ clearDownloads: true,
+ timespan: Sanitizer.TIMESPAN_4HOURS,
+ });
+ await clearAndValidateDataSizes({
+ clearCookies: true,
+ clearCache: true,
+ clearDownloads: true,
+ timespan: Sanitizer.TIMESPAN_EVERYTHING,
+ });
+});
+
+// test the case when we open the dialog through the clear on shutdown settings
+add_task(async function test_clear_on_shutdown() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.sanitize.sanitizeOnShutdown", true]],
+ });
+
+ let dh = new DialogHelper("clearOnShutdown");
+ dh.onload = async function () {
+ this.uncheckAllCheckboxes();
+ this.checkPrefCheckbox("historyFormDataAndDownloads", false);
+ this.checkPrefCheckbox("cookiesAndStorage", true);
+ this.acceptDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+
+ // 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);
+ }
+
+ boolPrefIs(
+ "clearOnShutdown_v2.historyFormDataAndDownloads",
+ false,
+ "clearOnShutdown_v2 history should be false"
+ );
+
+ boolPrefIs(
+ "clearOnShutdown_v2.cookiesAndStorage",
+ true,
+ "clearOnShutdown_v2 cookies should be true"
+ );
+
+ boolPrefIs(
+ "clearOnShutdown_v2.cache",
+ false,
+ "clearOnShutdown_v2 cache should be false"
+ );
+
+ await createDummyDataForHost("example.org");
+ await createDummyDataForHost("example.com");
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.org"),
+ "We have indexedDB data for example.org"
+ );
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.com"),
+ "We have indexedDB data for example.com"
+ );
+
+ // Cleaning up
+ await Sanitizer.runSanitizeOnShutdown();
+
+ // Data for example.org should be cleared
+ ok(
+ !(await SiteDataTestUtils.hasIndexedDB("https://example.org")),
+ "We don't have indexedDB 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"
+ );
+
+ // Downloads shouldn't have cleared
+ await ensureDownloadsClearedState(downloadIDs, false);
+ await ensureDownloadsClearedState(olderDownloadIDs, false);
+
+ dh = new DialogHelper("clearOnShutdown");
+ dh.onload = async function () {
+ this.uncheckAllCheckboxes();
+ this.checkPrefCheckbox("historyFormDataAndDownloads", true);
+ this.acceptDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+
+ boolPrefIs(
+ "clearOnShutdown_v2.historyFormDataAndDownloads",
+ true,
+ "clearOnShutdown_v2 history should be true"
+ );
+
+ boolPrefIs(
+ "clearOnShutdown_v2.cookiesAndStorage",
+ false,
+ "clearOnShutdown_v2 cookies should be false"
+ );
+
+ boolPrefIs(
+ "clearOnShutdown_v2.cache",
+ false,
+ "clearOnShutdown_v2 cache should be false"
+ );
+
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.org"),
+ "We have indexedDB data for example.org"
+ );
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.com"),
+ "We have indexedDB data for example.com"
+ );
+
+ // Cleaning up
+ await Sanitizer.runSanitizeOnShutdown();
+
+ // Data for example.org should not be cleared
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.org"),
+ "We have indexedDB data for example.org"
+ );
+ // Data for example.com should not be cleared
+ ok(
+ await SiteDataTestUtils.hasIndexedDB("https://example.com"),
+ "We have indexedDB data for example.com"
+ );
+
+ // downloads should have cleared
+ await ensureDownloadsClearedState(downloadIDs, true);
+ await ensureDownloadsClearedState(olderDownloadIDs, true);
+
+ // Clean up
+ await SiteDataTestUtils.clear();
+});
+
+// test default prefs for entry points
+add_task(async function test_defaults_prefs() {
+ let dh = new DialogHelper("clearSiteData");
+ dh.onload = function () {
+ this.validateCheckbox("historyFormDataAndDownloads", false);
+ this.validateCheckbox("cache", true);
+ this.validateCheckbox("cookiesAndStorage", true);
+ this.validateCheckbox("siteSettings", false);
+
+ this.cancelDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+
+ // We don't need to specify the mode again,
+ // as the default mode is taken (browser, clear history)
+
+ dh = new DialogHelper();
+ dh.onload = function () {
+ // Default checked for browser and clear history mode
+ this.validateCheckbox("historyFormDataAndDownloads", true);
+ this.validateCheckbox("cache", true);
+ this.validateCheckbox("cookiesAndStorage", true);
+ this.validateCheckbox("siteSettings", false);
+
+ this.cancelDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
+
+/**
+ * Helper function to simulate switching timespan selections and
+ * validate data sizes before and after clearing
+ *
+ * @param {Object}
+ * clearCookies - boolean
+ * clearDownloads - boolean
+ * clearCaches - boolean
+ * timespan - one of Sanitizer.TIMESPAN_*
+ */
+async function clearAndValidateDataSizes({
+ clearCache,
+ clearDownloads,
+ clearCookies,
+ timespan,
+}) {
+ await blankSlate();
+
+ await addToDownloadList();
+ await addToSiteUsage();
+ let promiseSanitized = promiseSanitizationComplete();
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let dh = new DialogHelper();
+ dh.onload = async function () {
+ await validateDataSizes(this);
+ this.checkPrefCheckbox("cache", clearCache);
+ this.checkPrefCheckbox("cookiesAndStorage", clearCookies);
+ this.checkPrefCheckbox("historyFormDataAndDownloads", clearDownloads);
+ this.selectDuration(timespan);
+ this.acceptDialog();
+ };
+ dh.onunload = async function () {
+ await promiseSanitized;
+ };
+ dh.open();
+ await dh.promiseClosed;
+
+ let dh2 = new DialogHelper();
+ // Check if the newly cleared values are reflected
+ dh2.onload = async function () {
+ await validateDataSizes(this);
+ this.acceptDialog();
+ };
+ dh2.open();
+ await dh2.promiseClosed;
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_task(async function testEntryPointTelemetry() {
+ Services.fog.testResetFOG();
+
+ // Telemetry count we expect for each context
+ const EXPECTED_CONTEXT_COUNTS = {
+ browser: 3,
+ clearHistory: 2,
+ clearSiteData: 1,
+ };
+
+ for (let key in EXPECTED_CONTEXT_COUNTS) {
+ let count = 0;
+
+ for (let i = 0; i < EXPECTED_CONTEXT_COUNTS[key]; i++) {
+ await performActionsOnDialog({ context: key });
+ }
+
+ let contextTelemetry = Glean.privacySanitize.dialogOpen.testGetValue();
+ for (let object of contextTelemetry) {
+ if (object.extra.context == key) {
+ count += 1;
+ }
+ }
+
+ is(
+ count,
+ EXPECTED_CONTEXT_COUNTS[key],
+ `There should be ${EXPECTED_CONTEXT_COUNTS[key]} opens from ${key} context`
+ );
+ }
+});
+
+add_task(async function testTimespanTelemetry() {
+ Services.fog.testResetFOG();
+
+ // Expected timespan selections from telemetry
+ const EXPECTED_TIMESPANS = [
+ Sanitizer.TIMESPAN_HOUR,
+ Sanitizer.TIMESPAN_2HOURS,
+ Sanitizer.TIMESPAN_4HOURS,
+ Sanitizer.TIMESPAN_EVERYTHING,
+ ];
+
+ for (let timespan of EXPECTED_TIMESPANS) {
+ await performActionsOnDialog({ timespan });
+ }
+
+ for (let index in EXPECTED_TIMESPANS) {
+ is(
+ Glean.privacySanitize.clearingTimeSpanSelected.testGetValue()[index].extra
+ .time_span,
+ EXPECTED_TIMESPANS[index].toString(),
+ `Selected timespan should be ${EXPECTED_TIMESPANS[index]}`
+ );
+ }
+});
+
+add_task(async function testLoadtimeTelemetry() {
+ Services.fog.testResetFOG();
+
+ // loadtime metric is collected everytime that the dialog is opened
+ // expected number of times dialog will be opened for the test for each context
+ let EXPECTED_CONTEXT_COUNTS = {
+ browser: 2,
+ clearHistory: 3,
+ clearSiteData: 2,
+ };
+
+ // open dialog based on expected_context_counts
+ for (let context in EXPECTED_CONTEXT_COUNTS) {
+ for (let i = 0; i < EXPECTED_CONTEXT_COUNTS[context]; i++) {
+ await performActionsOnDialog({ context });
+ }
+ }
+
+ let loadTimeDistribution = Glean.privacySanitize.loadTime.testGetValue();
+
+ let expectedNumberOfCounts = Object.entries(EXPECTED_CONTEXT_COUNTS).reduce(
+ (acc, [key, value]) => acc + value,
+ 0
+ );
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ is(
+ Object.entries(loadTimeDistribution.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ expectedNumberOfCounts,
+ `Only ${expectedNumberOfCounts} buckets with samples`
+ );
+});
+
+add_task(async function testClearingOptionsTelemetry() {
+ Services.fog.testResetFOG();
+
+ let expectedObject = {
+ context: "clearSiteData",
+ history_form_data_downloads: "true",
+ cookies_and_storage: "false",
+ cache: "true",
+ site_settings: "true",
+ };
+
+ await performActionsOnDialog({
+ context: "clearSiteData",
+ historyFormDataAndDownloads: true,
+ cookiesAndStorage: false,
+ cache: true,
+ siteSettings: true,
+ });
+
+ let telemetryObject = Glean.privacySanitize.clear.testGetValue();
+ Assert.equal(
+ telemetryObject.length,
+ 1,
+ "There should be only 1 telemetry object recorded"
+ );
+
+ Assert.deepEqual(
+ expectedObject,
+ telemetryObject[0].extra,
+ `Expected ${telemetryObject} to be the same as ${expectedObject}`
+ );
+});
+
+add_task(async function testCheckboxStatesAfterMigration() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.history", false],
+ ["privacy.clearOnShutdown.formdata", true],
+ ["privacy.clearOnShutdown.cookies", true],
+ ["privacy.clearOnShutdown.offlineApps", false],
+ ["privacy.clearOnShutdown.sessions", false],
+ ["privacy.clearOnShutdown.siteSettings", false],
+ ["privacy.clearOnShutdown.cache", true],
+ ["privacy.clearOnShutdown_v2.cookiesAndStorage", false],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false],
+ ],
+ });
+
+ let dh = new DialogHelper("clearOnShutdown");
+ dh.onload = function () {
+ this.validateCheckbox("cookiesAndStorage", true);
+ this.validateCheckbox("historyFormDataAndDownloads", false);
+ this.validateCheckbox("cache", true);
+ this.validateCheckbox("siteSettings", false);
+ this.cancelDialog();
+ };
+ dh.open();
+ await dh.promiseClosed;
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitizeOnShutdown_migration.js b/browser/base/content/test/sanitize/browser_sanitizeOnShutdown_migration.js
new file mode 100644
index 0000000000..3c2af1d513
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitizeOnShutdown_migration.js
@@ -0,0 +1,312 @@
+/* -*- 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/. */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.sanitize.useOldClearHistoryDialog", false]],
+ });
+});
+
+add_task(async function testMigrationOfCacheAndSiteSettings() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cache", true],
+ ["privacy.clearOnShutdown.siteSettings", true],
+ ["privacy.clearOnShutdown_v2.cache", false],
+ ["privacy.clearOnShutdown_v2.siteSettings", false],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false],
+ ],
+ });
+
+ Sanitizer.runSanitizeOnShutdown();
+
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown_v2.cache"),
+ true,
+ "Cache should be set to true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown_v2.siteSettings"),
+ true,
+ "siteSettings should be set to true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"),
+ true,
+ "old cache should remain true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.siteSettings"),
+ true,
+ "old siteSettings should remain true"
+ );
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs"
+ ),
+ true,
+ "migration pref has been flipped"
+ );
+});
+
+add_task(async function testHistoryAndFormData_historyTrue() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.history", true],
+ ["privacy.clearOnShutdown.formdata", false],
+ ["privacy.clearOnShutdown_v2.historyFormDataAndDownloads", false],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false],
+ ],
+ });
+
+ Sanitizer.runSanitizeOnShutdown();
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads"
+ ),
+ true,
+ "historyFormDataAndDownloads should be set to true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.history"),
+ true,
+ "old history pref should remain true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.formdata"),
+ false,
+ "old formdata pref should remain false"
+ );
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs"
+ ),
+ true,
+ "migration pref has been flipped"
+ );
+});
+
+add_task(async function testHistoryAndFormData_historyFalse() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.history", false],
+ ["privacy.clearOnShutdown.formdata", true],
+ ["privacy.clearOnShutdown_v2.historyFormDataAndDownloads", true],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false],
+ ],
+ });
+
+ Sanitizer.runSanitizeOnShutdown();
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.clearOnShutdown_v2.historyFormDataAndDownloads"
+ ),
+ false,
+ "historyFormDataAndDownloads should be set to true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.history"),
+ false,
+ "old history pref should remain false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.formdata"),
+ true,
+ "old formdata pref should remain true"
+ );
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs"
+ ),
+ true,
+ "migration pref has been flipped"
+ );
+});
+
+add_task(async function testCookiesAndStorage_cookiesFalse() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cookies", false],
+ ["privacy.clearOnShutdown.offlineApps", true],
+ ["privacy.clearOnShutdown.sessions", true],
+ ["privacy.clearOnShutdown_v2.cookiesAndStorage", true],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false],
+ ],
+ });
+
+ // Simulate clearing on shutdown.
+ Sanitizer.runSanitizeOnShutdown();
+
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown_v2.cookiesAndStorage"),
+ false,
+ "cookiesAndStorage should be set to false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ false,
+ "old cookies pref should remain false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ true,
+ "old offlineApps pref should remain true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions"),
+ true,
+ "old sessions pref should remain true"
+ );
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs"
+ ),
+ true,
+ "migration pref has been flipped"
+ );
+});
+
+add_task(async function testCookiesAndStorage_cookiesTrue() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cookies", true],
+ ["privacy.clearOnShutdown.offlineApps", false],
+ ["privacy.clearOnShutdown.sessions", false],
+ ["privacy.clearOnShutdown_v2.cookiesAndStorage", false],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false],
+ ],
+ });
+
+ Sanitizer.runSanitizeOnShutdown();
+
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown_v2.cookiesAndStorage"),
+ true,
+ "cookiesAndStorage should be set to true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ true,
+ "old cookies pref should remain true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ false,
+ "old offlineApps pref should remain false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions"),
+ false,
+ "old sessions pref should remain false"
+ );
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs"
+ ),
+ true,
+ "migration pref has been flipped"
+ );
+});
+
+add_task(async function testMigrationDoesNotRepeat() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cookies", true],
+ ["privacy.clearOnShutdown.offlineApps", false],
+ ["privacy.clearOnShutdown.sessions", false],
+ ["privacy.clearOnShutdown_v2.cookiesAndStorage", false],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", true],
+ ],
+ });
+
+ // Simulate clearing on shutdown.
+ Sanitizer.runSanitizeOnShutdown();
+
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown_v2.cookiesAndStorage"),
+ false,
+ "cookiesAndStorage should remain false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ true,
+ "old cookies pref should remain true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ false,
+ "old offlineApps pref should remain false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions"),
+ false,
+ "old sessions pref should remain false"
+ );
+
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs"
+ ),
+ true,
+ "migration pref has been flipped"
+ );
+});
+
+add_task(async function ensureNoOldPrefsAreEffectedByMigration() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.history", true],
+ ["privacy.clearOnShutdown.formdata", true],
+ ["privacy.clearOnShutdown.cookies", true],
+ ["privacy.clearOnShutdown.offlineApps", false],
+ ["privacy.clearOnShutdown.sessions", false],
+ ["privacy.clearOnShutdown.siteSettings", true],
+ ["privacy.clearOnShutdown.cache", true],
+ ["privacy.clearOnShutdown_v2.cookiesAndStorage", false],
+ ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false],
+ ],
+ });
+
+ Sanitizer.runSanitizeOnShutdown();
+
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown_v2.cookiesAndStorage"),
+ true,
+ "cookiesAndStorage should become true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ true,
+ "old cookies pref should remain true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ false,
+ "old offlineApps pref should remain false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions"),
+ false,
+ "old sessions pref should remain false"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.history"),
+ true,
+ "old history pref should remain true"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.formdata"),
+ true,
+ "old formdata pref should remain true"
+ );
+});
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..f5a6031b84
--- /dev/null
+++ b/browser/base/content/test/sanitize/head.js
@@ -0,0 +1,371 @@
+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,
+ });
+ });
+ });
+}
+
+function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) {
+ return new Promise(resolve => {
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ openPreferences(aPane);
+ let newTabBrowser = gBrowser.selectedBrowser;
+
+ newTabBrowser.addEventListener(
+ "Initialized",
+ function () {
+ newTabBrowser.contentWindow.addEventListener(
+ "load",
+ async function () {
+ let win = gBrowser.contentWindow;
+ let selectedPane = win.history.state;
+ await finalPrefPaneLoaded;
+ if (!aOptions || !aOptions.leaveOpen) {
+ gBrowser.removeCurrentTab();
+ }
+ resolve({ selectedPane });
+ },
+ { once: true }
+ );
+ },
+ { capture: true, once: true }
+ );
+ });
+}
+
+async function createDummyDataForHost(host) {
+ let origin = "https://" + host;
+ let dummySWURL =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ "dummy.js";
+
+ await SiteDataTestUtils.addToIndexedDB(origin);
+ await SiteDataTestUtils.addServiceWorker(dummySWURL);
+}
diff --git a/browser/base/content/test/sanitize/site_data_test.html b/browser/base/content/test/sanitize/site_data_test.html
new file mode 100644
index 0000000000..7e5ede2646
--- /dev/null
+++ b/browser/base/content/test/sanitize/site_data_test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html manifest="manifest.appcache">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="public" />
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
+
+ <title>Site Data Test</title>
+
+ </head>
+
+ <body>
+ <h1>Site Data Test</h1>
+ <script type="text/javascript">
+ let request = indexedDB.open("TestDatabase", 1);
+ request.onupgradeneeded = function(e) {
+ let db = e.target.result;
+ db.createObjectStore("TestStore", { keyPath: "id" });
+ };
+ request.onsuccess = function(e) {
+ let db = e.target.result;
+ let tx = db.transaction("TestStore", "readwrite");
+ let store = tx.objectStore("TestStore");
+ store.put({ id: "test_id", description: "Site Data Test"});
+ tx.oncomplete = () => document.dispatchEvent(new CustomEvent("test-indexedDB-done", {bubbles: true, cancelable: false}));
+ };
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/sidebar/browser.toml b/browser/base/content/test/sidebar/browser.toml
new file mode 100644
index 0000000000..2e7e7c03c4
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser.toml
@@ -0,0 +1,13 @@
+[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..032c23b029
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_switcher.js
@@ -0,0 +1,289 @@
+registerCleanupFunction(() => {
+ SidebarUI.hide();
+});
+
+/**
+ * Helper function that opens a sidebar switcher panel popup menu
+ * @returns Promise that resolves when the switcher panel popup is shown
+ * without any action from a user/test
+ */
+function showSwitcherPanelPromise() {
+ return new Promise(resolve => {
+ SidebarUI._switcherPanel.addEventListener(
+ "popupshown",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ SidebarUI.showSwitcherPanel();
+ });
+}
+
+/**
+ * Helper function that waits for a sidebar switcher panel's "popupshown" event
+ * @returns Promise which resolves when the popup menu is opened
+ */
+async function waitForSwitcherPopupShown() {
+ return BrowserTestUtils.waitForEvent(SidebarUI._switcherPanel, "popupshown");
+}
+
+/**
+ * Helper function that sends a mouse click to a specific menu item or a key
+ * event to a active menu item of the sidebar switcher menu popup. Provide a
+ * querySelector parameter when a click behavior is needed.
+ * @param {String} [querySelector=null] An HTML attribute of the menu item
+ * to be clicked
+ * @returns Promise that resolves when both the menu popup is hidden and
+ * the sidebar itself is focused
+ */
+function pickSwitcherMenuitem(querySelector = null) {
+ let sidebarPopup = document.querySelector("#sidebarMenu-popup");
+ let hideSwitcherPanelPromise = Promise.all([
+ BrowserTestUtils.waitForEvent(window, "SidebarFocused"),
+ BrowserTestUtils.waitForEvent(sidebarPopup, "popuphidden"),
+ ]);
+ if (querySelector) {
+ document.querySelector(querySelector).click();
+ } else {
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ }
+ return hideSwitcherPanelPromise;
+}
+
+/**
+ * Helper function to test a key handling of sidebar menu popup items used to
+ * access a specific sidebar
+ * @param {String} key Event.key to open the switcher menu popup
+ * @param {String} sidebarTitle Title of the sidebar that is to be activated
+ * during the test (capitalized one word versions),
+ * i.e. "History" or "Tabs"
+ */
+async function testSidebarMenuKeyToggle(key, sidebarTitle) {
+ info(`Testing "${key}" key handling of sidebar menu popup items
+ to access ${sidebarTitle} sidebar`);
+
+ Assert.ok(SidebarUI.isOpen, "Sidebar is open");
+
+ let sidebarSwitcher = document.querySelector("#sidebar-switcher-target");
+ let sidebar = document.getElementById("sidebar");
+ let searchBox = sidebar.firstElementChild;
+
+ // If focus is on the search field (i.e. on the History sidebar),
+ // or if the focus is on the awesomebar (bug 1835899),
+ // move it to the switcher target:
+
+ if (searchBox && searchBox.matches(":focus")) {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true, repeat: 2 });
+ } else if (!sidebarSwitcher.matches(":focus")) {
+ sidebarSwitcher.focus();
+ }
+
+ Assert.equal(
+ document.activeElement,
+ sidebarSwitcher,
+ "The sidebar switcher target button is focused"
+ );
+ Assert.ok(
+ sidebarSwitcher.matches(":focus"),
+ "The sidebar switcher target button is focused"
+ );
+ Assert.equal(
+ SidebarUI._switcherPanel.state,
+ "closed",
+ "Sidebar menu popup is closed"
+ );
+
+ let promisePopupShown = waitForSwitcherPopupShown();
+
+ // Activate sidebar switcher target to open its sidebar menu popup:
+ EventUtils.synthesizeKey(key, {});
+
+ await promisePopupShown;
+
+ Assert.equal(
+ SidebarUI._switcherPanel.state,
+ "open",
+ "Sidebar menu popup is open"
+ );
+
+ info("Testing keyboard navigation between sidebar menu popup controls");
+
+ let arrowDown = async (menuitemId, msg) => {
+ let menuItemActive = BrowserTestUtils.waitForEvent(
+ SidebarUI._switcherPanel,
+ "DOMMenuItemActive"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ await menuItemActive;
+ Assert.ok(
+ document.getElementById(menuitemId).hasAttribute("_moz-menuactive"),
+ msg
+ );
+ };
+
+ // Move to the first sidebar switcher option:
+ await arrowDown(
+ "sidebar-switcher-bookmarks",
+ "The 1st sidebar menu item (Bookmarks) is active"
+ );
+
+ // Move to the next sidebar switcher option:
+ await arrowDown(
+ "sidebar-switcher-history",
+ "The 2nd sidebar menu item (History) is active"
+ );
+
+ if (sidebarTitle === "Tabs") {
+ await arrowDown(
+ "sidebar-switcher-tabs",
+ "The 3rd sidebar menu item (Synced Tabs) is active"
+ );
+ }
+
+ // Activate the tested sidebar switcher option to open the tested sidebar:
+ let sidebarShown = BrowserTestUtils.waitForEvent(window, "SidebarShown");
+ await pickSwitcherMenuitem(/* querySelector = */ null);
+ await sidebarShown;
+
+ info("Testing keyboard navigation when a sidebar menu popup is closed");
+
+ Assert.equal(
+ SidebarUI._switcherPanel.state,
+ "closed",
+ "Sidebar menu popup is closed"
+ );
+ // Test the sidebar panel is updated
+ Assert.equal(
+ SidebarUI._box.getAttribute("sidebarcommand"),
+ `view${sidebarTitle}Sidebar` /* e.g. "viewHistorySidebar" */,
+ `${sidebarTitle} sidebar loaded`
+ );
+ Assert.equal(
+ SidebarUI.currentID,
+ `view${sidebarTitle}Sidebar` /* e.g. "viewHistorySidebar" */,
+ `${sidebarTitle}'s current ID is updated to a target view`
+ );
+}
+
+add_task(async function markup() {
+ // If a sidebar is already open, close it.
+ if (!document.getElementById("sidebar-box").hidden) {
+ Assert.ok(
+ false,
+ "Unexpected sidebar found - a previous test failed to cleanup correctly"
+ );
+ SidebarUI.hide();
+ }
+
+ let sidebarPopup = document.querySelector("#sidebarMenu-popup");
+ let sidebarSwitcher = document.querySelector("#sidebar-switcher-target");
+ let sidebarTitle = document.querySelector("#sidebar-title");
+
+ info("Test default markup of the sidebar switcher control");
+
+ Assert.equal(
+ sidebarSwitcher.tagName,
+ "toolbarbutton",
+ "Sidebar switcher target control is a toolbarbutton"
+ );
+ Assert.equal(
+ sidebarSwitcher.children[1],
+ sidebarTitle,
+ "Sidebar switcher target control has a child label element"
+ );
+ Assert.equal(
+ sidebarTitle.tagName,
+ "label",
+ "Sidebar switcher target control has a label element (that is expected to provide its accessible name"
+ );
+ Assert.equal(
+ sidebarSwitcher.getAttribute("aria-expanded"),
+ "false",
+ "Sidebar switcher button is collapsed by default"
+ );
+
+ info("Test dynamic changes in the markup of the sidebar switcher control");
+
+ await SidebarUI.show("viewBookmarksSidebar");
+ await showSwitcherPanelPromise();
+
+ Assert.equal(
+ sidebarSwitcher.getAttribute("aria-expanded"),
+ "true",
+ "Sidebar switcher button is expanded when a sidebar menu is shown"
+ );
+
+ let waitForPopupHidden = BrowserTestUtils.waitForEvent(
+ sidebarPopup,
+ "popuphidden"
+ );
+
+ // Close on Escape anywhere
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ await waitForPopupHidden;
+
+ Assert.equal(
+ sidebarSwitcher.getAttribute("aria-expanded"),
+ "false",
+ "Sidebar switcher button is collapsed when a sidebar menu is dismissed"
+ );
+
+ SidebarUI.hide();
+});
+
+add_task(async function keynav() {
+ // If a sidebar is already open, close it.
+ if (SidebarUI.isOpen) {
+ Assert.ok(
+ false,
+ "Unexpected sidebar found - a previous test failed to cleanup correctly"
+ );
+ SidebarUI.hide();
+ }
+
+ await SidebarUI.show("viewBookmarksSidebar");
+
+ await testSidebarMenuKeyToggle("KEY_Enter", "History");
+ await testSidebarMenuKeyToggle(" ", "Tabs");
+
+ SidebarUI.hide();
+});
+
+add_task(async function mouse() {
+ // If a sidebar is already open, close it.
+ if (!document.getElementById("sidebar-box").hidden) {
+ Assert.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 pickSwitcherMenuitem("#sidebar-switcher-history");
+ Assert.equal(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewHistorySidebar",
+ "History sidebar loaded"
+ );
+
+ await showSwitcherPanelPromise();
+ await pickSwitcherMenuitem("#sidebar-switcher-tabs");
+ Assert.equal(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewTabsSidebar",
+ "Tabs sidebar loaded"
+ );
+
+ await showSwitcherPanelPromise();
+ await pickSwitcherMenuitem("#sidebar-switcher-bookmarks");
+ Assert.equal(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewBookmarksSidebar",
+ "Bookmarks sidebar loaded"
+ );
+});
diff --git a/browser/base/content/test/siteIdentity/browser.toml b/browser/base/content/test/siteIdentity/browser.toml
new file mode 100644
index 0000000000..f0b6191302
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser.toml
@@ -0,0 +1,194 @@
+[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_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_bug1045809.js"]
+tags = "mcb"
+support-files = [
+ "file_bug1045809_1.html",
+ "file_bug1045809_2.html",
+]
+
+["browser_check_identity_state.js"]
+https_first_disabled = true
+
+["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",
+]
+skip-if = ["a11y_checks"] # Bugs 1858041 and 1824058 for causing intermittent crashes
+
+["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..d7ec29dc14
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_about_blank_same_document_tabswitch.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+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(
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ UrlbarTestUtils.trimURL("http://example.org/")
+ ),
+ "URL bar value should be correct, was " + gURLBar.value
+ );
+ is(
+ identityBox.className,
+ "unknownIdentity",
+ "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..2012380a6e
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..9da5115051
--- /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.startLoadingURIString(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..481a72d67e
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug906190.js
@@ -0,0 +1,339 @@
+/* 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");
+
+ 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..b232510575
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_check_identity_state.js
@@ -0,0 +1,720 @@
+/*
+ * 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_TEXT_PREF = "security.insecure_connection_text.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",
+ "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], () => {
+ Assert.notEqual(
+ getIdentityMode(),
+ "chromeUI",
+ "Identity should not be chromeUI"
+ );
+ });
+ }
+});
+
+add_task(async function test_webpage() {
+ let oldTab = await loadNewTab("about:robots");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+});
+
+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]],
+ });
+ 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"
+ );
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should be not secure"
+ );
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage_text_warning_combined() {
+ await webpageTestTextWarningCombined(false);
+ await webpageTestTextWarningCombined(true);
+});
+
+add_task(async function test_blank_page() {
+ let oldTab = await loadNewTab("about:robots");
+
+ 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);
+});
+
+add_task(async function test_secure() {
+ let oldTab = await loadNewTab("about:robots");
+
+ 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);
+});
+
+add_task(async function test_view_source() {
+ 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_insecure() {
+ let oldTab = await loadNewTab("about:robots");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ 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."
+ );
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ 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."
+ );
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+});
+
+add_task(async function test_addons() {
+ let oldTab = await loadNewTab("about:robots");
+
+ 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);
+});
+
+add_task(async function test_file() {
+ let oldTab = await loadNewTab("about:robots");
+ 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);
+});
+
+add_task(async function test_resource_uri() {
+ let oldTab = await loadNewTab("about:robots");
+ 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);
+});
+
+add_task(async function test_no_cert_error() {
+ let oldTab = await loadNewTab("about:robots");
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ 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);
+});
+
+add_task(async function test_https_only_error() {
+ 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);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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();
+});
+
+add_task(async function test_no_cert_error_from_navigation() {
+ // 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);
+});
+
+add_task(async function test_tls_error_page() {
+ 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 test_net_error_page() {
+ // Connect to a server that rejects all requests, to test network error pages:
+ let { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+ );
+ 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);
+});
+
+add_task(async function test_about_blocked() {
+ // 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: [["urlclassifier.blockedTable", "moztest-block-simple"]],
+ });
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ BrowserTestUtils.startLoadingURIString(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_no_cert_error_security_connection_bg() {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = tab;
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ 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);
+});
+
+add_task(async function test_about_uri() {
+ let oldTab = await loadNewTab("about:robots");
+ 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);
+});
+
+add_task(async function test_reader_uri() {
+ 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_data_uri() {
+ let oldTab = await loadNewTab("about:robots");
+ let dataURI = "data:text/html,hi";
+
+ let newTab = await loadNewTab(dataURI);
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+});
+
+add_task(async function test_pb_mode() {
+ await SpecialPowers.pushPrefEnv({ set: [[HTTPS_FIRST_PBM_PREF, false]] });
+
+ 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
+ );
+
+ is(getIdentityMode(privateWin), "notSecure", "Identity should be not secure");
+
+ privateWin.gBrowser.selectedTab = oldTab;
+ is(
+ getIdentityMode(privateWin),
+ "localResource",
+ "Identity should be localResource"
+ );
+
+ privateWin.gBrowser.selectedTab = newTab;
+ is(getIdentityMode(privateWin), "notSecure", "Identity should be not secure");
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_setup(() => {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.insecure_connection_text.enabled", false],
+ ["security.insecure_connection_text.pbmode.enabled", 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..9f4e593c3e
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_check_identity_state_pdf.js
@@ -0,0 +1,82 @@
+/* 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() {
+ let expectedIdentity = Services.prefs.getBoolPref(
+ "security.insecure_connection_text.enabled"
+ )
+ ? "notSecure notSecureText"
+ : "notSecure";
+ 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,
+ expectedIdentity,
+ `Identity should be ${expectedIdentity} 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..62a9f948ea
--- /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.startLoadingURIString(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..17d3fc56cb
--- /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.startLoadingURIString(browser, HTTPS_TLS1_0);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "broken");
+ is(
+ getIdentityMode(),
+ "unknownIdentity weakCipher",
+ "Identity should be unknownIdentity"
+ );
+ await checkConnectionState("not-secure");
+
+ BrowserTestUtils.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..39502f609c
--- /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);
+ },
+ }
+ );
+ });
+
+ Assert.notEqual(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..4708f9ee45
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_getSecurityInfo.js
@@ -0,0 +1,38 @@
+/* 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.startLoadingURIString(
+ 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.startLoadingURIString(browser, "http://example.com");
+ await loaded;
+ securityInfo = gBrowser.securityUI.secInfo;
+ ok(!securityInfo, "Found no security info");
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(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..7689f44ac8
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js
@@ -0,0 +1,55 @@
+/* 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.startLoadingURIString(
+ 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..9d692b453f
--- /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/AppConstants.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..5564b0ae40
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_HttpsOnlyMode.js
@@ -0,0 +1,246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const HTTPS_ONLY_PERMISSION = "https-only-load-insecure";
+const WEBSITE = scheme => `${scheme}://example.com`;
+
+add_task(async function () {
+ info("Running regular tests");
+ await runTests();
+ info("Running tests with view-source: uri");
+ await runTests({ outerScheme: "view-source" });
+});
+
+async function runTests(options = {}) {
+ const { outerScheme = "" } = options;
+ const outerSchemePrefix = outerScheme ? outerScheme + ":" : "";
+
+ 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: outerSchemePrefix + "https",
+ initialPermission: 0,
+ isUiVisible: false,
+ });
+
+ // Site gets upgraded to HTTPS, so the UI should be visible.
+ // Adding a HTTPS-Only exemption through the menulist should reload the page and
+ // set the permission accordingly.
+ await runTest({
+ name: "Add HTTPS-Only exemption",
+ initialScheme: outerSchemePrefix + "http",
+ initialPermission: 0,
+ isUiVisible: true,
+ selectPermission: 1,
+ expectReload: true,
+ finalScheme: outerScheme || "https",
+ });
+
+ // HTTPS-Only Mode is disabled for this site, so the UI should be visible.
+ // Switching HTTPS-Only exemption modes through the menulist should not reload the page
+ // but set the permission accordingly.
+ await runTest({
+ name: "Switch between HTTPS-Only exemption modes",
+ initialScheme: outerSchemePrefix + "http",
+ initialPermission: 1,
+ isUiVisible: true,
+ selectPermission: 2,
+ expectReload: false,
+ finalScheme: outerScheme || "http",
+ });
+
+ // HTTPS-Only Mode is disabled for this site, so the UI should be visible.
+ // Disabling HTTPS-Only exemptions through the menulist should reload and upgrade the
+ // page and set the permission accordingly.
+ await runTest({
+ name: "Remove HTTPS-Only exemption again",
+ initialScheme: outerSchemePrefix + "http",
+ initialPermission: 2,
+ permissionScheme: "http",
+ isUiVisible: true,
+ selectPermission: 0,
+ expectReload: true,
+ finalScheme: outerScheme || "https",
+ });
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+
+ // Site is already HTTPS, so the UI should not be visible.
+ await runTest({
+ name: "No HTTPS-Only UI",
+ initialScheme: outerSchemePrefix + "https",
+ initialPermission: 0,
+ permissionScheme: "https",
+ isUiVisible: false,
+ });
+
+ // Site gets upgraded to HTTPS, so the UI should be visible.
+ // Adding a HTTPS-Only exemption through the menulist should reload the page and
+ // set the permission accordingly.
+ await runTest({
+ name: "Add HTTPS-Only exemption",
+ initialScheme: outerSchemePrefix + "http",
+ initialPermission: 0,
+ permissionScheme: "https",
+ isUiVisible: true,
+ selectPermission: 1,
+ expectReload: true,
+ finalScheme: outerScheme || "https",
+ });
+
+ // HTTPS-First Mode is disabled for this site, so the UI should be visible.
+ // Switching HTTPS-Only exemption modes through the menulist should not reload the page
+ // but set the permission accordingly.
+ await runTest({
+ name: "Switch between HTTPS-Only exemption modes",
+ initialScheme: outerSchemePrefix + "http",
+ initialPermission: 1,
+ permissionScheme: "http",
+ isUiVisible: true,
+ selectPermission: 2,
+ expectReload: false,
+ finalScheme: outerScheme || "http",
+ });
+
+ // HTTPS-First Mode is disabled for this site, so the UI should be visible.
+ // Disabling HTTPS-Only exemptions through the menulist should reload and upgrade the
+ // page and set the permission accordingly.
+ await runTest({
+ name: "Remove HTTPS-Only exemption again",
+ initialScheme: outerSchemePrefix + "http",
+ initialPermission: 2,
+ isUiVisible: true,
+ selectPermission: 0,
+ expectReload: true,
+ finalScheme: outerScheme || "https",
+ });
+}
+
+async function runTest(options) {
+ // Set the initial permission
+ setPermission(WEBSITE("http"), options.initialPermission);
+
+ await BrowserTestUtils.withNewTab(
+ WEBSITE(options.initialScheme),
+ async function (browser) {
+ const name = options.name + " | ";
+
+ // 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("http")),
+ 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..0107814b98
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js
@@ -0,0 +1,237 @@
+/* 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) {
+ let promises = ["test1", "test2", "test3", "test4"].map(cookieName =>
+ TestUtils.topicObserved("cookie-changed", subj => {
+ let notification = subj.QueryInterface(Ci.nsICookieNotification);
+ return (
+ notification.action == Ci.nsICookieNotification.COOKIE_DELETED &&
+ notification.cookie.name == cookieName
+ );
+ })
+ );
+ cookiesCleared = Promise.all(promises);
+ }
+
+ // 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..f4074ba00d
--- /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.isVisible(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.isVisible(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.isHidden(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.isHidden(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..d896c165d6
--- /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.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ currentTest.location
+ );
+ await loaded;
+ await popupHidden;
+ ok(
+ !gIdentityHandler._identityPopup ||
+ BrowserTestUtils.isHidden(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.isHidden(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..1e5e01762e
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_iframe_navigation.js
@@ -0,0 +1,130 @@
+/* -*- 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://");
+
+const NOT_SECURE_LABEL = Services.prefs.getBoolPref(
+ "security.insecure_connection_text.enabled"
+)
+ ? "notSecure notSecureText"
+ : "notSecure";
+
+// 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,
+ NOT_SECURE_LABEL,
+ "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,
+ NOT_SECURE_LABEL,
+ "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,
+ NOT_SECURE_LABEL,
+ "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,
+ NOT_SECURE_LABEL,
+ "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..97c6f8f406
--- /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.startLoadingURIString(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..f3ad5a2b19
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mcb_redirect.js
@@ -0,0 +1,345 @@
+/*
+ * 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"
+);
+
+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);
+
+ // Make sure we are online again
+ Services.io.offline = false;
+});
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+ finish();
+}
+
+// ------------------------ Test 1 ------------------------------
+
+function testInsecure1() {
+ var url = HTTPS_TEST_ROOT + "test_mcb_redirect.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkUIForTest1
+ );
+ BrowserTestUtils.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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);
+ 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..5ddd8b4c22
--- /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.startLoadingURIString(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.startLoadingURIString(browser, url);
+ await BrowserTestUtils.browserLoaded(browser);
+ url = HTTPS_TEST_ROOT_2 + "file_mixedContentFromOnunload_test2.html";
+ BrowserTestUtils.startLoadingURIString(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..99f4efce9c
--- /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.startLoadingURIString(
+ 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..dd8280b204
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..ac3fcc4067
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..0c1ca3296f
--- /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.startLoadingURIString(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.startLoadingURIString(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..9dce76266a
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js
@@ -0,0 +1,195 @@
+/* -*- 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.
+
+const NOT_SECURE_LABEL = Services.prefs.getBoolPref(
+ "security.insecure_connection_text.enabled"
+)
+ ? "notSecure notSecureText"
+ : "notSecure";
+
+/**
+ * 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.isVisible(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,
+ NOT_SECURE_LABEL,
+ `identity should be '${NOT_SECURE_LABEL}'`
+ );
+
+ // 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..733796ffb7
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/head.js
@@ -0,0 +1,422 @@
+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.startLoadingURIString(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) {
+ // HTTP request, there should be a broken padlock shown always.
+ ok(classList.contains("notSecure"), "notSecure on HTTP page");
+ ok(
+ !BrowserTestUtils.isHidden(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.isHidden(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.isHidden(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.startLoadingURIString(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..bf1aefcb3f
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html
@@ -0,0 +1,43 @@
+<!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";
+ 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..b8eb3c7be5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html
@@ -0,0 +1,44 @@
+<!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";
+ 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..7c2b50488f
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html
@@ -0,0 +1,43 @@
+<!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";
+ 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.toml b/browser/base/content/test/startup/browser.toml
new file mode 100644
index 0000000000..44e1857e62
--- /dev/null
+++ b/browser/base/content/test/startup/browser.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+["browser_preXULSkeletonUIRegistry.js"]
+run-if = ["os == 'win'"] # We only enable the skele UI on Win10
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.toml b/browser/base/content/test/static/browser.toml
new file mode 100644
index 0000000000..4e8a0ebe06
--- /dev/null
+++ b/browser/base/content/test/static/browser.toml
@@ -0,0 +1,29 @@
+[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..6389e59e00
--- /dev/null
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -0,0 +1,1088 @@
+/* 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 allowlist specific files, please use the 'allowlist' 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/",
+ // Localization file added programatically in FormAutofillUtils.sys.mjs
+ "resource://gre/localization/en-US/toolkit/formautofill",
+
+ // 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/",
+
+ // 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/",
+
+ // Normandy schemas are referenced programmatically.
+ "resource://normandy/schemas/",
+
+ // ASRouter schemas are referenced programmatically.
+ "chrome://browser/content/asrouter/schemas/",
+
+ // Localization file added programatically in FeatureCallout.sys.mjs
+ "resource://app/localization/en-US/browser/featureCallout.ftl",
+
+ // CSS files are referenced inside JS in an html template
+ "chrome://browser/content/aboutlogins/components/",
+];
+
+// 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_*.sys.mjs` are loaded at runtime by `app --backgroundtask id ...`.
+ gExceptionPaths.push("resource://gre/modules/backgroundtasks/");
+ gExceptionPaths.push("resource://app/modules/backgroundtasks/");
+}
+
+if (AppConstants.NIGHTLY_BUILD) {
+ // This is nightly-only debug tool.
+ gExceptionPaths.push(
+ "chrome://browser/content/places/interactionsViewer.html"
+ );
+}
+
+// Each allowlist entry should have a comment indicating which file is
+// referencing the listed 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 allowlist = [
+ // 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.mjs" },
+ { file: "resource://pdf.js/web/debugger.css" },
+
+ // Starting from here, files in the allowlist 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.sys.mjs
+ {
+ 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 1854618 - referenced by aboutWebauthn.html which is only for Linux and Mac
+ {
+ file: "resource://gre/localization/en-US/toolkit/about/aboutWebauthn.ftl",
+ platforms: ["win", "android"],
+ },
+ // 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" },
+];
+
+if (AppConstants.platform != "win") {
+ // toolkit/mozapps/defaultagent/Notification.cpp
+ // toolkit/mozapps/defaultagent/ScheduledTask.cpp
+ // toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs
+ // Bug 1854425 - referenced by default browser agent which is not detected
+ allowlist.push({
+ file: "resource://app/localization/en-US/browser/backgroundtasks/defaultagent.ftl",
+ });
+
+ if (AppConstants.NIGHTLY_BUILD) {
+ // This path is refereneced in nsFxrCommandLineHandler.cpp, which is only
+ // compiled in Windows. This path is allowed so that non-Windows builds
+ // can access the FxR UI via --chrome rather than --fxr (which includes VR-
+ // specific functionality)
+ allowlist.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
+ allowlist.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.
+ allowlist.push({
+ file: "resource://gre/modules/TaskScheduler.sys.mjs",
+ });
+}
+
+allowlist = new Set(
+ allowlist
+ .filter(
+ item =>
+ "isFromDevTools" in item == isDevtools &&
+ (!item.skipUnofficial || !AppConstants.MOZILLA_OFFICIAL) &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))
+ )
+ .map(item => item.file)
+);
+
+const ignorableAllowlist = 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 ignorableAllowlist) {
+ allowlist.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",
+ ]) {
+ allowlist.add("resource://services-sync/engines/" + module);
+ }
+ // resource://devtools/shared/worker/loader.js,
+ // resource://devtools/shared/loader/builtin-modules.js
+ if (!AppConstants.ENABLE_WEBDRIVER) {
+ allowlist.add("resource://gre/modules/jsdebugger.sys.mjs");
+ }
+}
+
+if (AppConstants.MOZ_CODE_COVERAGE) {
+ allowlist.add(
+ "chrome://remote/content/marionette/PerTestCoverageUtils.sys.mjs"
+ );
+}
+
+const gInterestingCategories = new Set([
+ "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, "allowlist-direct" means that we have not found
+// any reference in the code, but have a matching allowlist entry for this file.
+// "allowlist" means that the file is indirectly allowlisted, ie. a allowlisted
+// 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-shared-images/",
+ "resource://devtools-highlighter-styles/",
+ "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 == "allowlist" ||
+ refType == "allowlist-direct"
+ ) {
+ return false;
+ }
+ }
+ }
+ }
+ return !gOverrideMap.has(file) || isUnreferenced(gOverrideMap.get(file));
+ };
+
+ let unreferencedFiles = chromeFiles;
+
+ let removeReferenced = useAllowlist => {
+ 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 (useAllowlist) {
+ info(
+ "indirectly allowlisted file: " +
+ f +
+ " used from " +
+ listCodeReferences(gReferencesFromCode.get(f))
+ );
+ }
+ gReferencesFromCode.set(f, useAllowlist ? "allowlist" : 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 allowed.
+ unreferencedFiles = unreferencedFiles.filter(file => {
+ if (allowlist.has(file)) {
+ allowlist.delete(file);
+ gReferencesFromCode.set(file, "allowlist-direct");
+ return false;
+ }
+ return true;
+ });
+ // Run the process again, this time when more files are marked as referenced,
+ // it's a consequence of the allowlist.
+ 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 allowlist) {
+ if (ignorableAllowlist.has(file)) {
+ info("ignored unused allowlist entry: " + file);
+ } else {
+ ok(false, "unused allowlist 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 allowlist 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..4191cc966e
--- /dev/null
+++ b/browser/base/content/test/static/browser_misused_characters_in_strings.js
@@ -0,0 +1,247 @@
+/* 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",
+ },
+ {
+ file: "dom.properties",
+ key: "MathML_DeprecatedMathVariantWarning",
+ type: "single-quote",
+ },
+ // dom.properties is packaged twice so we need to have two exceptions for this string.
+ {
+ file: "dom.properties",
+ key: "MathML_DeprecatedMathVariantWarning",
+ 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 domParser = new DOMParser();
+ domParser.forceEnableDTD();
+
+ for (let uri of uris) {
+ let rawContents = await fetchFile(uri.spec);
+ const resource = new FluentResource(rawContents);
+
+ for (const info of resource.textElements()) {
+ const key = info.attr ? `${info.id}.${info.attr}` : info.id;
+
+ const stripped_val = domParser.parseFromString(
+ "<!DOCTYPE html>" + info.text,
+ "text/html"
+ ).documentElement.textContent;
+
+ testForErrors(uri.spec, key, stripped_val);
+ }
+ }
+});
+
+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..b34ae7d9c1
--- /dev/null
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -0,0 +1,577 @@
+/* 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 ignoreList = [
+ // CodeMirror is imported as-is, see bug 1004423.
+ { sourceName: /codemirror\.css$/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,
+ },
+];
+
+if (!Services.prefs.getBoolPref("layout.css.zoom.enabled")) {
+ ignoreList.push({
+ sourceName: /\bscrollbars\.css$/i,
+ errorMessage: /Error in parsing value for ‘zoom’/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.math-depth.enabled")) {
+ // mathml.css UA sheet rule for math-depth.
+ ignoreList.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.
+ ignoreList.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")) {
+ ignoreList.push({
+ sourceName: /webconsole\.css$/i,
+ errorMessage: /Unknown property .*\boverflow-anchor\b/i,
+ isFromDevTools: true,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.forced-colors.enabled")) {
+ ignoreList.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.
+ ignoreList.push({
+ sourceName: /web\/viewer\.css$/i,
+ errorMessage:
+ /Unknown property ‘forced-color-adjust’\. {2}Declaration dropped\./i,
+ isFromDevTools: false,
+ });
+}
+
+let propNameAllowlist = [
+ // 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 },
+ { propName: "--highlighter-font-family", 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 },
+
+ // These variables are specified from devtools but read from non-devtools
+ // styles, which confuses the test.
+ { propName: "--panel-border-radius", isFromDevTools: true },
+ { propName: "--panel-padding", isFromDevTools: true },
+ { propName: "--panel-background", isFromDevTools: true },
+ { propName: "--panel-border-color", isFromDevTools: true },
+ { propName: "--panel-shadow", isFromDevTools: true },
+ { propName: "--panel-shadow-margin", isFromDevTools: true },
+];
+
+// 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 dumpAllowlistItem(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 allowlist
+ * objects.
+ *
+ * @param aErrorObject the error to check
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(aErrorObject) {
+ for (let allowlistItem of ignoreList) {
+ let matches = true;
+ let catchAll = true;
+ for (let prop of ["sourceName", "errorMessage"]) {
+ if (allowlistItem.hasOwnProperty(prop)) {
+ catchAll = false;
+ if (!allowlistItem[prop].test(aErrorObject[prop] || "")) {
+ matches = false;
+ break;
+ }
+ }
+ }
+ if (catchAll) {
+ ok(
+ false,
+ "An allowlist item is catching all errors. " +
+ dumpAllowlistItem(allowlistItem)
+ );
+ continue;
+ }
+ if (matches) {
+ allowlistItem.used = true;
+ let { sourceName, errorMessage } = aErrorObject;
+ info(
+ `Ignored error "${errorMessage}" on ${sourceName} ` +
+ "because of allowlist item " +
+ dumpAllowlistItem(allowlistItem)
+ );
+ 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 allowlisted in allowlist
+ 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)"],
+ 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, style rules with nested rules.
+ }
+ 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\(\s*|\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).trim();
+ 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 propNameAllowlist) {
+ 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 allowlist rules have been used.
+ function checkAllowlist(list) {
+ for (let item of list) {
+ if (
+ !item.used &&
+ isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform)) &&
+ !item.intermittent
+ ) {
+ ok(false, "Unused allowlist item: " + dumpAllowlistItem(item));
+ }
+ }
+ }
+ checkAllowlist(ignoreList);
+ checkAllowlist(propNameAllowlist);
+
+ // 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..d4dcbd87fe
--- /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 kAllowlist = 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 allowlist
+ * objects.
+ *
+ * @param uri the uri to check against the allowlist
+ * @return true if the uri should be skipped, false otherwise.
+ */
+function uriIsAllowed(uri) {
+ for (let allowlistItem of kAllowlist) {
+ if (allowlistItem.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 allowlistItem of kESModuleList) {
+ if (allowlistItem.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 (uriIsAllowed(uri)) {
+ info("Not checking allowlisted " + 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.toml b/browser/base/content/test/statuspanel/browser.toml
new file mode 100644
index 0000000000..3c08e73912
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser.toml
@@ -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..308321f8c2
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+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>`
+);
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_STATUS_TEXT = UrlbarTestUtils.trimURL("http://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.toml b/browser/base/content/test/sync/browser.toml
new file mode 100644
index 0000000000..caed29af35
--- /dev/null
+++ b/browser/base/content/test/sync/browser.toml
@@ -0,0 +1,16 @@
+[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..a80cf8a1d0
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -0,0 +1,474 @@
+/* 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"
+ );
+
+ if (
+ Services.prefs.getBoolPref("privacy.query_stripping.strip_on_share.enabled")
+ ) {
+ expectedArray.push("context-stripOnShareLink");
+ }
+
+ expectedArray.push(
+ "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..c232f26f26
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
+ ON_PROFILE_CHANGE_NOTIFICATION:
+ "resource://gre/modules/FxAccountsCommon.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(
+ 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..168c6f22b0
--- /dev/null
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -0,0 +1,935 @@
+/* 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.isVisible(button),
+ "Check button visibility with STATUS_NOT_CONFIGURED"
+ );
+
+ state.email = "foo@bar.com";
+ state.status = UIState.STATUS_NOT_VERIFIED;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.isVisible(button),
+ "Check button visibility with STATUS_NOT_VERIFIED"
+ );
+
+ state.status = UIState.STATUS_LOGIN_FAILED;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.isVisible(button),
+ "Check button visibility with STATUS_LOGIN_FAILED"
+ );
+
+ state.status = UIState.STATUS_SIGNED_IN;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.isVisible(button),
+ "Check button visibility with STATUS_SIGNED_IN"
+ );
+
+ state.syncEnabled = false;
+ gSync.updateAllUI(state);
+ is(
+ BrowserTestUtils.isVisible(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.isVisible(button),
+ "Button should still be visable even if user sync 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();
+ ok(
+ BrowserTestUtils.isVisible(
+ document.getElementById("fxa-menu-header-title")
+ ),
+ "expected toolbar to be visible after opening"
+ );
+ checkFxaToolbarButtonPanel({
+ headerTitle: "Manage account",
+ headerDescription: state.displayName,
+ 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",
+ 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_signed_in() {
+ 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",
+ 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",
+ titleHidden: true,
+ hideFxAText: true,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_signed_in_no_display_name() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: false,
+ email: "foo@bar.com",
+ 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",
+ displayName: "Foo Bar",
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ const expectedLabel = gSync.fluentStrings.formatValueSync(
+ "account-disconnected2"
+ );
+
+ checkMenuBarItem("sync-reauthitem");
+ checkPanelHeader();
+ checkFxaToolbarButtonPanel({
+ headerTitle: expectedLabel,
+ headerDescription: state.displayName,
+ 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.displayName,
+ 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);
+ }
+);
+
+// If the PXI experiment is enabled, we need to ensure we can see the CTAs when signed out
+add_task(async function test_experiment_ui_state_unconfigured() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ // The experiment enables this bool, found in FeatureManifest.yaml
+ Services.prefs.setBoolPref(
+ "identity.fxaccounts.toolbar.pxiToolbarEnabled",
+ true
+ );
+ let state = {
+ status: UIState.STATUS_NOT_CONFIGURED,
+ };
+
+ gSync.updateAllUI(state);
+
+ checkMenuBarItem("sync-setup");
+
+ checkFxAAvatar("not_configured");
+
+ let expectedLabel = gSync.fluentStrings.formatValueSync(
+ "appmenuitem-sign-in-account"
+ );
+
+ await openMainPanel();
+
+ checkFxaToolbarButtonPanel({
+ headerTitle: expectedLabel,
+ headerDescription: "",
+ enabledItems: [
+ "PanelUI-fxa-cta-menu",
+ "PanelUI-fxa-menu-sync-button",
+ "PanelUI-fxa-menu-monitor-button",
+ "PanelUI-fxa-menu-relay-button",
+ "PanelUI-fxa-menu-vpn-button",
+ ],
+ disabledItems: [],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+
+ // Revert the pref at the end of the test
+ Services.prefs.setBoolPref(
+ "identity.fxaccounts.toolbar.pxiToolbarEnabled",
+ false
+ );
+ await closeTabAndMainPanel();
+});
+
+// Ensure we can see the regular signed in flow + the extra PXI CTAs when
+// the experiment is enabled
+add_task(async function test_experiment_ui_state_signedin() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ // The experiment enables this bool, found in FeatureManifest.yaml
+ Services.prefs.setBoolPref(
+ "identity.fxaccounts.toolbar.pxiToolbarEnabled",
+ true
+ );
+
+ 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();
+ ok(
+ BrowserTestUtils.isVisible(
+ document.getElementById("fxa-menu-header-title")
+ ),
+ "expected toolbar to be visible after opening"
+ );
+ checkFxaToolbarButtonPanel({
+ headerTitle: "Manage account",
+ headerDescription: state.displayName,
+ 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",
+ "PanelUI-fxa-cta-menu",
+ "PanelUI-fxa-menu-sync-button",
+ "PanelUI-fxa-menu-monitor-button",
+ "PanelUI-fxa-menu-relay-button",
+ "PanelUI-fxa-menu-vpn-button",
+ ],
+ disabledItems: [],
+ hiddenItems: ["PanelUI-fxa-menu-setup-sync-button"],
+ });
+ checkFxAAvatar("signedin");
+ gSync.relativeTimeFormat = origRelativeTimeFormat;
+ await closeFxaPanel();
+
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ description: "Foo Bar",
+ titleHidden: true,
+ hideFxAText: true,
+ });
+
+ // Revert the pref at the end of the test
+ Services.prefs.setBoolPref(
+ "identity.fxaccounts.toolbar.pxiToolbarEnabled",
+ false
+ );
+ await closeTabAndMainPanel();
+});
+
+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-account-header"),
+ "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.isVisible(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")',
+ };
+ Assert.equal(
+ 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";
+ Assert.equal(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.toml b/browser/base/content/test/tabMediaIndicator/browser.toml
new file mode 100644
index 0000000000..9eac10b029
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser.toml
@@ -0,0 +1,44 @@
+[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..89143bf837
--- /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.startLoadingURIString(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..fb771c23a7
--- /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.startLoadingURIString(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.startLoadingURIString(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..6c0c9de2b9
--- /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.startLoadingURIString(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..d360edd60b
--- /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.startLoadingURIString(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..882a9bc804
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_sound_indicator_silent_video.js
@@ -0,0 +1,95 @@
+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;
+ Assert.less(
+ 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.startLoadingURIString(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.startLoadingURIString(
+ 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..7cdc87020a
--- /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.startLoadingURIString(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.toml b/browser/base/content/test/tabPrompts/browser.toml
new file mode 100644
index 0000000000..037f1f0d2b
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser.toml
@@ -0,0 +1,50 @@
+[DEFAULT]
+
+["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_close_event.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..86d7c992c5
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+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,
+ UrlbarTestUtils.trimURL(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,
+ UrlbarTestUtils.trimURL("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,
+ UrlbarTestUtils.trimURL(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,
+ UrlbarTestUtils.trimURL(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,
+ UrlbarTestUtils.trimURL(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,
+ UrlbarTestUtils.trimURL(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,
+ UrlbarTestUtils.trimURL(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..f955999e8b
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+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,
+ UrlbarTestUtils.trimURL(AUTH_URL),
+ "url bar copy value set"
+ );
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ });
+
+ // select only part of the URL
+ gURLBar.focus();
+ let endOfSelectionRange =
+ UrlbarTestUtils.trimURL(AUTH_URL).indexOf("/auth-route.sjs");
+
+ let isProtocolTrimmed = AUTH_URL.startsWith(
+ UrlbarTestUtils.getTrimmedProtocolWithSlashes()
+ );
+ await SimpleTest.promiseClipboardChange(
+ AUTH_URL.substring(
+ 0,
+ endOfSelectionRange +
+ (isProtocolTrimmed
+ ? UrlbarTestUtils.getTrimmedProtocolWithSlashes().length
+ : 0)
+ ),
+ () => {
+ Assert.equal(
+ gURLBar.value,
+ UrlbarTestUtils.trimURL(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,
+ UrlbarTestUtils.trimURL(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,
+ UrlbarTestUtils.trimURL(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() {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.trimHttps", false],
+ ["browser.urlbar.trimURLs", true],
+ ],
+ });
+ await trigger401AndHandle();
+ SpecialPowers.popPrefEnv();
+
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.trimHttps", true],
+ ["browser.urlbar.trimURLs", true],
+ ],
+ });
+ 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..829fa91f75
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js
@@ -0,0 +1,100 @@
+/* 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");
+ // We intentionally turn off a11y_checks for the following click, because
+ // it is send to prepare the URL Bar for the mouse-specific action - for a
+ // drag event, while there are other ways are accessible for users of
+ // assistive technology and keyboards, therefore this test can be excluded
+ // from the accessibility tests.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ urlBarContainer.click();
+ AccessibilityUtils.resetEnv();
+ // 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..52044b5874
--- /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.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..10c8809490
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_contentOrigins.js
@@ -0,0 +1,219 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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.isVisible(titleEl), "New title should be shown.");
+ ok(
+ BrowserTestUtils.isHidden(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.startLoadingURIString(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..6b116b71f9
--- /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;
+
+ Assert.strictEqual(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_close_event.js b/browser/base/content/test/tabPrompts/browser_prompt_close_event.js
new file mode 100644
index 0000000000..9705062cef
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_prompt_close_event.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Execute the provided script content by generating a dynamic script tag and
+ * inserting it in the page for the current selected browser.
+ *
+ * @param {string} script
+ * The script to execute.
+ * @returns {Promise}
+ * A promise that resolves when the script node was added and removed from
+ * the content page.
+ */
+function createScriptNode(script) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [script],
+ function (_script) {
+ const scriptTag = content.document.createElement("script");
+ scriptTag.append(content.document.createTextNode(_script));
+ content.document.body.append(scriptTag);
+ }
+ );
+}
+
+add_task(
+ async function test_close_prompt_event_detail_accepted_with_default_value() {
+ const closed = BrowserTestUtils.waitForEvent(
+ window,
+ "DOMModalDialogClosed"
+ );
+
+ const promptPromise = BrowserTestUtils.promiseAlertDialogOpen();
+
+ const defaultValue = "Default value";
+ await createScriptNode(
+ `setTimeout(() => window.prompt('Enter your name:', '${defaultValue}'))`
+ );
+ const promptWin = await promptPromise;
+
+ info("accepting prompt with default value.");
+ promptWin.document.querySelector("dialog").acceptDialog();
+
+ const closedEvent = await closed;
+
+ is(
+ closedEvent.detail.areLeaving,
+ true,
+ "Received correct `areLeaving` value in the event detail"
+ );
+ is(
+ closedEvent.detail.value,
+ defaultValue,
+ "Received correct `value` value in the event detail"
+ );
+ }
+);
+
+add_task(
+ async function test_close_prompt_event_detail_accepted_with_amended_default_value() {
+ const closed = BrowserTestUtils.waitForEvent(
+ window,
+ "DOMModalDialogClosed"
+ );
+
+ const promptPromise = BrowserTestUtils.promiseAlertDialogOpen();
+
+ const defaultValue = "Default value";
+ await createScriptNode(
+ `setTimeout(() => window.prompt('Enter your name:', '${defaultValue}'))`
+ );
+ const promptWin = await promptPromise;
+
+ const amendedValue = "Test";
+ promptWin.document.getElementById("loginTextbox").value = amendedValue;
+
+ info("accepting prompt with amended value.");
+ promptWin.document.querySelector("dialog").acceptDialog();
+
+ const closedEvent = await closed;
+
+ is(
+ closedEvent.detail.areLeaving,
+ true,
+ "Received correct `areLeaving` value in the event detail"
+ );
+ is(
+ closedEvent.detail.value,
+ amendedValue,
+ "Received correct `value` value in the event detail"
+ );
+ }
+);
+
+add_task(
+ async function test_close_prompt_event_detail_dismissed_with_default_value() {
+ const closed = BrowserTestUtils.waitForEvent(
+ window,
+ "DOMModalDialogClosed"
+ );
+
+ const promptPromise = BrowserTestUtils.promiseAlertDialogOpen();
+
+ const defaultValue = "Default value";
+ await createScriptNode(
+ `setTimeout(() => window.prompt('Enter your name:', '${defaultValue}'))`
+ );
+ const promptWin = await promptPromise;
+
+ info("Dismissing prompt with default value.");
+ promptWin.document.querySelector("dialog").cancelDialog();
+
+ const closedEvent = await closed;
+
+ is(
+ closedEvent.detail.areLeaving,
+ false,
+ "Received correct `areLeaving` value in the event detail"
+ );
+ is(
+ closedEvent.detail.value,
+ null,
+ "Received correct `value` value in the event detail"
+ );
+ }
+);
+
+add_task(
+ async function test_close_prompt_event_detail_dismissed_with_amended_default_value() {
+ const closed = BrowserTestUtils.waitForEvent(
+ window,
+ "DOMModalDialogClosed"
+ );
+
+ const promptPromise = BrowserTestUtils.promiseAlertDialogOpen();
+
+ const defaultValue = "Default value";
+ await createScriptNode(
+ `setTimeout(() => window.prompt('Enter your name:', '${defaultValue}'))`
+ );
+ const promptWin = await promptPromise;
+
+ const amendedValue = "Test";
+ promptWin.document.getElementById("loginTextbox").value = amendedValue;
+
+ info("Dismissing prompt with amended value.");
+ promptWin.document.querySelector("dialog").cancelDialog();
+
+ const closedEvent = await closed;
+
+ is(
+ closedEvent.detail.areLeaving,
+ false,
+ "Received correct `areLeaving` value in the event detail"
+ );
+ is(
+ closedEvent.detail.value,
+ null,
+ "Received correct `value` value in the event detail"
+ );
+ }
+);
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..4c77a51275
--- /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.startLoadingURIString(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.toml b/browser/base/content/test/tabcrashed/browser.toml
new file mode 100644
index 0000000000..a00c5f741b
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser.toml
@@ -0,0 +1,29 @@
+[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.toml b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml
new file mode 100644
index 0000000000..88988434fb
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml
@@ -0,0 +1,21 @@
+[DEFAULT]
+run-if = ["crashreporter"]
+skip-if = ["!debug"]
+
+support-files = ["head.js"]
+prefs = [
+ "dom.ipc.processCount=1",
+ "dom.ipc.processPrelaunch.fission.number=0",
+]
+
+["browser_aboutRestartRequired_basic.js"]
+
+# Bug 1876056: re-enable once bug 1877361 is fixed
+#["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_aboutRestartRequired_noForkServer.toml b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_noForkServer.toml
new file mode 100644
index 0000000000..ec045ddf79
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_noForkServer.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+run-if = ["crashreporter"]
+skip-if = ["!debug"]
+
+support-files = ["head.js"]
+prefs = [
+ "dom.ipc.processCount=1",
+ "dom.ipc.processPrelaunch.fission.number=0",
+ "dom.ipc.forkserver.enable=false",
+]
+
+# Bug 1876056: remove once bug 1877361 is fixed
+["browser_aboutRestartRequired_buildid_false-positive.js"]
+skip-if = ["win11_2009 && msix && debug"] # bug 1823581
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..83fc5e157d
--- /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.isVisible(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..bb57c85d6d
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/head.js
@@ -0,0 +1,242 @@
+"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"
+ );
+ Assert.strictEqual(
+ currPrefValue,
+ origPrefValue,
+ "processPrelaunch properly re-enabled"
+ );
+}
diff --git a/browser/base/content/test/tabdialogs/browser.toml b/browser/base/content/test/tabdialogs/browser.toml
new file mode 100644
index 0000000000..2029209cbd
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser.toml
@@ -0,0 +1,20 @@
+[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
+]
+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..4d41a76482
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_multiple_dialog_navigation.js
@@ -0,0 +1,64 @@
+/* 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.startLoadingURIString(
+ 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..20d8496681
--- /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.startLoadingURIString(
+ 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..50b94e1a36
--- /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.isVisible(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..db3ac81aee
--- /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.isHidden(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..fd75ed4dcf
--- /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.startLoadingURIString(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.isHidden(dialogBoxManager._dialogStack),
+ "Dialog stack is showing"
+ );
+
+ dialogBoxManager.hideDialog(browser);
+
+ is(
+ dialogBoxManager._dialogs.length,
+ 2,
+ "Dialog manager still has two dialogs."
+ );
+
+ ok(
+ BrowserTestUtils.isHidden(dialogBoxManager._dialogStack),
+ "Dialog stack is hidden"
+ );
+
+ // Navigate to a different page
+ BrowserTestUtils.startLoadingURIString(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.toml b/browser/base/content/test/tabs/browser.toml
new file mode 100644
index 0000000000..8008d70f0c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser.toml
@@ -0,0 +1,345 @@
+[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",
+]
+prefs = [
+ "browser.sessionstore.closedTabsFromAllWindows=true",
+ "browser.sessionstore.closedTabsFromClosedWindows=true",
+]
+
+["browser_addAdjacentNewTab.js"]
+
+["browser_addTab_index.js"]
+
+["browser_adoptTab_failure.js"]
+
+["browser_allow_process_switches_despite_related_browser.js"]
+
+["browser_audioTabIcon.js"]
+tags = "audiochannel"
+skip-if = [
+ "apple_silicon && !debug" # Bug 1862716
+]
+
+["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_bookmarks_toolbar_height.js"]
+skip-if = ["!verify && os == 'mac'"] # Bug 1872477
+support-files = ["file_observe_height_changes.html"]
+
+["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_openURI_background.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"]
+fail-if = ["a11y_checks"] # Bugs 1854233 and 1873049 scrollbutton-down/up are not labeled
+skip-if = [
+ "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'", #Bug 1421183, disabled on Linux/OSX for leaked windows
+ "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 = ["true"] # Bug 1616418 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_preview.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"]
+
+["browser_window_open_modifiers.js"]
+support-files = ["file_window_open.html"]
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..b782c3aada
--- /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.startLoadingURIString(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..53b5140abb
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_audioTabIcon.js
@@ -0,0 +1,684 @@
+/* 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 should still be muted (attribute check)");
+ ok(
+ !tab.hasAttribute("soundplaying"),
+ "Tab should not be playing (attribute check)"
+ );
+ ok(tab.muted, "Tab should still be muted (property check)");
+ ok(!tab.soundPlaying, "Tab should not be playing (property check)");
+
+ await test_tooltip(icon, "Unmute tab", isActiveTab, tab);
+
+ await test_mute_tab(tab, icon, false);
+
+ ok(
+ !tab.hasAttribute("muted"),
+ "Tab should not be be muted (attribute check)"
+ );
+ ok(
+ !tab.hasAttribute("soundplaying"),
+ "Tab should not be be playing (attribute check)"
+ );
+ ok(!tab.muted, "Tab should not be be muted (property check)");
+ ok(!tab.soundPlaying, "Tab should not be be playing (property check)");
+
+ // 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"
+ );
+ ok(
+ tabContainer.hasAttribute("hiddensoundplaying"),
+ "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.startLoadingURIString(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;
+ is(
+ tab.hasAttribute("indicator-replaces-favicon"),
+ !tab.pinned,
+ "Mute indicator should replace the favicon on hover if the tab isn't pinned"
+ );
+ 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..6eb9298ea6
--- /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.startLoadingURIString(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..511c2ea03e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_bug580956.js
@@ -0,0 +1,25 @@
+function numClosedTabs() {
+ return SessionStore.getClosedTabCount();
+}
+
+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..32bbb65b62
--- /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.startLoadingURIString(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..26e7cd617a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js
@@ -0,0 +1,63 @@
+"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.startLoadingURIString(
+ 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..b35170ad1e
--- /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.startLoadingURIString(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.startLoadingURIString(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..4610551977
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js
@@ -0,0 +1,210 @@
+"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 |
+ Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT
+ ),
+ 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..c7590a0954
--- /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.startLoadingURIString(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.startLoadingURIString(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..36d6bfbece
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_switchbrowser.js
@@ -0,0 +1,493 @@
+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.startLoadingURIString(
+ 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.startLoadingURIString(
+ 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..f01d36fa16
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.js
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+/* 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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(BLANK_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(BLANK_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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..f8773e3720
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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".
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+/* 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: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(REQUEST_TIMEOUT_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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..07cf7a8ea2
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// 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
+);
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+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: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(BLANK_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(BLANK_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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..ab18d7c7e0
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// 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
+);
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: HOME_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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..7dc0e8fa45
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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".
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+/* 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: UrlbarTestUtils.trimURL(BLANK_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(BLANK_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: BLANK_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(BLANK_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: WAIT_A_BIT_PAGE_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Abort loading");
+ document.getElementById("stop-button").click();
+ },
+ finalState: {
+ tab: WAIT_A_BIT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(HOME_URL),
+ },
+ async actionWhileLoading(onTabLoaded) {
+ info("Wait until loading the link target");
+ await onTabLoaded;
+ },
+ finalState: {
+ tab: REQUEST_TIMEOUT_LOADING_TITLE,
+ urlbar: UrlbarTestUtils.trimURL(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..d9d0728ecb
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_navigatePinnedTab.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ // Test 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/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ appTab.linkedBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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..5b50cc615f
--- /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}`);
+ BrowserTestUtils.startLoadingURIString(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_bookmarks_toolbar_height.js b/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js
new file mode 100644
index 0000000000..66258659fd
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Tests that showing the bookmarks toolbar for new tabs only doesn't affect
+// the view port height in background tabs.
+
+let gHeightChanges = 0;
+async function expectHeightChanges(tab, expectedNewHeightChanges, msg) {
+ let contentObservedHeightChanges = await ContentTask.spawn(
+ tab.linkedBrowser,
+ null,
+ async args => {
+ await new Promise(resolve => content.requestAnimationFrame(resolve));
+ return content.document.body.innerText;
+ }
+ );
+ is(
+ contentObservedHeightChanges - gHeightChanges,
+ expectedNewHeightChanges,
+ msg
+ );
+ gHeightChanges = contentObservedHeightChanges;
+}
+
+async function expectBmToolbarVisibilityChange(triggerFn, visible, msg) {
+ let collapsedState = BrowserTestUtils.waitForMutationCondition(
+ BookmarkingUI.toolbar,
+ { attributes: true, attributeFilter: ["collapsed"] },
+ () => BookmarkingUI.toolbar.collapsed != visible
+ );
+ let toolbarItemsVisibilityUpdated = visible
+ ? BrowserTestUtils.waitForEvent(
+ BookmarkingUI.toolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ )
+ : null;
+ triggerFn();
+ await collapsedState;
+ is(
+ BookmarkingUI.toolbar.getAttribute("collapsed"),
+ (!visible).toString(),
+ `${msg}; collapsed attribute state`
+ );
+ if (visible) {
+ info(`${msg}; waiting for toolbar items to become visible`);
+ await toolbarItemsVisibilityUpdated;
+ isnot(
+ BookmarkingUI.toolbar.getBoundingClientRect().height,
+ 0,
+ `${msg}; should have a height`
+ );
+ } else {
+ is(
+ BookmarkingUI.toolbar.getBoundingClientRect().height,
+ 0,
+ `${msg}; should have zero height`
+ );
+ }
+}
+
+add_task(async function () {
+ registerCleanupFunction(() => {
+ setToolbarVisibility(
+ BookmarkingUI.toolbar,
+ gBookmarksToolbarVisibility,
+ false,
+ false
+ );
+ });
+
+ await expectBmToolbarVisibilityChange(
+ () => setToolbarVisibility(BookmarkingUI.toolbar, false, false, false),
+ false,
+ "bookmarks toolbar is hidden initially"
+ );
+
+ let pageURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ pageURL = `${pageURL}file_observe_height_changes.html`;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL);
+
+ await expectBmToolbarVisibilityChange(
+ () => setToolbarVisibility(BookmarkingUI.toolbar, true, false, false),
+ true,
+ "bookmarks toolbar is visible after explicitly showing it for tab with content loaded"
+ );
+ await expectHeightChanges(
+ tab,
+ 1,
+ "content area height changes when showing the toolbar without the animation"
+ );
+
+ await expectBmToolbarVisibilityChange(
+ () => setToolbarVisibility(BookmarkingUI.toolbar, "newtab", false, false),
+ false,
+ "bookmarks toolbar is hidden for non-new tab after setting it to only show for new tabs"
+ );
+ await expectHeightChanges(
+ tab,
+ 1,
+ "content area height changes when hiding the toolbar without the animation"
+ );
+
+ info("Opening a new tab, making the previous tab non-selected");
+ await expectBmToolbarVisibilityChange(
+ () => {
+ BrowserOpenTab();
+ ok(
+ !tab.selected,
+ "non-new tab is in the background (not the selected tab)"
+ );
+ },
+ true,
+ "bookmarks toolbar is visible for new tab after setting it to only show for new tabs"
+ );
+ await expectHeightChanges(
+ tab,
+ 0,
+ "no additional content area height change in background tab when showing the bookmarks toolbar in new tab"
+ );
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeTab(tab);
+});
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..ec11951cb0
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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_openURI_background.js b/browser/base/content/test/tabs/browser_openURI_background.js
new file mode 100644
index 0000000000..53e329dd8f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_openURI_background.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/. */
+
+"use strict";
+
+add_task(async function () {
+ const tabCount = gBrowser.tabs.length;
+ const currentTab = gBrowser.selectedTab;
+
+ const tests = [
+ ["OPEN_NEWTAB", false],
+ ["OPEN_NEWTAB_BACKGROUND", true],
+ ];
+
+ for (const [flag, isBackground] of tests) {
+ window.browserDOMWindow.openURI(
+ makeURI("about:blank"),
+ null,
+ Ci.nsIBrowserDOMWindow[flag],
+ Ci.nsIBrowserDOMWindow.OPEN_NEW,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+
+ is(gBrowser.tabs.length, tabCount + 1, `${flag} opens a new tab`);
+
+ const openedTab = gBrowser.tabs[tabCount];
+
+ if (isBackground) {
+ is(
+ gBrowser.selectedTab,
+ currentTab,
+ `${flag} opens a new background tab`
+ );
+ } else {
+ is(gBrowser.selectedTab, openedTab, `${flag} opens a new foreground tab`);
+ }
+
+ gBrowser.removeTab(openedTab);
+ }
+});
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..baf85d0025
--- /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.startLoadingURIString(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..8e88be644b
--- /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.startLoadingURIString(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.startLoadingURIString(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..cae033e3bf
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_overflowScroll.js
@@ -0,0 +1,114 @@
+"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();
+
+ Assert.lessOrEqual(
+ 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();
+ Assert.lessOrEqual(
+ 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());
+ Assert.lessOrEqual(
+ 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..9e1c1ff5cd
--- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..69aa7f8f8c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js
@@ -0,0 +1,95 @@
+/* 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");
+const IS_UPGRADING_SCHEMELESS = SpecialPowers.getBoolPref(
+ "dom.security.https_first_schemeless"
+);
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const DEFAULT_URL_SCHEME = IS_UPGRADING_SCHEMELESS ? "https://" : "http://";
+
+add_setup(async function () {
+ 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,
+ DEFAULT_URL_SCHEME + 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..83002a749d
--- /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.startLoadingURIString(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..ecdbe8f5eb
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js
@@ -0,0 +1,118 @@
+/* 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.startLoadingURIString(
+ 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..262d71162e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js
@@ -0,0 +1,102 @@
+"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);
+ Assert.greater(
+ 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..6bf15f5648
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_close.js
@@ -0,0 +1,82 @@
+/* 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.openNewBrowserWindow();
+ 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.openNewBrowserWindow();
+ 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..b00a09fcdb
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_drag.js
@@ -0,0 +1,257 @@
+/**
+ * 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.openNewBrowserWindow();
+
+ 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.openNewBrowserWindow();
+
+ 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..7ce50f20ec
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js
@@ -0,0 +1,37 @@
+/* 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.openNewBrowserWindow();
+ 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..b7de777512
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_manager_visibility.js
@@ -0,0 +1,53 @@
+/**
+ * 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.openNewBrowserWindow();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: newWindow.gBrowser,
+ url: TEST_HOSTNAME + DUMMY_PAGE_PATH,
+ },
+ async function (browser) {
+ await Assert.ok(
+ BrowserTestUtils.isVisible(
+ 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.openNewBrowserWindow();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: newWindow.gBrowser,
+ url: TEST_HOSTNAME + DUMMY_PAGE_PATH,
+ },
+ async function (browser) {
+ await Assert.ok(
+ BrowserTestUtils.isHidden(
+ 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_preview.js b/browser/base/content/test/tabs/browser_tab_preview.js
new file mode 100644
index 0000000000..e3dd1b6842
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_preview.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 openPreview(tab) {
+ const previewShown = BrowserTestUtils.waitForEvent(
+ document.getElementById("tabbrowser-tab-preview"),
+ "previewshown",
+ false,
+ e => {
+ return e.detail.tab === tab;
+ }
+ );
+ EventUtils.synthesizeMouseAtCenter(tab, { type: "mouseover" });
+ return previewShown;
+}
+
+async function closePreviews() {
+ const tabs = document.getElementById("tabbrowser-tabs");
+ const previewHidden = BrowserTestUtils.waitForEvent(
+ document.getElementById("tabbrowser-tab-preview"),
+ "previewhidden"
+ );
+ EventUtils.synthesizeMouse(tabs, 0, tabs.outerHeight + 1, {
+ type: "mouseout",
+ });
+ return previewHidden;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.cardPreview.enabled", true],
+ ["browser.tabs.cardPreview.showThumbnails", false],
+ ["browser.tabs.cardPreview.delayMs", 0],
+ ],
+ });
+});
+
+/**
+ * Verify the following:
+ *
+ * 1. Tab preview card appears when the mouse hovers over a tab
+ * 2. Tab preview card shows the correct preview for the tab being hovered
+ * 3. Tab preview card is dismissed when the mouse leaves the tab bar
+ */
+add_task(async () => {
+ const tabUrl1 =
+ "data:text/html,<html><head><title>First New Tab</title></head><body>Hello</body></html>";
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1);
+ const tabUrl2 =
+ "data:text/html,<html><head><title>Second New Tab</title></head><body>Hello</body></html>";
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2);
+ const previewContainer = document.getElementById("tabbrowser-tab-preview");
+
+ await openPreview(tab1);
+ Assert.ok(
+ ["open", "showing"].includes(previewContainer.panel.state),
+ "tab1 preview shown"
+ );
+ Assert.equal(
+ previewContainer.renderRoot.querySelector(".tab-preview-title").innerText,
+ "First New Tab",
+ "Preview of tab1 shows correct title"
+ );
+
+ await openPreview(tab2);
+ Assert.ok(
+ ["open", "showing"].includes(previewContainer.panel.state),
+ "tab2 preview shown"
+ );
+ Assert.equal(
+ previewContainer.renderRoot.querySelector(".tab-preview-title").innerText,
+ "Second New Tab",
+ "Preview of tab2 shows correct title"
+ );
+
+ await closePreviews();
+ Assert.ok(
+ ["closed", "hiding"].includes(previewContainer.panel.state),
+ "preview container is now hidden"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+/**
+ * Verify that non-selected tabs display a thumbnail in their preview
+ * when browser.tabs.cardPreview.showThumbnails is set to true,
+ * while the currently selected tab never displays a thumbnail in its preview.
+ */
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.cardPreview.showThumbnails", true]],
+ });
+ const tabUrl1 = "about:blank";
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1);
+ const tabUrl2 = "about:blank";
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2);
+ const previewContainer = document.getElementById("tabbrowser-tab-preview");
+
+ const thumbnailUpdated = BrowserTestUtils.waitForEvent(
+ previewContainer,
+ "previewThumbnailUpdated"
+ );
+ await openPreview(tab1);
+ await thumbnailUpdated;
+ Assert.ok(
+ previewContainer.renderRoot.querySelectorAll("img,canvas").length,
+ "Tab1 preview contains thumbnail"
+ );
+
+ await openPreview(tab2);
+ Assert.equal(
+ previewContainer.renderRoot.querySelectorAll("img,canvas").length,
+ 0,
+ "Tab2 (selected) does not contain thumbnail"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Wheel events at the document-level of the window should hide the preview.
+ */
+add_task(async () => {
+ const tabUrl1 = "about:blank";
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1);
+ const tabUrl2 = "about:blank";
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2);
+
+ await openPreview(tab1);
+
+ const tabs = document.getElementById("tabbrowser-tabs");
+ const previewHidden = BrowserTestUtils.waitForEvent(
+ document.getElementById("tabbrowser-tab-preview"),
+ "previewhidden"
+ );
+ EventUtils.synthesizeMouse(tabs, 0, tabs.outerHeight + 1, {
+ wheel: true,
+ deltaY: -1,
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ });
+ await previewHidden;
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ await SpecialPowers.popPrefEnv();
+});
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..ee82816bce
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tab_tooltips.js
@@ -0,0 +1,149 @@
+// 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"
+ );
+ Assert.greaterOrEqual(
+ 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"
+ );
+ Assert.greater(
+ 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);
+});
+
+// This test verifies that the tooltip in the tab manager panel matches the
+// tooltip in the tab strip.
+add_task(async function () {
+ // Open a new tab
+ const tabUrl = "https://example.com";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl);
+
+ // Make the popup of allTabs showing up
+ gTabsPanel.init();
+ let allTabsView = document.getElementById("allTabsMenu-allTabsView");
+ let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
+ allTabsView,
+ "ViewShown"
+ );
+ gTabsPanel.showAllTabsPanel();
+ await allTabsPopupShownPromise;
+
+ // Get tooltips and compare them
+ let tabInPanel = Array.from(
+ gTabsPanel.allTabsViewTabs.querySelectorAll(".all-tabs-button")
+ ).at(-1).label;
+ let tabInTabStrip = tab.getAttribute("label");
+
+ is(
+ tabInPanel,
+ tabInTabStrip,
+ "Tooltip in tab manager panel matches tooltip in tab strip"
+ );
+
+ // Close everything
+ let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(
+ allTabsView.panelMultiView,
+ "PanelMultiViewHidden"
+ );
+ gTabsPanel.hideAllTabsPanel();
+ await allTabsPopupHiddenPromise;
+
+ 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..b5ae94ce84
--- /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.startLoadingURIString(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/browser_window_open_modifiers.js b/browser/base/content/test/tabs/browser_window_open_modifiers.js
new file mode 100644
index 0000000000..b4376d6824
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_window_open_modifiers.js
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Opening many windows take long time on some configuration.
+requestLongerTimeout(4);
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/browser/base/content/test/tabs/file_window_open.html",
+ async function (browser) {
+ const metaKey = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
+ const normalEvent = {};
+ const shiftEvent = { shiftKey: true };
+ const metaEvent = { [metaKey]: true };
+ const metaShiftEvent = { [metaKey]: true, shiftKey: true };
+
+ const tests = [
+ // type, id, options, result
+ ["mouse", "#instant", normalEvent, "tab"],
+ ["mouse", "#instant", shiftEvent, "window"],
+ ["mouse", "#instant", metaEvent, "tab-bg"],
+ ["mouse", "#instant", metaShiftEvent, "tab"],
+
+ ["mouse", "#instant-popup", normalEvent, "popup"],
+ ["mouse", "#instant-popup", shiftEvent, "window"],
+ ["mouse", "#instant-popup", metaEvent, "tab-bg"],
+ ["mouse", "#instant-popup", metaShiftEvent, "tab"],
+
+ ["mouse", "#delayed", normalEvent, "tab"],
+ ["mouse", "#delayed", shiftEvent, "window"],
+ ["mouse", "#delayed", metaEvent, "tab-bg"],
+ ["mouse", "#delayed", metaShiftEvent, "tab"],
+
+ ["mouse", "#delayed-popup", normalEvent, "popup"],
+ ["mouse", "#delayed-popup", shiftEvent, "window"],
+ ["mouse", "#delayed-popup", metaEvent, "tab-bg"],
+ ["mouse", "#delayed-popup", metaShiftEvent, "tab"],
+
+ // NOTE: meta+keyboard doesn't activate.
+
+ ["VK_SPACE", "#instant", normalEvent, "tab"],
+ ["VK_SPACE", "#instant", shiftEvent, "window"],
+
+ ["VK_SPACE", "#instant-popup", normalEvent, "popup"],
+ ["VK_SPACE", "#instant-popup", shiftEvent, "window"],
+
+ ["VK_SPACE", "#delayed", normalEvent, "tab"],
+ ["VK_SPACE", "#delayed", shiftEvent, "window"],
+
+ ["VK_SPACE", "#delayed-popup", normalEvent, "popup"],
+ ["VK_SPACE", "#delayed-popup", shiftEvent, "window"],
+
+ ["KEY_Enter", "#link-instant", normalEvent, "tab"],
+ ["KEY_Enter", "#link-instant", shiftEvent, "window"],
+
+ ["KEY_Enter", "#link-instant-popup", normalEvent, "popup"],
+ ["KEY_Enter", "#link-instant-popup", shiftEvent, "window"],
+
+ ["KEY_Enter", "#link-delayed", normalEvent, "tab"],
+ ["KEY_Enter", "#link-delayed", shiftEvent, "window"],
+
+ ["KEY_Enter", "#link-delayed-popup", normalEvent, "popup"],
+ ["KEY_Enter", "#link-delayed-popup", shiftEvent, "window"],
+
+ // Trigger user-defined shortcut key, where modifiers shouldn't affect.
+
+ ["x", "#instant", normalEvent, "tab"],
+ ["x", "#instant", shiftEvent, "tab"],
+ ["x", "#instant", metaEvent, "tab"],
+ ["x", "#instant", metaShiftEvent, "tab"],
+
+ ["y", "#instant", normalEvent, "popup"],
+ ["y", "#instant", shiftEvent, "popup"],
+ ["y", "#instant", metaEvent, "popup"],
+ ["y", "#instant", metaShiftEvent, "popup"],
+ ];
+ for (const [type, id, event, result] of tests) {
+ const eventStr = JSON.stringify(event);
+
+ let openPromise;
+ if (result == "tab" || result == "tab-bg") {
+ openPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:blank",
+ true
+ );
+ } else {
+ openPromise = BrowserTestUtils.waitForNewWindow({
+ url: "about:blank",
+ });
+ }
+
+ if (type == "mouse") {
+ BrowserTestUtils.synthesizeMouseAtCenter(id, { ...event }, browser);
+ } else {
+ // Make sure the keyboard activates a simple button on the page.
+ await ContentTask.spawn(browser, id, elementId => {
+ content.document.querySelector("#focus-result").value = "";
+ content.document.querySelector("#focus-check").focus();
+ });
+ BrowserTestUtils.synthesizeKey("VK_SPACE", {}, browser);
+ await ContentTask.spawn(browser, {}, async () => {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#focus-result").value === "ok"
+ );
+ });
+
+ // Once confirmed the keyboard event works, send the actual event
+ // that calls window.open.
+ await ContentTask.spawn(browser, id, elementId => {
+ content.document.querySelector(elementId).focus();
+ });
+ BrowserTestUtils.synthesizeKey(type, { ...event }, browser);
+ }
+
+ const openedThing = await openPromise;
+
+ if (result == "tab" || result == "tab-bg") {
+ const newTab = openedThing;
+
+ if (result == "tab") {
+ Assert.equal(
+ gBrowser.selectedTab,
+ newTab,
+ `${id} with ${type} and ${eventStr} opened a foreground tab`
+ );
+ } else {
+ Assert.notEqual(
+ gBrowser.selectedTab,
+ newTab,
+ `${id} with ${type} and ${eventStr} opened a background tab`
+ );
+ }
+
+ gBrowser.removeTab(newTab);
+ } else {
+ const newWindow = openedThing;
+
+ const tabs = newWindow.document.getElementById("TabsToolbar");
+ if (result == "window") {
+ ok(
+ !tabs.collapsed,
+ `${id} with ${type} and ${eventStr} opened a regular window`
+ );
+ } else {
+ ok(
+ tabs.collapsed,
+ `${id} with ${type} and ${eventStr} opened a popup window`
+ );
+ }
+
+ const closedPopupPromise = BrowserTestUtils.windowClosed(newWindow);
+ newWindow.close();
+ await closedPopupPromise;
+
+ // Make sure the focus comes back to this window before proceeding
+ // to the next test.
+ if (Services.focus.focusedWindow != window) {
+ const focusBack = BrowserTestUtils.waitForEvent(
+ window,
+ "focus",
+ true
+ );
+ window.focus();
+ await focusBack;
+ }
+ }
+ }
+ }
+ );
+});
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..a06b982615
--- /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.startLoadingURIString(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.startLoadingURIString(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_observe_height_changes.html b/browser/base/content/test/tabs/file_observe_height_changes.html
new file mode 100644
index 0000000000..18b0fdf251
--- /dev/null
+++ b/browser/base/content/test/tabs/file_observe_height_changes.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+ /* This lets us measure the height of the viewport
+ by measuring documentElement.offsetHeight. */
+ html { height: 100vh }
+</style>
+<script>
+ let mostRecentHeight = 0;
+ let heightChanges = 0;
+ function checkDocumentHeight() {
+ let curHeight = document.documentElement.offsetHeight;
+ if (curHeight != mostRecentHeight) {
+ mostRecentHeight = curHeight;
+ document.body.innerText = heightChanges++;
+ }
+ requestAnimationFrame(checkDocumentHeight);
+ }
+</script>
+<body onload="checkDocumentHeight();">
+</body>
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/file_window_open.html b/browser/base/content/test/tabs/file_window_open.html
new file mode 100644
index 0000000000..831bafe6bd
--- /dev/null
+++ b/browser/base/content/test/tabs/file_window_open.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+ <title>window.open test</title>
+<style>
+div {
+ padding: 20px;
+}
+</style>
+</head>
+<body>
+ <div>
+ <input id="instant" type="button" value="instant no features"
+ onclick="window.open('about:blank', '_blank');">
+ </div>
+ <div>
+ <input id="instant-popup" type="button" value="instant popup"
+ onclick="window.open('about:blank', '_blank', 'popup=true');">
+ </div>
+ <div>
+ <input id="delayed" type="button" value="delayed no features"
+ onclick="setTimeout(() => window.open('about:blank', '_blank'), 100);">
+ </div>
+ <div>
+ <input id="delayed-popup" type="button" value="delayed popup"
+ onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100);">
+ <div>
+ <div>
+ <a id="link-instant" href=""
+ onclick="window.open('about:blank', '_blank'); event.preventDefault()">
+ instant no features
+ </a>
+ </div>
+ <div>
+ <a id="link-instant-popup" href=""
+ onclick="window.open('about:blank', '_blank', 'popup=true'); event.preventDefault()">
+ instant popup
+ </a>
+ </div>
+ <div>
+ <a id="link-delayed" href=""
+ onclick="setTimeout(() => window.open('about:blank', '_blank'), 100); event.preventDefault()">
+ delayed no features
+ </a>
+ </div>
+ <div>
+ <a id="link-delayed-popup" href=""
+ onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100); event.preventDefault()">
+ delayed popup
+ </a>
+ <div>
+ <div>
+ <input id="focus-check" type="button" value="check focus"
+ onclick="document.getElementById('focus-result').value = 'ok';">
+ </div>
+ <div>
+ <input id="focus-result" type="text" value="">
+
+<script type="text/javascript">
+document.addEventListener("keydown", event => {
+ if (event.key == "x") {
+ window.open('about:blank', '_blank');
+ }
+ if (event.key == "y") {
+ window.open('about:blank', '_blank', 'popup=true');
+ }
+});
+</script>
+</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..10ef8248c8
--- /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.startLoadingURIString(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.toml b/browser/base/content/test/touch/browser.toml
new file mode 100644
index 0000000000..0e368f83fd
--- /dev/null
+++ b/browser/base/content/test/touch/browser.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+["browser_menu_touch.js"]
+run-if = ["os == 'win'"]
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..385b050b37
--- /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
+add_setup(async function () {
+ let isWindows = AppConstants.platform == "win";
+ 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.toml b/browser/base/content/test/utilityOverlay/browser.toml
new file mode 100644
index 0000000000..f4b16b46a9
--- /dev/null
+++ b/browser/base/content/test/utilityOverlay/browser.toml
@@ -0,0 +1,3 @@
+[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.toml b/browser/base/content/test/webextensions/browser.toml
new file mode 100644
index 0000000000..6ea2421a74
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser.toml
@@ -0,0 +1,49 @@
+[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"]
+skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try)
+
+["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",
+ "a11y_checks", # Bugs 1858041 and 1854461 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try)
+]
+
+["browser_update_checkForUpdates.js"]
+
+["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..4e1fe07194
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js
@@ -0,0 +1,468 @@
+/* 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],
+ ],
+ });
+
+ Services.fog.testResetFOG();
+
+ 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.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function () {
+ // Return to about:blank when we're done
+ BrowserTestUtils.startLoadingURIString(
+ 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.
+ Assert.notEqual(
+ 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);
+
+ Assert.deepEqual(
+ AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID1 }),
+ [
+ {
+ addon_id: ID1,
+ method: "sideload_prompt",
+ addon_type: "extension",
+ source: "app-profile",
+ source_method: "sideload",
+ num_strings: "2",
+ },
+ {
+ addon_id: ID1,
+ method: "uninstall",
+ addon_type: "extension",
+ source: "app-profile",
+ source_method: "sideload",
+ },
+ ],
+ "Got the expected Glean events for addon1."
+ );
+
+ 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"
+ );
+
+ Assert.deepEqual(
+ AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID2 }),
+ [
+ {
+ addon_id: ID2,
+ method: "sideload_prompt",
+ addon_type: "extension",
+ source: "app-profile",
+ source_method: "sideload",
+ num_strings: "1",
+ },
+ {
+ addon_id: ID2,
+ method: "enable",
+ addon_type: "extension",
+ source: "app-profile",
+ source_method: "sideload",
+ },
+ {
+ addon_id: ID2,
+ method: "uninstall",
+ addon_type: "extension",
+ source: "app-profile",
+ source_method: "sideload",
+ },
+ ],
+ "Got the expected Glean 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..490544b2ec
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_update_background.js
@@ -0,0 +1,313 @@
+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.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function () {
+ // Return to about:blank when we're done
+ BrowserTestUtils.startLoadingURIString(
+ 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`,
+ ],
+ ],
+ });
+
+ Services.fog.testResetFOG();
+
+ // 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.
+ Assert.notEqual(
+ 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();
+
+ let gleanUpdates = AddonTestUtils.getAMGleanEvents("update");
+
+ // 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;
+ });
+
+ const expectedSteps = [
+ // First update (cancelled).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "cancelled",
+ // Second update (completed).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "completed",
+ ];
+
+ Assert.deepEqual(
+ expectedSteps,
+ updateEvents.map(evt => evt.extra && evt.extra.step),
+ "Got the steps from the collected telemetry events"
+ );
+
+ Assert.deepEqual(
+ expectedSteps,
+ gleanUpdates.map(evt => evt.step),
+ "Got the steps from the collected Glean 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"
+ );
+
+ Assert.deepEqual(
+ gleanUpdates.filter(e => e.step === "permissions_prompt"),
+ [
+ { ...baseExtra, addon_type: object, num_strings: "1" },
+ { ...baseExtra, addon_type: object, num_strings: "1" },
+ ],
+ "Got the expected permission_prompt events from Glean."
+ );
+}
+
+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..a0b10c82e2
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
@@ -0,0 +1,142 @@
+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.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "about:robots"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function () {
+ // Return to about:blank when we're done
+ BrowserTestUtils.startLoadingURIString(
+ 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`,
+ ],
+ ],
+ });
+ Services.fog.testResetFOG();
+
+ // 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;
+ });
+
+ let expected = [
+ "started",
+ "download_started",
+ "download_completed",
+ "completed",
+ ];
+ // Expect telemetry events related to a completed update with no permissions_prompt event.
+ Assert.deepEqual(
+ expected,
+ updateEventsSteps,
+ "Got the steps from the collected telemetry events"
+ );
+
+ Assert.deepEqual(
+ expected,
+ AddonTestUtils.getAMGleanEvents("update", { addon_id: id }).map(
+ e => e.step
+ ),
+ "Got the steps from the collected Glean 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..3cd916c3c5
--- /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.isVisible(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..a227518ebb
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js
@@ -0,0 +1,29 @@
+"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.startLoadingURIString(
+ 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..55a578221d
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js
@@ -0,0 +1,21 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+
+async function installMozAM(filename) {
+ BrowserTestUtils.startLoadingURIString(
+ 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..bffb671c8d
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_unsigned.js
@@ -0,0 +1,64 @@
+"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.startLoadingURIString(
+ 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();
+ const cancelledByUser = await promise;
+ is(cancelledByUser, true, "Install cancelled by user");
+
+ 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..0b0b912503
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
@@ -0,0 +1,80 @@
+// 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.startLoadingURIString(
+ 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..84f7cd02d7
--- /dev/null
+++ b/browser/base/content/test/webextensions/head.js
@@ -0,0 +1,679 @@
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+});
+
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+
+ChromeUtils.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);
+ Services.fog.testResetFOG();
+
+ 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.startLoadingURIString(
+ 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();
+ const cancelledByUser = await cancelPromise;
+ is(cancelledByUser, true, "Install cancelled by user");
+
+ 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";
+ }
+ );
+
+ const expectedSteps = [
+ // 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",
+ ];
+
+ Assert.deepEqual(
+ expectedSteps,
+ collectedUpdateEvents.map(evt => evt.extra.step),
+ "Got the expected sequence on update telemetry events"
+ );
+
+ let gleanEvents = AddonTestUtils.getAMGleanEvents("update");
+ Services.fog.testResetFOG();
+
+ Assert.deepEqual(
+ expectedSteps,
+ gleanEvents.map(e => e.step),
+ "Got the expected sequence on update Glean 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'"
+ );
+
+ for (let e of gleanEvents) {
+ is(e.addon_id, ID, "Glean event has the expected addon_id.");
+ is(e.source, FAKE_INSTALL_SOURCE, "Glean event has the expected source.");
+ is(e.updated_from, "user", "Glean event has the expected updated_from.");
+
+ if (e.step === "permissions_prompt") {
+ Assert.greater(parseInt(e.num_strings), 0, "Expected num_strings.");
+ }
+ if (e.step === "download_completed") {
+ Assert.greater(parseInt(e.download_time), 0, "Valid download_time.");
+ }
+ }
+
+ 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.toml b/browser/base/content/test/webrtc/browser.toml
new file mode 100644
index 0000000000..3f6fe38bf8
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser.toml
@@ -0,0 +1,159 @@
+[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
+]
+
+["browser_devices_get_user_media_anim.js"]
+https_first_disabled = true
+
+["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
+]
+
+["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
+]
+
+["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_macos_indicator_hiding.js"]
+run-if = [
+ "os == 'mac' && os_version >= '14.0'"
+]
+
+["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..1d23e56817
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js
@@ -0,0 +1,486 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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;
+ Assert.greater(
+ 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);
+ Assert.lessOrEqual(
+ 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..96e413ee1b
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -0,0 +1,889 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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: "'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.startLoadingURIString(
+ 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..d5c2cdca89
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
@@ -0,0 +1,771 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 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..134a2714f8
--- /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;
+ Assert.greaterOrEqual(
+ 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: [
+ ["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..181e18b179
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js
@@ -0,0 +1,250 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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: [
+ ["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..e78c075f4a
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
@@ -0,0 +1,893 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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;
+ Assert.greaterOrEqual(
+ 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;
+ }
+ }
+ Assert.equal(
+ 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;
+ Assert.greaterOrEqual(
+ 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"
+ );
+ }
+ Assert.equal(
+ 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(),
+ { window: true },
+ "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;
+ Assert.greaterOrEqual(
+ 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: "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..334397d58e
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js
@@ -0,0 +1,72 @@
+/* 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");
+ ok(aTab.hasAttribute("selected"), "Tab has attribute 'selected'");
+ ok(
+ aTab.hasAttribute("visuallyselected"),
+ "Tab has attribute '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..fbced1f5cc
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsIMediaDeviceQI = ChromeUtils.generateQI([Ci.nsIMediaDevice]);
+ const devices = [...Array(deviceCount).keys()].map(i => ({
+ type: "audiooutput",
+ rawName: `name ${i}`,
+ rawId: `rawId ${i}`,
+ id: `id ${i}`,
+ QueryInterface: nsIMediaDeviceQI,
+ }));
+ 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-richlistbox`
+ );
+ is(selectorList.selectedIndex, 2, "pre-selected index");
+ ok(selectorList.contains(document.activeElement), "richlistbox focus");
+ 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" }),
+ ]);
+ is(selectorList.selectedIndex, 1, "pre-selected index");
+ info("Expect allow from double click");
+ const targetIndex = 2;
+ const target = selectorList.getItemAtIndex(targetIndex);
+ EventUtils.synthesizeMouseAtCenter(target, { clickCount: 1 });
+ is(selectorList.selectedIndex, targetIndex, "selected index after click");
+ const messagePromise = promiseMessage();
+ const observerPromise = BrowserTestUtils.contentTopicObserved(
+ gBrowser.selectedBrowser.browsingContext,
+ "getUserMedia:response:allow",
+ 1,
+ (aSubject, aTopic, aData) => {
+ const device = aSubject
+ .QueryInterface(Ci.nsIArrayExtensions)
+ .GetElementAt(0).wrappedJSObject;
+ // `this` is the BrowserTestUtilsChild.
+ this.contentWindow.wrappedJSObject.message(device.id);
+ return true;
+ }
+ );
+ EventUtils.synthesizeMouseAtCenter(target, { clickCount: 2 });
+ await observerPromise;
+ const id = await messagePromise;
+ is(id, `id ${targetIndex}`, "selected device");
+
+ 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_macos_indicator_hiding.js b/browser/base/content/test/webrtc/browser_macos_indicator_hiding.js
new file mode 100644
index 0000000000..65110ae655
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_macos_indicator_hiding.js
@@ -0,0 +1,145 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * This test only runs for MacOS 14.0 and above to test the case for
+ * Bug 1857254 - MacOS 14 displays two camera in use icons in menu bar
+ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var systemStatusBarService = {
+ counter: 0,
+ _reset() {
+ this.counter = 0;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsISystemStatusBar"]),
+
+ addItem(element) {
+ info(
+ `Add item call was fired for element ${element}, updating counter from ${
+ this.counter
+ } to ${this.counter + 1}`
+ );
+ this.counter += 1;
+ },
+
+ removeItem(element) {
+ info(`remove item call was fired for element ${element}`);
+ },
+};
+
+/**
+ * Helper to test if the indicators are shown based on the params
+ *
+ * @param {Object}
+ * expectedCount (number) - expected number of indicators turned on
+ * cameraState (boolean) - if the camera indicator should be shown
+ * microphoneState (boolean) - if the microphone indicator should be shown
+ * screenShareState (string) - if the screen share indicator should be shown
+ * (SCREEN_SHARE or "")
+ */
+async function indicatorHelper({
+ expectedCount,
+ cameraState,
+ microphoneState,
+ shareScreenState,
+}) {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser, cameraState, microphoneState, shareScreenState);
+ await indicatorPromise;
+ });
+
+ is(
+ systemStatusBarService.counter,
+ expectedCount,
+ `${expectedCount} indicator(s) should be shown`
+ );
+
+ systemStatusBarService._reset();
+}
+
+add_task(async function testIconChanges() {
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.showIndicatorsOnMacos14AndAbove", false]],
+ });
+
+ let fakeStatusBarService = MockRegistrar.register(
+ "@mozilla.org/widget/systemstatusbar;1",
+ systemStatusBarService
+ );
+
+ systemStatusBarService._reset();
+
+ registerCleanupFunction(function () {
+ MockRegistrar.unregister(fakeStatusBarService);
+ });
+
+ info("Created mock system status bar service");
+
+ 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 indicatorHelper({
+ expectedCount: 0,
+ cameraState: true,
+ microphoneState: true,
+ shareScreenState: SHARE_SCREEN,
+ });
+ await indicatorHelper({
+ expectedCount: 0,
+ cameraState: true,
+ microphoneState: false,
+ shareScreenState: SHARE_SCREEN,
+ });
+
+ // In case we want to be able to see the indicator
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.showIndicatorsOnMacos14AndAbove", true]],
+ });
+
+ await indicatorHelper({
+ expectedCount: 3,
+ cameraState: true,
+ microphoneState: true,
+ shareScreenState: SHARE_SCREEN,
+ });
+ await indicatorHelper({
+ expectedCount: 1,
+ cameraState: false,
+ microphoneState: false,
+ shareScreenState: SHARE_SCREEN,
+ });
+ await indicatorHelper({
+ expectedCount: 1,
+ cameraState: true,
+ microphoneState: false,
+ shareScreenState: "",
+ });
+ await indicatorHelper({
+ expectedCount: 1,
+ cameraState: false,
+ microphoneState: true,
+ shareScreenState: "",
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
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..39f3de2794
--- /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.isHidden(
+ 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.isHidden(
+ 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.isHidden(
+ 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..633d640240
--- /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.isHidden(windowHeader),
+ "Should be showing window sharing header"
+ );
+ Assert.ok(
+ BrowserTestUtils.isHidden(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.isHidden(windowHeader),
+ "Should not be showing window sharing header"
+ );
+ Assert.ok(
+ !BrowserTestUtils.isHidden(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..3d6c9e3cb3
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media.html
@@ -0,0 +1,123 @@
+<!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) {
+ 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..2eda7406e0
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media2.html
@@ -0,0 +1,106 @@
+<!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) {
+ 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..13922b291f
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_frame.html
@@ -0,0 +1,97 @@
+<!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) {
+ 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..12b2cf52f7
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html
@@ -0,0 +1,70 @@
+<!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) {
+ 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.toml b/browser/base/content/test/webrtc/gracePeriod/browser.toml
new file mode 100644
index 0000000000..88dd800314
--- /dev/null
+++ b/browser/base/content/test/webrtc/gracePeriod/browser.toml
@@ -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"]
diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js
new file mode 100644
index 0000000000..639ae2e51a
--- /dev/null
+++ b/browser/base/content/test/webrtc/head.js
@@ -0,0 +1,1330 @@
+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 ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref(
+ "privacy.webrtc.allowSilencingNotifications",
+ false
+);
+
+const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref(
+ "privacy.webrtc.globalMuteToggles",
+ false
+);
+
+let IsIndicatorDisabled =
+ AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) &&
+ !Services.prefs.getBoolPref(
+ "privacy.webrtc.showIndicatorsOnMacos14AndAbove",
+ false
+ );
+
+const INDICATOR_PATH = "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();
+
+ 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.importESModule(
+ "resource:///modules/webrtcUI.sys.mjs"
+ ).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 && !IsIndicatorDisabled) {
+ 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 (!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 (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 (!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;
+
+ 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.window = true;
+ } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.browserwindow = true;
+ }
+
+ 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 tagName = type == "Speaker" ? "richlistbox" : "menulist";
+ let selectorList = document.getElementById(
+ `webRTC-select${type}-${tagName}`
+ );
+ 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.`);
+ let itemLabel =
+ tagName == "richlistbox"
+ ? selectorList.selectedItem.firstElementChild.getAttribute("value")
+ : selectorList.selectedItem.getAttribute("label");
+ is(
+ label.value,
+ itemLabel,
+ `${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 && !IsIndicatorDisabled) {
+ is(sharing, "screen", "showing screen icon in the identity block");
+ } else if (aExpected.video == STATE_CAPTURE_ENABLED && !IsIndicatorDisabled) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio == STATE_CAPTURE_ENABLED && !IsIndicatorDisabled) {
+ is(sharing, "microphone", "showing mic icon in the identity block");
+ } else if (aExpected.video && !IsIndicatorDisabled) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio && !IsIndicatorDisabled) {
+ 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/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.toml b/browser/base/content/test/zoom/browser.toml
new file mode 100644
index 0000000000..281fb9329c
--- /dev/null
+++ b/browser/base/content/test/zoom/browser.toml
@@ -0,0 +1,54 @@
+[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", # Bug 1315042
+ "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", # Bug 1652383
+ "!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..d757d8e7bf
--- /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));
+
+ Assert.greater(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..589e3d09cf
--- /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));
+
+ Assert.greater(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)
+ );
+
+ Assert.less(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..4a42aed98f
--- /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.startLoadingURIString(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>