summaryrefslogtreecommitdiffstats
path: root/browser/base/content
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/base/content
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content')
-rw-r--r--browser/base/content/.eslintrc.js16
-rw-r--r--browser/base/content/aboutDialog-appUpdater.js244
-rw-r--r--browser/base/content/aboutDialog.css139
-rw-r--r--browser/base/content/aboutDialog.js117
-rw-r--r--browser/base/content/aboutDialog.xhtml174
-rw-r--r--browser/base/content/aboutFrameCrashed.html17
-rw-r--r--browser/base/content/aboutNetError.js1265
-rw-r--r--browser/base/content/aboutNetError.xhtml216
-rw-r--r--browser/base/content/aboutRestartRequired.js39
-rw-r--r--browser/base/content/aboutRestartRequired.xhtml42
-rw-r--r--browser/base/content/aboutRobots-icon.pngbin0 -> 7599 bytes
-rw-r--r--browser/base/content/aboutRobots.css7
-rw-r--r--browser/base/content/aboutRobots.js15
-rw-r--r--browser/base/content/aboutRobots.xhtml62
-rw-r--r--browser/base/content/aboutTabCrashed.css11
-rw-r--r--browser/base/content/aboutTabCrashed.js306
-rw-r--r--browser/base/content/aboutTabCrashed.xhtml86
-rw-r--r--browser/base/content/blockedSite.js172
-rw-r--r--browser/base/content/blockedSite.xhtml62
-rw-r--r--browser/base/content/browser-a11yUtils.js80
-rw-r--r--browser/base/content/browser-addons.js1110
-rw-r--r--browser/base/content/browser-allTabsMenu.inc.xhtml45
-rw-r--r--browser/base/content/browser-allTabsMenu.js180
-rw-r--r--browser/base/content/browser-captivePortal.js352
-rw-r--r--browser/base/content/browser-context.inc396
-rw-r--r--browser/base/content/browser-ctrlTab.js684
-rw-r--r--browser/base/content/browser-customization.js181
-rw-r--r--browser/base/content/browser-data-submission-info-bar.js131
-rw-r--r--browser/base/content/browser-development-helpers.js51
-rw-r--r--browser/base/content/browser-doctype.inc14
-rw-r--r--browser/base/content/browser-fullScreenAndPointerLock.js861
-rw-r--r--browser/base/content/browser-fullZoom.js691
-rw-r--r--browser/base/content/browser-fxaSignout.js26
-rw-r--r--browser/base/content/browser-fxaSignout.xhtml32
-rw-r--r--browser/base/content/browser-gestureSupport.js846
-rw-r--r--browser/base/content/browser-graphics-utils.js44
-rw-r--r--browser/base/content/browser-menubar.inc528
-rw-r--r--browser/base/content/browser-pageActions.js1390
-rw-r--r--browser/base/content/browser-places.js2534
-rw-r--r--browser/base/content/browser-safebrowsing.js78
-rw-r--r--browser/base/content/browser-sets.inc388
-rw-r--r--browser/base/content/browser-sidebar.js608
-rw-r--r--browser/base/content/browser-siteIdentity.js2114
-rw-r--r--browser/base/content/browser-siteProtections.js2538
-rw-r--r--browser/base/content/browser-sync.js1609
-rw-r--r--browser/base/content/browser-tabsintitlebar.js125
-rw-r--r--browser/base/content/browser-thumbnails.js224
-rw-r--r--browser/base/content/browser-toolbarKeyNav.js423
-rw-r--r--browser/base/content/browser-webrtc.js140
-rw-r--r--browser/base/content/browser.css1578
-rw-r--r--browser/base/content/browser.js9405
-rw-r--r--browser/base/content/browser.xhtml2341
-rw-r--r--browser/base/content/contentTheme.js171
-rw-r--r--browser/base/content/defaultthemes/1.header.jpgbin0 -> 266398 bytes
-rw-r--r--browser/base/content/defaultthemes/1.icon.jpgbin0 -> 1093 bytes
-rw-r--r--browser/base/content/defaultthemes/1.preview.jpgbin0 -> 7953 bytes
-rw-r--r--browser/base/content/defaultthemes/2.header.jpgbin0 -> 173983 bytes
-rw-r--r--browser/base/content/defaultthemes/2.icon.jpgbin0 -> 509 bytes
-rw-r--r--browser/base/content/defaultthemes/2.preview.jpgbin0 -> 2877 bytes
-rw-r--r--browser/base/content/defaultthemes/3.header.pngbin0 -> 219688 bytes
-rw-r--r--browser/base/content/defaultthemes/3.icon.pngbin0 -> 832 bytes
-rw-r--r--browser/base/content/defaultthemes/3.preview.pngbin0 -> 47278 bytes
-rw-r--r--browser/base/content/defaultthemes/4.header.pngbin0 -> 663275 bytes
-rw-r--r--browser/base/content/defaultthemes/4.icon.pngbin0 -> 665 bytes
-rw-r--r--browser/base/content/defaultthemes/4.preview.pngbin0 -> 77913 bytes
-rw-r--r--browser/base/content/defaultthemes/5.header.pngbin0 -> 1469 bytes
-rw-r--r--browser/base/content/defaultthemes/5.icon.jpgbin0 -> 267 bytes
-rw-r--r--browser/base/content/defaultthemes/5.preview.jpgbin0 -> 2837 bytes
-rw-r--r--browser/base/content/docs/tabbrowser/async-tab-switcher.rst239
-rw-r--r--browser/base/content/docs/tabbrowser/index.rst14
-rw-r--r--browser/base/content/global-scripts.inc26
-rw-r--r--browser/base/content/hiddenWindowMac.xhtml34
-rw-r--r--browser/base/content/history-swipe-arrow.svg7
-rw-r--r--browser/base/content/logos/etp-mobile.svg13
-rw-r--r--browser/base/content/logos/lockwise.svg4
-rw-r--r--browser/base/content/logos/monitor.svg4
-rw-r--r--browser/base/content/logos/proxy-dark.svg4
-rw-r--r--browser/base/content/logos/proxy-light.svg4
-rw-r--r--browser/base/content/logos/send.svg4
-rw-r--r--browser/base/content/logos/tracking-protection-dark-theme.svg4
-rw-r--r--browser/base/content/logos/tracking-protection.svg4
-rw-r--r--browser/base/content/logos/vpn-dark.svg6
-rw-r--r--browser/base/content/logos/vpn-light.svg6
-rw-r--r--browser/base/content/macWindow.inc.xhtml35
-rw-r--r--browser/base/content/moz.build176
-rw-r--r--browser/base/content/newInstall.js54
-rw-r--r--browser/base/content/newInstall.xhtml28
-rw-r--r--browser/base/content/newInstallPage.html55
-rw-r--r--browser/base/content/newInstallPage.js74
-rw-r--r--browser/base/content/nonbrowser-mac.js151
-rw-r--r--browser/base/content/nsContextMenu.js2093
-rw-r--r--browser/base/content/overrides/app-license.html6
-rw-r--r--browser/base/content/pageinfo/pageInfo.css71
-rw-r--r--browser/base/content/pageinfo/pageInfo.js1135
-rw-r--r--browser/base/content/pageinfo/pageInfo.xhtml419
-rw-r--r--browser/base/content/pageinfo/permissions.js239
-rw-r--r--browser/base/content/pageinfo/security.js434
-rw-r--r--browser/base/content/popup-notifications.inc131
-rw-r--r--browser/base/content/robot.icobin0 -> 1791 bytes
-rw-r--r--browser/base/content/safeMode.css7
-rw-r--r--browser/base/content/safeMode.js90
-rw-r--r--browser/base/content/safeMode.xhtml46
-rw-r--r--browser/base/content/sanitize.xhtml104
-rw-r--r--browser/base/content/sanitizeDialog.css36
-rw-r--r--browser/base/content/sanitizeDialog.js232
-rw-r--r--browser/base/content/static-robot.pngbin0 -> 224 bytes
-rw-r--r--browser/base/content/tab-content.js58
-rw-r--r--browser/base/content/tabbrowser-tab.js676
-rw-r--r--browser/base/content/tabbrowser-tabs.js2040
-rw-r--r--browser/base/content/tabbrowser.css83
-rw-r--r--browser/base/content/tabbrowser.js6836
-rw-r--r--browser/base/content/test/about/.eslintrc.js5
-rw-r--r--browser/base/content/test/about/POSTSearchEngine.xml6
-rw-r--r--browser/base/content/test/about/browser.ini49
-rw-r--r--browser/base/content/test/about/browser_aboutCertError.js604
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_clockSkew.js155
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_exception.js222
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_mitm.js158
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_multiple_errors.js153
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_noSubjectAltName.js67
-rw-r--r--browser/base/content/test/about/browser_aboutCertError_telemetry.js160
-rw-r--r--browser/base/content/test/about/browser_aboutDialog_distribution.js70
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_POST.js88
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_composing.js128
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_searchbar.js45
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_suggestion.js76
-rw-r--r--browser/base/content/test/about/browser_aboutHome_search_telemetry.js92
-rw-r--r--browser/base/content/test/about/browser_aboutNetError.js302
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_csp_iframe.js143
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js129
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbar.js349
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js176
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js57
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarPrefs.js88
-rw-r--r--browser/base/content/test/about/browser_aboutNewTab_defaultBrowserNotification.js344
-rw-r--r--browser/base/content/test/about/browser_aboutStopReload.js169
-rw-r--r--browser/base/content/test/about/browser_aboutSupport.js63
-rw-r--r--browser/base/content/test/about/browser_aboutSupport_newtab_security_state.js19
-rw-r--r--browser/base/content/test/about/browser_bug435325.js57
-rw-r--r--browser/base/content/test/about/browser_bug633691.js31
-rw-r--r--browser/base/content/test/about/csp_iframe.sjs29
-rw-r--r--browser/base/content/test/about/dummy_page.html9
-rw-r--r--browser/base/content/test/about/head.js278
-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.sjs22
-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.sjs33
-rw-r--r--browser/base/content/test/alerts/.eslintrc.js5
-rw-r--r--browser/base/content/test/alerts/browser.ini16
-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.js159
-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.js85
-rw-r--r--browser/base/content/test/alerts/browser_notification_replace.js65
-rw-r--r--browser/base/content/test/alerts/browser_notification_tab_switching.js112
-rw-r--r--browser/base/content/test/alerts/file_dom_notifications.html39
-rw-r--r--browser/base/content/test/alerts/head.js72
-rw-r--r--browser/base/content/test/backforward/.eslintrc.js5
-rw-r--r--browser/base/content/test/backforward/browser.ini1
-rw-r--r--browser/base/content/test/backforward/browser_longpress_session_history_menu.js73
-rw-r--r--browser/base/content/test/caps/.eslintrc.js5
-rw-r--r--browser/base/content/test/caps/browser.ini6
-rw-r--r--browser/base/content/test/caps/browser_principalSerialization_csp.js106
-rw-r--r--browser/base/content/test/caps/browser_principalSerialization_json.js164
-rw-r--r--browser/base/content/test/caps/browser_principalSerialization_version1.js159
-rw-r--r--browser/base/content/test/captivePortal/.eslintrc.js5
-rw-r--r--browser/base/content/test/captivePortal/browser.ini11
-rw-r--r--browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js125
-rw-r--r--browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js106
-rw-r--r--browser/base/content/test/captivePortal/browser_captivePortalTabReference.js125
-rw-r--r--browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js175
-rw-r--r--browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js207
-rw-r--r--browser/base/content/test/captivePortal/head.js214
-rw-r--r--browser/base/content/test/chrome/chrome.ini4
-rw-r--r--browser/base/content/test/chrome/test_aboutCrashed.xhtml78
-rw-r--r--browser/base/content/test/chrome/test_aboutRestartRequired.xhtml76
-rw-r--r--browser/base/content/test/contextMenu/.eslintrc.js5
-rw-r--r--browser/base/content/test/contextMenu/browser.ini38
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu.js2058
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_childprocess.js129
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_iframe.js73
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_input.js332
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_linkopen.js111
-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.js80
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_spellcheck.js251
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_touch.js91
-rw-r--r--browser/base/content/test/contextMenu/browser_utilityOverlay.js78
-rw-r--r--browser/base/content/test/contextMenu/browser_utilityOverlayPrincipal.js68
-rw-r--r--browser/base/content/test/contextMenu/browser_view_image.js148
-rw-r--r--browser/base/content/test/contextMenu/contextmenu_common.js474
-rw-r--r--browser/base/content/test/contextMenu/ctxmenu-image.pngbin0 -> 5401 bytes
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu.html90
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu_input.html29
-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_revoked_cached_blob.html40
-rw-r--r--browser/base/content/test/favicons/.eslintrc.js5
-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.ini100
-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.js48
-rw-r--r--browser/base/content/test/favicons/browser_favicon_change.js33
-rw-r--r--browser/base/content/test/favicons/browser_favicon_change_not_in_document.js55
-rw-r--r--browser/base/content/test/favicons/browser_favicon_credentials.js71
-rw-r--r--browser/base/content/test/favicons/browser_favicon_crossorigin.js61
-rw-r--r--browser/base/content/test/favicons/browser_favicon_load.js175
-rw-r--r--browser/base/content/test/favicons/browser_favicon_nostore.js152
-rw-r--r--browser/base/content/test/favicons/browser_favicon_referer.js62
-rw-r--r--browser/base/content/test/favicons/browser_icon_discovery.js136
-rw-r--r--browser/base/content/test/favicons/browser_invalid_href_fallback.js24
-rw-r--r--browser/base/content/test/favicons/browser_missing_favicon.js33
-rw-r--r--browser/base/content/test/favicons/browser_mixed_content.js26
-rw-r--r--browser/base/content/test/favicons/browser_multiple_icons_in_short_timeframe.js37
-rw-r--r--browser/base/content/test/favicons/browser_oversized.js25
-rw-r--r--browser/base/content/test/favicons/browser_preferred_icons.js140
-rw-r--r--browser/base/content/test/favicons/browser_redirect.js20
-rw-r--r--browser/base/content/test/favicons/browser_rich_icons.js50
-rw-r--r--browser/base/content/test/favicons/browser_rooticon.js22
-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.js183
-rw-r--r--browser/base/content/test/favicons/cookie_favicon.html11
-rw-r--r--browser/base/content/test/favicons/cookie_favicon.sjs22
-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/discovery.html8
-rw-r--r--browser/base/content/test/favicons/file_bug970276_favicon1.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/favicons/file_bug970276_favicon2.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/favicons/file_bug970276_popup1.html14
-rw-r--r--browser/base/content/test/favicons/file_bug970276_popup2.html12
-rw-r--r--browser/base/content/test/favicons/file_favicon.html11
-rw-r--r--browser/base/content/test/favicons/file_favicon.pngbin0 -> 344 bytes
-rw-r--r--browser/base/content/test/favicons/file_favicon.png^headers^1
-rw-r--r--browser/base/content/test/favicons/file_favicon_change.html13
-rw-r--r--browser/base/content/test/favicons/file_favicon_change_not_in_document.html20
-rw-r--r--browser/base/content/test/favicons/file_favicon_no_referrer.html11
-rw-r--r--browser/base/content/test/favicons/file_favicon_redirect.html12
-rw-r--r--browser/base/content/test/favicons/file_favicon_redirect.ico0
-rw-r--r--browser/base/content/test/favicons/file_favicon_redirect.ico^headers^2
-rw-r--r--browser/base/content/test/favicons/file_favicon_thirdParty.html11
-rw-r--r--browser/base/content/test/favicons/file_generic_favicon.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/favicons/file_insecure_favicon.html11
-rw-r--r--browser/base/content/test/favicons/file_invalid_href.html12
-rw-r--r--browser/base/content/test/favicons/file_mask_icon.html11
-rw-r--r--browser/base/content/test/favicons/file_rich_icon.html12
-rw-r--r--browser/base/content/test/favicons/file_with_favicon.html12
-rw-r--r--browser/base/content/test/favicons/file_with_slow_favicon.html10
-rw-r--r--browser/base/content/test/favicons/head.js100
-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/.eslintrc.js5
-rw-r--r--browser/base/content/test/forms/browser.ini12
-rw-r--r--browser/base/content/test/forms/browser_selectpopup.js1337
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_colors.js683
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_searchfocus.js52
-rw-r--r--browser/base/content/test/forms/head.js20
-rw-r--r--browser/base/content/test/fullscreen/.eslintrc.js5
-rw-r--r--browser/base/content/test/fullscreen/FullscreenFrame.jsm105
-rw-r--r--browser/base/content/test/fullscreen/browser.ini23
-rw-r--r--browser/base/content/test/fullscreen/browser_bug1557041.js49
-rw-r--r--browser/base/content/test/fullscreen/browser_bug1620341.js92
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js246
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_cross_origin.js64
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_enterInUrlbar.js50
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_permissions_prompt.js160
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js52
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_window_open.js48
-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.js125
-rw-r--r--browser/base/content/test/fullscreen/open_and_focus_helper.html44
-rw-r--r--browser/base/content/test/general/.eslintrc.js5
-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.ini377
-rw-r--r--browser/base/content/test/general/browser_accesskeys.js204
-rw-r--r--browser/base/content/test/general/browser_addCertException.js77
-rw-r--r--browser/base/content/test/general/browser_addKeywordSearch.js89
-rw-r--r--browser/base/content/test/general/browser_alltabslistener.js326
-rw-r--r--browser/base/content/test/general/browser_backButtonFitts.js39
-rw-r--r--browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js88
-rw-r--r--browser/base/content/test/general/browser_bug1261299.js112
-rw-r--r--browser/base/content/test/general/browser_bug1297539.js122
-rw-r--r--browser/base/content/test/general/browser_bug1299667.js67
-rw-r--r--browser/base/content/test/general/browser_bug321000.js91
-rw-r--r--browser/base/content/test/general/browser_bug356571.js102
-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_bug423833.js168
-rw-r--r--browser/base/content/test/general/browser_bug424101.js72
-rw-r--r--browser/base/content/test/general/browser_bug427559.js41
-rw-r--r--browser/base/content/test/general/browser_bug431826.js56
-rw-r--r--browser/base/content/test/general/browser_bug432599.js104
-rw-r--r--browser/base/content/test/general/browser_bug455852.js27
-rw-r--r--browser/base/content/test/general/browser_bug462289.js118
-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.js48
-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.js166
-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.js59
-rw-r--r--browser/base/content/test/general/browser_bug575561.js117
-rw-r--r--browser/base/content/test/general/browser_bug577121.js27
-rw-r--r--browser/base/content/test/general/browser_bug578534.js30
-rw-r--r--browser/base/content/test/general/browser_bug579872.js23
-rw-r--r--browser/base/content/test/general/browser_bug581253.js75
-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.js23
-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.js44
-rw-r--r--browser/base/content/test/general/browser_bug624734.js47
-rw-r--r--browser/base/content/test/general/browser_bug647886.js51
-rw-r--r--browser/base/content/test/general/browser_bug664672.js27
-rw-r--r--browser/base/content/test/general/browser_bug676619.js122
-rw-r--r--browser/base/content/test/general/browser_bug710878.js49
-rw-r--r--browser/base/content/test/general/browser_bug724239.js53
-rw-r--r--browser/base/content/test/general/browser_bug734076.js186
-rw-r--r--browser/base/content/test/general/browser_bug749738.js31
-rw-r--r--browser/base/content/test/general/browser_bug763468_perwindowpb.js57
-rw-r--r--browser/base/content/test/general/browser_bug767836_perwindowpb.js76
-rw-r--r--browser/base/content/test/general/browser_bug817947.js59
-rw-r--r--browser/base/content/test/general/browser_bug832435.js26
-rw-r--r--browser/base/content/test/general/browser_bug882977.js33
-rw-r--r--browser/base/content/test/general/browser_bug963945.js26
-rw-r--r--browser/base/content/test/general/browser_clipboard.js290
-rw-r--r--browser/base/content/test/general/browser_clipboard_pastefile.js80
-rw-r--r--browser/base/content/test/general/browser_contentAltClick.js206
-rw-r--r--browser/base/content/test/general/browser_contentAreaClick.js327
-rw-r--r--browser/base/content/test/general/browser_ctrlTab.js282
-rw-r--r--browser/base/content/test/general/browser_datachoices_notification.js292
-rw-r--r--browser/base/content/test/general/browser_decoderDoctor.js297
-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.js93
-rw-r--r--browser/base/content/test/general/browser_drag.js64
-rw-r--r--browser/base/content/test/general/browser_duplicateIDs.js10
-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.js927
-rw-r--r--browser/base/content/test/general/browser_hide_removing.js27
-rw-r--r--browser/base/content/test/general/browser_homeDrop.js115
-rw-r--r--browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js48
-rw-r--r--browser/base/content/test/general/browser_lastAccessedTab.js62
-rw-r--r--browser/base/content/test/general/browser_menuButtonFitts.js60
-rw-r--r--browser/base/content/test/general/browser_middleMouse_noJSPaste.js49
-rw-r--r--browser/base/content/test/general/browser_minimize.js34
-rw-r--r--browser/base/content/test/general/browser_modifiedclick_inherit_principal.js40
-rw-r--r--browser/base/content/test/general/browser_newTabDrop.js221
-rw-r--r--browser/base/content/test/general/browser_newWindowDrop.js232
-rw-r--r--browser/base/content/test/general/browser_new_http_window_opened_from_file_tab.js62
-rw-r--r--browser/base/content/test/general/browser_newwindow_focus.js93
-rw-r--r--browser/base/content/test/general/browser_page_style_menu.js176
-rw-r--r--browser/base/content/test/general/browser_page_style_menu_update.js47
-rw-r--r--browser/base/content/test/general/browser_plainTextLinks.js232
-rw-r--r--browser/base/content/test/general/browser_printpreview.js86
-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.js157
-rw-r--r--browser/base/content/test/general/browser_relatedTabs.js74
-rw-r--r--browser/base/content/test/general/browser_remoteTroubleshoot.js131
-rw-r--r--browser/base/content/test/general/browser_remoteWebNavigation_postdata.js54
-rw-r--r--browser/base/content/test/general/browser_removeTabsToTheEnd.js27
-rw-r--r--browser/base/content/test/general/browser_restore_isAppTab.js91
-rw-r--r--browser/base/content/test/general/browser_save_link-perwindowpb.js215
-rw-r--r--browser/base/content/test/general/browser_save_link_when_window_navigates.js181
-rw-r--r--browser/base/content/test/general/browser_save_private_link_perwindowpb.js129
-rw-r--r--browser/base/content/test/general/browser_save_video.js100
-rw-r--r--browser/base/content/test/general/browser_save_video_frame.js103
-rw-r--r--browser/base/content/test/general/browser_search_discovery.js133
-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.sjs13
-rw-r--r--browser/base/content/test/general/browser_storagePressure_notification.js160
-rw-r--r--browser/base/content/test/general/browser_tabDrop.js208
-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.js53
-rw-r--r--browser/base/content/test/general/browser_tab_drag_drop_perwindow.js422
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop.js258
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop2.js65
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop2_frame1.xhtml169
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop_embed.html2
-rw-r--r--browser/base/content/test/general/browser_tabfocus.js817
-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.js148
-rw-r--r--browser/base/content/test/general/browser_typeAheadFind.js31
-rw-r--r--browser/base/content/test/general/browser_unknownContentType_title.js38
-rw-r--r--browser/base/content/test/general/browser_unloaddialogs.js40
-rw-r--r--browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js59
-rw-r--r--browser/base/content/test/general/browser_visibleFindSelection.js62
-rw-r--r--browser/base/content/test/general/browser_visibleTabs.js105
-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.js113
-rw-r--r--browser/base/content/test/general/browser_zbug569342.js77
-rw-r--r--browser/base/content/test/general/bug592338.html23
-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.html35
-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/discovery.html8
-rw-r--r--browser/base/content/test/general/download_page.html56
-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.sjs22
-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/gZipOfflineChild_uncompressed.html21
-rw-r--r--browser/base/content/test/general/head.js407
-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/page_style_only_alternates.html5
-rw-r--r--browser/base/content/test/general/page_style_sample.html45
-rw-r--r--browser/base/content/test/general/print_postdata.sjs22
-rw-r--r--browser/base/content/test/general/refresh_header.sjs24
-rw-r--r--browser/base/content/test/general/refresh_meta.sjs36
-rw-r--r--browser/base/content/test/general/test_bug462673.html18
-rw-r--r--browser/base/content/test/general/test_bug628179.html9
-rw-r--r--browser/base/content/test/general/test_remoteTroubleshoot.html50
-rw-r--r--browser/base/content/test/general/title_test.svg59
-rw-r--r--browser/base/content/test/general/unknownContentType_file.pif1
-rw-r--r--browser/base/content/test/general/unknownContentType_file.pif^headers^1
-rw-r--r--browser/base/content/test/general/video.oggbin0 -> 285310 bytes
-rw-r--r--browser/base/content/test/general/web_video.html10
-rw-r--r--browser/base/content/test/general/web_video1.ogvbin0 -> 28942 bytes
-rw-r--r--browser/base/content/test/general/web_video1.ogv^headers^3
-rw-r--r--browser/base/content/test/historySwipeAnimation/.eslintrc.js5
-rw-r--r--browser/base/content/test/historySwipeAnimation/browser.ini1
-rw-r--r--browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js49
-rw-r--r--browser/base/content/test/keyboard/.eslintrc.js5
-rw-r--r--browser/base/content/test/keyboard/browser.ini10
-rw-r--r--browser/base/content/test/keyboard/browser_bookmarks_shortcut.js144
-rw-r--r--browser/base/content/test/keyboard/browser_popup_keyNav.js49
-rw-r--r--browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js334
-rw-r--r--browser/base/content/test/keyboard/browser_toolbarKeyNav.js431
-rw-r--r--browser/base/content/test/keyboard/focusableContent.html1
-rw-r--r--browser/base/content/test/keyboard/head.js55
-rw-r--r--browser/base/content/test/menubar/.eslintrc.js5
-rw-r--r--browser/base/content/test/menubar/browser.ini5
-rw-r--r--browser/base/content/test/menubar/browser_file_menu_import_wizard.js19
-rw-r--r--browser/base/content/test/menubar/browser_window_menu_list.js45
-rw-r--r--browser/base/content/test/metaTags/.eslintrc.js5
-rw-r--r--browser/base/content/test/metaTags/bad_meta_tags.html14
-rw-r--r--browser/base/content/test/metaTags/browser.ini9
-rw-r--r--browser/base/content/test/metaTags/browser_bad_meta_tags.js36
-rw-r--r--browser/base/content/test/metaTags/browser_meta_tags.js57
-rw-r--r--browser/base/content/test/metaTags/head.js29
-rw-r--r--browser/base/content/test/metaTags/meta_tags.html29
-rw-r--r--browser/base/content/test/outOfProcess/.eslintrc.js5
-rw-r--r--browser/base/content/test/outOfProcess/browser.ini14
-rw-r--r--browser/base/content/test/outOfProcess/browser_basic_outofprocess.js148
-rw-r--r--browser/base/content/test/outOfProcess/browser_controller.js120
-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.js86
-rw-r--r--browser/base/content/test/pageActions/.eslintrc.js5
-rw-r--r--browser/base/content/test/pageActions/browser.ini22
-rw-r--r--browser/base/content/test/pageActions/browser_PageActions_removeExtension.js320
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu.js1241
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js672
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js40
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js172
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_share_win.html2
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_share_win.js53
-rw-r--r--browser/base/content/test/pageActions/head.js147
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml7
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml7
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml7
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html8
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html10
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html8
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html9
-rw-r--r--browser/base/content/test/pageStyle/.eslintrc.js5
-rw-r--r--browser/base/content/test/pageStyle/browser.ini4
-rw-r--r--browser/base/content/test/pageStyle/browser_disable_author_style_oop.js75
-rw-r--r--browser/base/content/test/pageStyle/page_style.html8
-rw-r--r--browser/base/content/test/pageinfo/.eslintrc.js5
-rw-r--r--browser/base/content/test/pageinfo/all_images.html15
-rw-r--r--browser/base/content/test/pageinfo/browser.ini24
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js89
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js30
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_image_info.js57
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_images.js31
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_permissions.js261
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_security.js364
-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/.eslintrc.js5
-rw-r--r--browser/base/content/test/performance/StartupContentSubframe.jsm68
-rw-r--r--browser/base/content/test/performance/browser.ini58
-rw-r--r--browser/base/content/test/performance/browser_appmenu.js146
-rw-r--r--browser/base/content/test/performance/browser_preferences_usage.js275
-rw-r--r--browser/base/content/test/performance/browser_startup.js244
-rw-r--r--browser/base/content/test/performance/browser_startup_content.js188
-rw-r--r--browser/base/content/test/performance/browser_startup_content_mainthreadio.js447
-rw-r--r--browser/base/content/test/performance/browser_startup_content_subframe.js149
-rw-r--r--browser/base/content/test/performance/browser_startup_flicker.js92
-rw-r--r--browser/base/content/test/performance/browser_startup_hiddenwindow.js50
-rw-r--r--browser/base/content/test/performance/browser_startup_images.js134
-rw-r--r--browser/base/content/test/performance/browser_startup_mainthreadio.js890
-rw-r--r--browser/base/content/test/performance/browser_startup_syncIPC.js418
-rw-r--r--browser/base/content/test/performance/browser_tabclose.js106
-rw-r--r--browser/base/content/test/performance/browser_tabclose_grow.js93
-rw-r--r--browser/base/content/test/performance/browser_tabdetach.js116
-rw-r--r--browser/base/content/test/performance/browser_tabopen.js143
-rw-r--r--browser/base/content/test/performance/browser_tabopen_squeeze.js99
-rw-r--r--browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js199
-rw-r--r--browser/base/content/test/performance/browser_tabswitch.js112
-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_window_resize.js131
-rw-r--r--browser/base/content/test/performance/browser_windowclose.js58
-rw-r--r--browser/base/content/test/performance/browser_windowopen.js183
-rw-r--r--browser/base/content/test/performance/file_empty.html1
-rw-r--r--browser/base/content/test/performance/head.js923
-rw-r--r--browser/base/content/test/performance/hidpi/browser.ini7
-rw-r--r--browser/base/content/test/performance/io/browser.ini26
-rw-r--r--browser/base/content/test/performance/lowdpi/browser.ini8
-rw-r--r--browser/base/content/test/perftest.ini1
-rw-r--r--browser/base/content/test/perftest_browser_xhtml_dom.js85
-rw-r--r--browser/base/content/test/permissions/.eslintrc.js5
-rw-r--r--browser/base/content/test/permissions/browser.ini33
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_blocked.html14
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_blocked.js338
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs32
-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_permission_delegate_geo.js266
-rw-r--r--browser/base/content/test/permissions/browser_permissions.js590
-rw-r--r--browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js46
-rw-r--r--browser/base/content/test/permissions/browser_permissions_handling_user_input.js93
-rw-r--r--browser/base/content/test/permissions/browser_permissions_postPrompt.js101
-rw-r--r--browser/base/content/test/permissions/browser_reservedkey.js228
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions.js119
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions_expiry.js110
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions_navigation.js247
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions_tabs.js105
-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.js9
-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/.eslintrc.js5
-rw-r--r--browser/base/content/test/plugins/BlocklistTestProxy.jsm88
-rw-r--r--browser/base/content/test/plugins/browser.ini25
-rw-r--r--browser/base/content/test/plugins/browser_CTP_favorfallback.js104
-rw-r--r--browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js122
-rw-r--r--browser/base/content/test/plugins/browser_CTP_zoom.js61
-rw-r--r--browser/base/content/test/plugins/browser_bug797677.js45
-rw-r--r--browser/base/content/test/plugins/browser_enable_DRM_prompt.js228
-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.js452
-rw-r--r--browser/base/content/test/plugins/plugin_bug797677.html5
-rw-r--r--browser/base/content/test/plugins/plugin_favorfallback.html96
-rw-r--r--browser/base/content/test/plugins/plugin_outsideScrollArea.html25
-rw-r--r--browser/base/content/test/plugins/plugin_simple_blank.swfbin0 -> 37 bytes
-rw-r--r--browser/base/content/test/plugins/plugin_test.html9
-rw-r--r--browser/base/content/test/plugins/plugin_zoom.html10
-rw-r--r--browser/base/content/test/popupNotifications/.eslintrc.js5
-rw-r--r--browser/base/content/test/popupNotifications/browser.ini30
-rw-r--r--browser/base/content/test/popupNotifications/browser_displayURI.js156
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification.js393
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_2.js304
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_3.js366
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_4.js289
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_5.js496
-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_keyboard.js274
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js64
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js285
-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.js376
-rw-r--r--browser/base/content/test/popups/.eslintrc.js5
-rw-r--r--browser/base/content/test/popups/browser.ini33
-rw-r--r--browser/base/content/test/popups/browser_popupUI.js192
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker.js119
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker_frames.js97
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker_identity_block.js241
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker_iframes.js180
-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.js125
-rw-r--r--browser/base/content/test/popups/head.js12
-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/protectionsUI/.eslintrc.js5
-rw-r--r--browser/base/content/test/protectionsUI/benignPage.html18
-rw-r--r--browser/base/content/test/protectionsUI/browser.ini41
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI.js740
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_3.js54
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_animation.js72
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_animation_2.js264
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_background_tabs.js72
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_categories.js291
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js511
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cryptominers.js298
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js38
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js297
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js95
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js153
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js163
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js400
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js123
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js315
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_state.js381
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_state_reset.js127
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js86
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js128
-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.sjs20
-rw-r--r--browser/base/content/test/protectionsUI/cookieSetterPage.html6
-rw-r--r--browser/base/content/test/protectionsUI/embeddedPage.html6
-rw-r--r--browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html16
-rw-r--r--browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js2
-rw-r--r--browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^1
-rw-r--r--browser/base/content/test/protectionsUI/head.js220
-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.js70
-rw-r--r--browser/base/content/test/protectionsUI/trackingPage.html13
-rw-r--r--browser/base/content/test/referrer/.eslintrc.js5
-rw-r--r--browser/base/content/test/referrer/browser.ini25
-rw-r--r--browser/base/content/test/referrer/browser_referrer_click_pinned_tab.js75
-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.sjs39
-rw-r--r--browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs39
-rw-r--r--browser/base/content/test/referrer/file_referrer_testserver.sjs31
-rw-r--r--browser/base/content/test/referrer/head.js319
-rw-r--r--browser/base/content/test/sanitize/.eslintrc.js5
-rw-r--r--browser/base/content/test/sanitize/browser.ini21
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission.js1
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js106
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_containers.js1
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js226
-rw-r--r--browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js72
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-formhistory.js44
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-history.js129
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-offlineData.js208
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js28
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js37
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-timespans.js1194
-rw-r--r--browser/base/content/test/sanitize/browser_sanitizeDialog.js997
-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.js331
-rw-r--r--browser/base/content/test/sidebar/.eslintrc.js5
-rw-r--r--browser/base/content/test/sidebar/browser.ini6
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_adopt.js67
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_keys.js24
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_move.js72
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_switcher.js64
-rw-r--r--browser/base/content/test/siteIdentity/.eslintrc.js5
-rw-r--r--browser/base/content/test/siteIdentity/browser.ini125
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug1045809.js105
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug822367.js249
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug902156.js169
-rw-r--r--browser/base/content/test/siteIdentity/browser_bug906190.js331
-rw-r--r--browser/base/content/test/siteIdentity/browser_check_identity_state.js706
-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.js77
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js52
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityBlock_focus.js117
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityIcon_img_url.js145
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_HttpsOnlyMode.js191
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js180
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_custom_roots.js81
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_focus.js120
-rw-r--r--browser/base/content/test/siteIdentity/browser_identity_UI.js166
-rw-r--r--browser/base/content/test/siteIdentity/browser_iframe_navigation.js107
-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.js359
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js36
-rw-r--r--browser/base/content/test/siteIdentity/browser_mixedContentFromOnunload.js66
-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.js177
-rw-r--r--browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js80
-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.js126
-rw-r--r--browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js199
-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.sjs17
-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/head.js412
-rw-r--r--browser/base/content/test/siteIdentity/iframe_navigation.html43
-rw-r--r--browser/base/content/test/siteIdentity/insecure_opener.html9
-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.sjs22
-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.html55
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_for_onions.html28
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css10
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html44
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css1
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html45
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css3
-rw-r--r--browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html44
-rw-r--r--browser/base/content/test/startup/.eslintrc.js5
-rw-r--r--browser/base/content/test/startup/browser.ini2
-rw-r--r--browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js137
-rw-r--r--browser/base/content/test/static/.eslintrc.js5
-rw-r--r--browser/base/content/test/static/browser.ini19
-rw-r--r--browser/base/content/test/static/browser_all_files_referenced.js1000
-rw-r--r--browser/base/content/test/static/browser_misused_characters_in_strings.js341
-rw-r--r--browser/base/content/test/static/browser_parsable_css.js489
-rw-r--r--browser/base/content/test/static/browser_parsable_script.js169
-rw-r--r--browser/base/content/test/static/browser_title_case_menus.js143
-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.js193
-rw-r--r--browser/base/content/test/statuspanel/.eslintrc.js5
-rw-r--r--browser/base/content/test/statuspanel/browser.ini7
-rw-r--r--browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js28
-rw-r--r--browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js28
-rw-r--r--browser/base/content/test/statuspanel/head.js53
-rw-r--r--browser/base/content/test/sync/.eslintrc.js5
-rw-r--r--browser/base/content/test/sync/browser.ini11
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendpage.js428
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendtab.js267
-rw-r--r--browser/base/content/test/sync/browser_fxa_badge.js71
-rw-r--r--browser/base/content/test/sync/browser_fxa_web_channel.html138
-rw-r--r--browser/base/content/test/sync/browser_fxa_web_channel.js248
-rw-r--r--browser/base/content/test/sync/browser_sync.js608
-rw-r--r--browser/base/content/test/sync/head.js24
-rw-r--r--browser/base/content/test/tabMediaIndicator/.eslintrc.js8
-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.ini31
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_destroy_iframe.js49
-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.js253
-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.js70
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_sound_indicator_silent_video.js88
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_webAudio_hideSoundPlayingIcon.js60
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_webAudio_silentData.js57
-rw-r--r--browser/base/content/test/tabMediaIndicator/browser_webaudio_audibility_change.js171
-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.js152
-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/.eslintrc.js5
-rw-r--r--browser/base/content/test/tabPrompts/browser.ini8
-rw-r--r--browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js61
-rw-r--r--browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js51
-rw-r--r--browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js140
-rw-r--r--browser/base/content/test/tabPrompts/browser_multiplePrompts.js96
-rw-r--r--browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js170
-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/tabcrashed/.eslintrc.js5
-rw-r--r--browser/base/content/test/tabcrashed/browser.ini19
-rw-r--r--browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js183
-rw-r--r--browser/base/content/test/tabcrashed/browser_clearEmail.js71
-rw-r--r--browser/base/content/test/tabcrashed/browser_launchFail.js57
-rw-r--r--browser/base/content/test/tabcrashed/browser_multipleCrashedTabs.js133
-rw-r--r--browser/base/content/test/tabcrashed/browser_noPermanentKey.js41
-rw-r--r--browser/base/content/test/tabcrashed/browser_printpreview_crash.js94
-rw-r--r--browser/base/content/test/tabcrashed/browser_showForm.js44
-rw-r--r--browser/base/content/test/tabcrashed/browser_shown.js202
-rw-r--r--browser/base/content/test/tabcrashed/browser_shownRestartRequired.js114
-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.js140
-rw-r--r--browser/base/content/test/tabdialogs/.eslintrc.js5
-rw-r--r--browser/base/content/test/tabdialogs/browser.ini10
-rw-r--r--browser/base/content/test/tabdialogs/browser_subdialog_esc.js118
-rw-r--r--browser/base/content/test/tabdialogs/browser_tabdialogbox_content_prompts.js76
-rw-r--r--browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js162
-rw-r--r--browser/base/content/test/tabdialogs/browser_tabdialogbox_tab_switch_focus.js131
-rw-r--r--browser/base/content/test/tabdialogs/loadDelayedReply.sjs22
-rw-r--r--browser/base/content/test/tabdialogs/subdialog.xhtml33
-rw-r--r--browser/base/content/test/tabs/.eslintrc.js5
-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.ini122
-rw-r--r--browser/base/content/test/tabs/browser_accessibility_indicator.js148
-rw-r--r--browser/base/content/test/tabs/browser_addTab_index.js8
-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.js670
-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.js40
-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.js57
-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.js196
-rw-r--r--browser/base/content/test/tabs/browser_e10s_about_process.js181
-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.js480
-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_multiselect_tabs_active_tab_selected_by_default.js52
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js80
-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.js122
-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_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.js48
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js75
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js112
-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.js74
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js126
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js355
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js144
-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_positional_attrs.js50
-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.js137
-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.js142
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js72
-rw-r--r--browser/base/content/test/tabs/browser_navigatePinnedTab.js67
-rw-r--r--browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js16
-rw-r--r--browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js187
-rw-r--r--browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js36
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js222
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_insert_position.js295
-rw-r--r--browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js45
-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.js54
-rw-r--r--browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js125
-rw-r--r--browser/base/content/test/tabs/browser_origin_attrs_rel.js325
-rw-r--r--browser/base/content/test/tabs/browser_overflowScroll.js121
-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.js93
-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.js140
-rw-r--r--browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js89
-rw-r--r--browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js211
-rw-r--r--browser/base/content/test/tabs/browser_progress_keyword_search_handling.js87
-rw-r--r--browser/base/content/test/tabs/browser_reload_deleted_file.js38
-rw-r--r--browser/base/content/test/tabs/browser_tabCloseProbes.js112
-rw-r--r--browser/base/content/test/tabs/browser_tabCloseSpacer.js99
-rw-r--r--browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js57
-rw-r--r--browser/base/content/test/tabs/browser_tabReorder.js64
-rw-r--r--browser/base/content/test/tabs/browser_tabReorder_overflow.js60
-rw-r--r--browser/base/content/test/tabs/browser_tabSpinnerProbe.js100
-rw-r--r--browser/base/content/test/tabs/browser_tabSuccessors.js131
-rw-r--r--browser/base/content/test/tabs/browser_tabSwitchPrintPreview.js58
-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_manager_visibility.js53
-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.js102
-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.js91
-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_anchor_elements.html12
-rw-r--r--browser/base/content/test/tabs/file_mediaPlayback.html2
-rw-r--r--browser/base/content/test/tabs/file_new_tab_page.html9
-rw-r--r--browser/base/content/test/tabs/file_rel_opener_noopener.html12
-rw-r--r--browser/base/content/test/tabs/head.js514
-rw-r--r--browser/base/content/test/tabs/helper_origin_attrs_testing.js143
-rw-r--r--browser/base/content/test/tabs/open_window_in_new_tab.html15
-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/touch/.eslintrc.js5
-rw-r--r--browser/base/content/test/touch/browser.ini4
-rw-r--r--browser/base/content/test/touch/browser_menu_touch.js192
-rw-r--r--browser/base/content/test/webextensions/.eslintrc.js9
-rw-r--r--browser/base/content/test/webextensions/browser.ini33
-rw-r--r--browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js26
-rw-r--r--browser/base/content/test/webextensions/browser_extension_sideloading.js405
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background.js293
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js124
-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.js61
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_installTrigger.js18
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_local_file.js87
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js18
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_optional.js52
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_pointerevent.js59
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_unsigned.js53
-rw-r--r--browser/base/content/test/webextensions/browser_update_checkForUpdates.js17
-rw-r--r--browser/base/content/test/webextensions/browser_update_interactive_noprompt.js77
-rw-r--r--browser/base/content/test/webextensions/browser_webext_nopermissions.xpibin0 -> 4273 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_permissions.xpibin0 -> 16602 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_unsigned.xpibin0 -> 12620 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update.json70
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update1.xpibin0 -> 4271 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update2.xpibin0 -> 4291 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_icon1.xpibin0 -> 16545 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_icon2.xpibin0 -> 16564 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_origins1.xpibin0 -> 268 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_origins2.xpibin0 -> 275 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_perms1.xpibin0 -> 4273 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_perms2.xpibin0 -> 4282 bytes
-rw-r--r--browser/base/content/test/webextensions/file_install_extensions.html19
-rw-r--r--browser/base/content/test/webextensions/head.js689
-rw-r--r--browser/base/content/test/webrtc/.eslintrc.js5
-rw-r--r--browser/base/content/test/webrtc/browser.ini47
-rw-r--r--browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js229
-rw-r--r--browser/base/content/test/webrtc/browser_device_controls_menus.js54
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media.js837
-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_default_permissions.js206
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js653
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js793
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js252
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js518
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js1008
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js281
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js922
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js73
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js100
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js413
-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_global_mute_toggles.js293
-rw-r--r--browser/base/content/test/webrtc/browser_indicator_popuphiding.js50
-rw-r--r--browser/base/content/test/webrtc/browser_notification_silencing.js231
-rw-r--r--browser/base/content/test/webrtc/browser_stop_sharing_button.js172
-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.js373
-rw-r--r--browser/base/content/test/webrtc/get_user_media.html91
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_frame.html90
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html63
-rw-r--r--browser/base/content/test/webrtc/get_user_media_in_xorigin_frame_ancestor.html12
-rw-r--r--browser/base/content/test/webrtc/head.js1131
-rw-r--r--browser/base/content/test/webrtc/legacyIndicator/browser.ini35
-rw-r--r--browser/base/content/test/webrtc/single_peerconnection.html14
-rw-r--r--browser/base/content/test/zoom/.eslintrc.js5
-rw-r--r--browser/base/content/test/zoom/browser.ini24
-rw-r--r--browser/base/content/test/zoom/browser_background_link_zoom_reset.js44
-rw-r--r--browser/base/content/test/zoom/browser_background_zoom.js113
-rw-r--r--browser/base/content/test/zoom/browser_default_zoom.js148
-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.js186
-rw-r--r--browser/base/content/test/zoom/browser_default_zoom_multitab_002.js91
-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.js38
-rw-r--r--browser/base/content/test/zoom/browser_mousewheel_zoom.js68
-rw-r--r--browser/base/content/test/zoom/browser_sitespecific_background_pref.js34
-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.js126
-rw-r--r--browser/base/content/test/zoom/browser_subframe_textzoom.js51
-rw-r--r--browser/base/content/test/zoom/browser_tabswitch_zoom_flicker.js44
-rw-r--r--browser/base/content/test/zoom/head.js226
-rw-r--r--browser/base/content/test/zoom/zoom_test.html14
-rw-r--r--browser/base/content/titlebar-items.inc.xhtml24
-rw-r--r--browser/base/content/utilityOverlay.js1162
-rw-r--r--browser/base/content/webext-panels.js184
-rw-r--r--browser/base/content/webext-panels.xhtml86
-rw-r--r--browser/base/content/webrtcIndicator.js682
-rw-r--r--browser/base/content/webrtcIndicator.xhtml58
-rw-r--r--browser/base/content/webrtcLegacyIndicator.js213
-rw-r--r--browser/base/content/webrtcLegacyIndicator.xhtml35
1091 files changed, 153452 insertions, 0 deletions
diff --git a/browser/base/content/.eslintrc.js b/browser/base/content/.eslintrc.js
new file mode 100644
index 0000000000..f474a5a3d9
--- /dev/null
+++ b/browser/base/content/.eslintrc.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ overrides: [
+ {
+ files: "aboutNetError.js",
+ parserOptions: {
+ sourceType: "module",
+ },
+ },
+ ],
+};
diff --git a/browser/base/content/aboutDialog-appUpdater.js b/browser/base/content/aboutDialog-appUpdater.js
new file mode 100644
index 0000000000..97d59902d5
--- /dev/null
+++ b/browser/base/content/aboutDialog-appUpdater.js
@@ -0,0 +1,244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Note: this file is included in aboutDialog.xhtml and preferences/advanced.xhtml
+// if MOZ_UPDATER is defined.
+
+/* import-globals-from aboutDialog.js */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppUpdater: "resource:///modules/AppUpdater.jsm",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
+});
+
+var UPDATING_MIN_DISPLAY_TIME_MS = 1500;
+
+var gAppUpdater;
+
+function onUnload(aEvent) {
+ if (gAppUpdater) {
+ gAppUpdater.destroy();
+ gAppUpdater = null;
+ }
+}
+
+function appUpdater(options = {}) {
+ this._appUpdater = new AppUpdater();
+
+ this._appUpdateListener = (status, ...args) => {
+ this._onAppUpdateStatus(status, ...args);
+ };
+ this._appUpdater.addListener(this._appUpdateListener);
+
+ this.options = options;
+ this.updatingMinDisplayTimerId = null;
+ this.updateDeck = document.getElementById("updateDeck");
+
+ this.bundle = Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ );
+
+ let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
+ let manualLink = document.getElementById("manualLink");
+ manualLink.textContent = manualURL;
+ manualLink.href = manualURL;
+ document.getElementById("failedLink").href = manualURL;
+
+ this._appUpdater.check();
+}
+
+appUpdater.prototype = {
+ destroy() {
+ this.stopCurrentCheck();
+ if (this.updatingMinDisplayTimerId) {
+ clearTimeout(this.updatingMinDisplayTimerId);
+ }
+ },
+
+ stopCurrentCheck() {
+ this._appUpdater.removeListener(this._appUpdateListener);
+ this._appUpdater.stop();
+ },
+
+ get update() {
+ return this._appUpdater.update;
+ },
+
+ _onAppUpdateStatus(status, ...args) {
+ switch (status) {
+ case AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY:
+ this.selectPanel("policyDisabled");
+ break;
+ case AppUpdater.STATUS.READY_FOR_RESTART:
+ this.selectPanel("apply");
+ break;
+ case AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES:
+ this.selectPanel("otherInstanceHandlingUpdates");
+ break;
+ case AppUpdater.STATUS.DOWNLOADING:
+ this.downloadStatus = document.getElementById("downloadStatus");
+ if (!args.length) {
+ this.downloadStatus.textContent = DownloadUtils.getTransferTotal(
+ 0,
+ this.update.selectedPatch.size
+ );
+ this.selectPanel("downloading");
+ } else {
+ let [progress, max] = args;
+ this.downloadStatus.textContent = DownloadUtils.getTransferTotal(
+ progress,
+ max
+ );
+ }
+ break;
+ case AppUpdater.STATUS.STAGING:
+ this.selectPanel("applying");
+ break;
+ case AppUpdater.STATUS.CHECKING: {
+ this.checkingForUpdatesDelayPromise = new Promise(resolve => {
+ this.updatingMinDisplayTimerId = setTimeout(
+ resolve,
+ UPDATING_MIN_DISPLAY_TIME_MS
+ );
+ });
+ if (Services.policies.isAllowed("appUpdate")) {
+ this.selectPanel("checkingForUpdates");
+ } else {
+ this.selectPanel("policyDisabled");
+ }
+ break;
+ }
+ case AppUpdater.STATUS.NO_UPDATES_FOUND:
+ this.checkingForUpdatesDelayPromise.then(() => {
+ if (Services.policies.isAllowed("appUpdate")) {
+ this.selectPanel("noUpdatesFound");
+ } else {
+ this.selectPanel("policyDisabled");
+ }
+ });
+ break;
+ case AppUpdater.STATUS.UNSUPPORTED_SYSTEM:
+ if (this.update.detailsURL) {
+ let unsupportedLink = document.getElementById("unsupportedLink");
+ unsupportedLink.href = this.update.detailsURL;
+ }
+ this.selectPanel("unsupportedSystem");
+ break;
+ case AppUpdater.STATUS.MANUAL_UPDATE:
+ this.selectPanel("manualUpdate");
+ break;
+ case AppUpdater.STATUS.DOWNLOAD_AND_INSTALL:
+ this.selectPanel("downloadAndInstall");
+ break;
+ case AppUpdater.STATUS.DOWNLOAD_FAILED:
+ this.selectPanel("downloadFailed");
+ break;
+ }
+ },
+
+ /**
+ * Sets the panel of the updateDeck and the visibility of icons
+ * in the #icons element.
+ *
+ * @param aChildID
+ * The id of the deck's child to select, e.g. "apply".
+ */
+ selectPanel(aChildID) {
+ let panel = document.getElementById(aChildID);
+ let icons = document.getElementById("icons");
+ if (icons) {
+ icons.className = aChildID;
+ }
+
+ let button = panel.querySelector("button");
+ if (button) {
+ if (aChildID == "downloadAndInstall") {
+ let updateVersion = gAppUpdater.update.displayVersion;
+ // Include the build ID if this is an "a#" (nightly or aurora) build
+ if (/a\d+$/.test(updateVersion)) {
+ let buildID = gAppUpdater.update.buildID;
+ let year = buildID.slice(0, 4);
+ let month = buildID.slice(4, 6);
+ let day = buildID.slice(6, 8);
+ updateVersion += ` (${year}-${month}-${day})`;
+ }
+ button.label = this.bundle.formatStringFromName(
+ "update.downloadAndInstallButton.label",
+ [updateVersion]
+ );
+ button.accessKey = this.bundle.GetStringFromName(
+ "update.downloadAndInstallButton.accesskey"
+ );
+ }
+ this.updateDeck.selectedPanel = panel;
+ if (
+ this.options.buttonAutoFocus &&
+ (!document.commandDispatcher.focusedElement || // don't steal the focus
+ document.commandDispatcher.focusedElement.localName == "button")
+ ) {
+ // except from the other buttons
+ button.focus();
+ }
+ } else {
+ this.updateDeck.selectedPanel = panel;
+ }
+ },
+
+ /**
+ * Check for updates
+ */
+ checkForUpdates() {
+ this._appUpdater.checkForUpdates();
+ },
+
+ /**
+ * Handles oncommand for the "Restart to Update" button
+ * which is presented after the download has been downloaded.
+ */
+ buttonRestartAfterDownload() {
+ if (!this._appUpdater.isReadyForRestart) {
+ return;
+ }
+
+ gAppUpdater.selectPanel("restarting");
+
+ // Notify all windows that an application quit has been requested.
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+
+ // Something aborted the quit process.
+ if (cancelQuit.data) {
+ gAppUpdater.selectPanel("apply");
+ return;
+ }
+
+ // If already in safe mode restart in safe mode (bug 327119)
+ if (Services.appinfo.inSafeMode) {
+ Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
+ return;
+ }
+
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ },
+
+ /**
+ * Starts the download of an update mar.
+ */
+ startDownload() {
+ this._appUpdater.startDownload();
+ },
+};
diff --git a/browser/base/content/aboutDialog.css b/browser/base/content/aboutDialog.css
new file mode 100644
index 0000000000..fcb8c2a1fa
--- /dev/null
+++ b/browser/base/content/aboutDialog.css
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+@namespace html "http://www.w3.org/1999/xhtml";
+
+#aboutDialog {
+ width: 620px;
+ /* Set an explicit line-height to avoid discrepancies in 'auto' spacing
+ across screens with different device DPI, which may cause font metrics
+ to round differently. */
+ line-height: 1.5;
+}
+
+#rightBox {
+ background-image: url("chrome://branding/content/about-wordmark.svg");
+ background-repeat: no-repeat;
+ background-size: 288px auto;
+ /* padding-top creates room for the wordmark */
+ padding-top: 38px;
+ margin-top: 20px;
+}
+
+#rightBox:-moz-locale-dir(rtl) {
+ background-position: 100% 0;
+}
+
+#bottomBox {
+ padding: 15px 10px 0;
+}
+
+#release {
+ font-weight: bold;
+ font-size: 125%;
+ margin-top: 10px;
+ margin-inline-start: 0;
+}
+
+#version {
+ font-weight: bold;
+ margin-top: 10px;
+ margin-inline-start: 0;
+ user-select: text;
+ -moz-user-focus: normal;
+ cursor: text;
+}
+
+#version.update {
+ font-weight: normal;
+ margin-top: 0;
+}
+
+#releasenotes {
+ margin-top: 10px;
+}
+
+#distribution,
+#distributionId {
+ display: none;
+ margin-block: 0;
+}
+
+.text-blurb {
+ margin-bottom: 10px;
+ margin-inline-start: 0;
+ padding-inline-start: 0;
+}
+
+#updateButton,
+#updateDeck > hbox > label {
+ margin-inline-start: 0;
+ padding-inline-start: 0;
+}
+
+.update-throbber {
+ width: 16px;
+ min-height: 16px;
+ margin-inline-end: 3px;
+}
+
+html|img.update-throbber {
+ vertical-align: middle;
+}
+
+image.update-throbber {
+ list-style-image: url("chrome://global/skin/icons/loading.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+ .update-throbber {
+ list-style-image: url("chrome://global/skin/icons/loading@2x.png");
+ }
+}
+
+description > .text-link,
+description > .text-link:focus {
+ margin: 0;
+ padding: 0;
+}
+
+#submit-feedback {
+ padding-inline-start: 10px;
+}
+
+.bottom-link,
+.bottom-link:focus {
+ text-align: center;
+ margin: 0 40px;
+}
+
+#currentChannel {
+ margin: 0;
+ padding: 0;
+ font-weight: bold;
+}
+
+#updateBox {
+ line-height: normal;
+}
+
+#icons > .icon {
+ -moz-context-properties: fill;
+ margin: 5px;
+ width: 16px;
+ height: 16px;
+}
+
+#icons:not(.checkingForUpdates, .downloading, .applying, .restarting) > .update-throbber,
+#icons:not(.noUpdatesFound) > .noUpdatesFound,
+#icons:not(.apply) > .apply {
+ display: none;
+}
+
+#icons > .noUpdatesFound {
+ fill: #30e60b;
+}
+
+#icons > .apply {
+ fill: white;
+}
diff --git a/browser/base/content/aboutDialog.js b/browser/base/content/aboutDialog.js
new file mode 100644
index 0000000000..b96337666c
--- /dev/null
+++ b/browser/base/content/aboutDialog.js
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from aboutDialog-appUpdater.js */
+
+// Services = object with smart getters for common XPCOM services
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+if (AppConstants.MOZ_UPDATER) {
+ Services.scriptloader.loadSubScript(
+ "chrome://browser/content/aboutDialog-appUpdater.js",
+ this
+ );
+}
+
+async function init(aEvent) {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ var distroId = Services.prefs.getCharPref("distribution.id", "");
+ if (distroId) {
+ var distroAbout = Services.prefs.getStringPref("distribution.about", "");
+ // If there is about text, we always show it.
+ if (distroAbout) {
+ var distroField = document.getElementById("distribution");
+ distroField.value = distroAbout;
+ distroField.style.display = "block";
+ }
+ // If it's not a mozilla distribution, show the rest,
+ // unless about text exists, then we always show.
+ if (!distroId.startsWith("mozilla-") || distroAbout) {
+ var distroVersion = Services.prefs.getCharPref(
+ "distribution.version",
+ ""
+ );
+ if (distroVersion) {
+ distroId += " - " + distroVersion;
+ }
+
+ var distroIdField = document.getElementById("distributionId");
+ distroIdField.value = distroId;
+ distroIdField.style.display = "block";
+ }
+ }
+
+ // Include the build ID and display warning if this is an "a#" (nightly or aurora) build
+ let versionId = "aboutDialog-version";
+ let versionAttributes = {
+ version: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ bits: Services.appinfo.is64Bit ? 64 : 32,
+ };
+
+ let version = Services.appinfo.version;
+ if (/a\d+$/.test(version)) {
+ versionId = "aboutDialog-version-nightly";
+ let buildID = Services.appinfo.appBuildID;
+ let year = buildID.slice(0, 4);
+ let month = buildID.slice(4, 6);
+ let day = buildID.slice(6, 8);
+ versionAttributes.isodate = `${year}-${month}-${day}`;
+
+ document.getElementById("experimental").hidden = false;
+ document.getElementById("communityDesc").hidden = true;
+ }
+
+ // Use Fluent arguments for append version and the architecture of the build
+ let versionField = document.getElementById("version");
+
+ document.l10n.setAttributes(versionField, versionId, versionAttributes);
+
+ await document.l10n.translateElements([versionField]);
+
+ // Show a release notes link if we have a URL.
+ let relNotesLink = document.getElementById("releasenotes");
+ let relNotesPrefType = Services.prefs.getPrefType(
+ "app.releaseNotesURL.aboutDialog"
+ );
+ if (relNotesPrefType != Services.prefs.PREF_INVALID) {
+ let relNotesURL = Services.urlFormatter.formatURLPref(
+ "app.releaseNotesURL.aboutDialog"
+ );
+ if (relNotesURL != "about:blank") {
+ relNotesLink.href = relNotesURL;
+ relNotesLink.hidden = false;
+ }
+ }
+
+ if (AppConstants.MOZ_UPDATER) {
+ gAppUpdater = new appUpdater({ buttonAutoFocus: true });
+
+ let channelLabel = document.getElementById("currentChannel");
+ let currentChannelText = document.getElementById("currentChannelText");
+ channelLabel.value = UpdateUtils.UpdateChannel;
+ if (/^release($|\-)/.test(channelLabel.value)) {
+ currentChannelText.hidden = true;
+ }
+ }
+
+ if (AppConstants.MOZ_APP_VERSION_DISPLAY.endsWith("esr")) {
+ document.getElementById("release").hidden = false;
+ }
+
+ window.sizeToContent();
+
+ if (AppConstants.platform == "macosx") {
+ window.moveTo(
+ screen.availWidth / 2 - window.outerWidth / 2,
+ screen.availHeight / 5
+ );
+ }
+}
diff --git a/browser/base/content/aboutDialog.xhtml b/browser/base/content/aboutDialog.xhtml
new file mode 100644
index 0000000000..fc6c382171
--- /dev/null
+++ b/browser/base/content/aboutDialog.xhtml
@@ -0,0 +1,174 @@
+<?xml version="1.0"?> <!-- -*- Mode: 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/.
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/aboutDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://branding/content/aboutDialog.css" type="text/css"?>
+
+<!DOCTYPE window [
+#ifdef XP_MACOSX
+#include browser-doctype.inc
+#endif
+]>
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="aboutDialog"
+ windowtype="Browser:About"
+ onload="init(event);"
+#ifdef MOZ_UPDATER
+ onunload="onUnload(event);"
+#endif
+#ifdef XP_MACOSX
+ inwindowmenu="false"
+#else
+ data-l10n-id="aboutDialog-title"
+#endif
+ role="dialog"
+ aria-describedby="version distribution distributionId communityDesc contributeDesc trademark"
+ >
+#ifdef XP_MACOSX
+#include macWindow.inc.xhtml
+#else
+ <script src="chrome://browser/content/utilityOverlay.js"/>
+#endif
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl"/>
+ <html:link rel="localization" href="browser/aboutDialog.ftl"/>
+ </linkset>
+
+ <script src="chrome://browser/content/aboutDialog.js"/>
+
+ <vbox id="aboutDialogContainer">
+ <hbox id="clientBox">
+ <vbox id="leftBox" flex="1"/>
+ <vbox id="rightBox" flex="1">
+ <label id="release" hidden="true">
+ <!-- This string is explicitly not translated -->
+ Extended Support Release
+ </label>
+#ifndef MOZ_UPDATER
+ <!-- This HBOX is duplicated below with class="update" -->
+ <hbox align="baseline">
+ <label id="version"/>
+ <label id="releasenotes" is="text-link" hidden="true" data-l10n-id="releaseNotes-link"/>
+ </hbox>
+#endif
+
+ <label id="distribution" class="text-blurb"/>
+ <label id="distributionId" class="text-blurb"/>
+
+ <vbox id="detailsBox">
+ <hbox id="updateBox">
+#ifdef MOZ_UPDATER
+ <html:div id="icons">
+ <html:img class="icon update-throbber" src="chrome://global/skin/icons/loading.png" role="presentation"/>
+ <html:img class="icon noUpdatesFound" src="chrome://global/skin/icons/check.svg" role="presentation"/>
+ <html:img class="icon apply" src="chrome://global/skin/icons/icon-refresh.svg" role="presentation"/>
+ </html:div>
+ <vbox>
+ <deck id="updateDeck" orient="vertical">
+ <hbox id="checkForUpdates" align="center">
+ <button id="checkForUpdatesButton" align="start"
+ data-l10n-id="update-checkForUpdatesButton"
+ oncommand="gAppUpdater.checkForUpdates();"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox id="downloadAndInstall" align="center">
+ <button id="downloadAndInstallButton" align="start"
+ oncommand="gAppUpdater.startDownload();"/>
+ <!-- label and accesskey will be filled by JS -->
+ <spacer flex="1"/>
+ </hbox>
+ <hbox id="apply" align="center">
+ <button id="updateButton" align="start"
+ data-l10n-id="update-updateButton"
+ oncommand="gAppUpdater.buttonRestartAfterDownload();"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox id="checkingForUpdates" align="center">
+ <label data-l10n-id="update-checkingForUpdates"/>
+ </hbox>
+ <hbox id="downloading" data-l10n-id="update-downloading-message" align="center">
+ <label id="downloadStatus" data-l10n-name="download-status"/>
+ </hbox>
+ <hbox id="applying" align="center">
+ <label data-l10n-id="update-applying"/>
+ </hbox>
+ <hbox id="downloadFailed" align="center" data-l10n-id="update-failed">
+ <label id="failedLink" is="text-link" data-l10n-name="failed-link"/>
+ </hbox>
+ <hbox id="policyDisabled" align="center">
+ <label data-l10n-id="update-adminDisabled"/>
+ </hbox>
+ <hbox id="noUpdatesFound" align="center">
+ <label data-l10n-id="update-noUpdatesFound"/>
+ </hbox>
+ <hbox id="otherInstanceHandlingUpdates" align="center">
+ <label data-l10n-id="update-otherInstanceHandlingUpdates"/>
+ </hbox>
+ <hbox id="manualUpdate" align="center" data-l10n-id="update-manual">
+ <label id="manualLink" is="text-link" data-l10n-name="manual-link"/>
+ </hbox>
+ <hbox id="unsupportedSystem" align="center" data-l10n-id="update-unsupported">
+ <label id="unsupportedLink" is="text-link" data-l10n-name="unsupported-link"/>
+ </hbox>
+ <hbox id="restarting" align="center">
+ <label data-l10n-id="update-restarting"/>
+ </hbox>
+ </deck>
+ <!-- This HBOX is duplicated above without class="update" -->
+ <hbox align="baseline">
+ <label id="version" class="update"/>
+ <label id="releasenotes" is="text-link" hidden="true" data-l10n-id="releaseNotes-link"/>
+ </hbox>
+ <description class="text-blurb">
+ <label is="text-link" onclick="openHelpLink('firefox-help')" data-l10n-id="aboutdialog-help-user"/>
+ <label id="submit-feedback" is="text-link" onclick="openFeedbackPage()" data-l10n-id="aboutdialog-submit-feedback"/>
+ </description>
+ </vbox>
+#endif
+ </hbox>
+
+#ifdef MOZ_UPDATER
+ <description class="text-blurb" id="currentChannelText" data-l10n-id="channel-description">
+ <label id="currentChannel" data-l10n-name="current-channel"/>
+ </description>
+#endif
+ <vbox id="experimental" hidden="true">
+ <description class="text-blurb" id="warningDesc" data-l10n-id="warningDesc-version"></description>
+ <description class="text-blurb" id="communityExperimentalDesc" data-l10n-id="community-exp">
+ <label is="text-link" href="https://www.mozilla.org/?utm_source=firefox-browser&#38;utm_medium=firefox-desktop&#38;utm_campaign=about-dialog" data-l10n-name="community-exp-mozillaLink"/>
+ <label is="text-link" useoriginprincipal="true" href="about:credits" data-l10n-name="community-exp-creditsLink"/>
+ </description>
+ </vbox>
+ <description class="text-blurb" id="communityDesc" data-l10n-id="community-2">
+ <label is="text-link" href="https://www.mozilla.org/?utm_source=firefox-browser&#38;utm_medium=firefox-desktop&#38;utm_campaign=about-dialog" data-l10n-name="community-mozillaLink"/>
+ <label is="text-link" useoriginprincipal="true" href="about:credits" data-l10n-name="community-creditsLink"/>
+ </description>
+ <description class="text-blurb" id="contributeDesc" data-l10n-id="helpus">
+ <label is="text-link" href="https://donate.mozilla.org/?utm_source=firefox&#38;utm_medium=referral&#38;utm_campaign=firefox_about&#38;utm_content=firefox_about" data-l10n-name="helpus-donateLink"/>
+ <label is="text-link" href="https://www.mozilla.org/contribute/?utm_source=firefox-browser&#38;utm_medium=firefox-desktop&#38;utm_campaign=about-dialog" data-l10n-name="helpus-getInvolvedLink"/>
+ </description>
+ </vbox>
+ </vbox>
+ </hbox>
+ <vbox id="bottomBox">
+ <hbox pack="center">
+ <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:license" data-l10n-id="bottomLinks-license"/>
+ <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:rights" data-l10n-id="bottomLinks-rights"/>
+ <label is="text-link" class="bottom-link" href="https://www.mozilla.org/privacy/?utm_source=firefox-browser&#38;utm_medium=firefox-desktop&#38;utm_campaign=about-dialog" data-l10n-id="bottomLinks-privacy"/>
+ </hbox>
+ <description id="trademark" data-l10n-id="trademarkInfo"></description>
+ </vbox>
+ </vbox>
+
+ <keyset>
+ <key keycode="VK_ESCAPE" oncommand="window.close();"/>
+ </keyset>
+
+</window>
diff --git a/browser/base/content/aboutFrameCrashed.html b/browser/base/content/aboutFrameCrashed.html
new file mode 100644
index 0000000000..2c4c3d38da
--- /dev/null
+++ b/browser/base/content/aboutFrameCrashed.html
@@ -0,0 +1,17 @@
+<!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>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://global/skin/in-content/info-pages.css"/>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/skin/aboutFrameCrashed.css"/>
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/aboutNetError.js b/browser/base/content/aboutNetError.js
new file mode 100644
index 0000000000..376d9d9dea
--- /dev/null
+++ b/browser/base/content/aboutNetError.js
@@ -0,0 +1,1265 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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-env mozilla/frame-script */
+
+import { parse } from "chrome://global/content/certviewer/certDecoder.js";
+import { pemToDER } from "chrome://global/content/certviewer/utils.js";
+
+const formatter = new Intl.DateTimeFormat("default");
+
+const HOST_NAME = new URL(RPMGetInnerMostURI(document.location.href)).hostname;
+
+// Used to check if we have a specific localized message for an error.
+const KNOWN_ERROR_TITLE_IDS = new Set([
+ // Error titles:
+ "connectionFailure-title",
+ "deniedPortAccess-title",
+ "dnsNotFound-title",
+ "fileNotFound-title",
+ "fileAccessDenied-title",
+ "generic-title",
+ "captivePortal-title",
+ "malformedURI-title",
+ "netInterrupt-title",
+ "notCached-title",
+ "netOffline-title",
+ "contentEncodingError-title",
+ "unsafeContentType-title",
+ "netReset-title",
+ "netTimeout-title",
+ "unknownProtocolFound-title",
+ "proxyConnectFailure-title",
+ "proxyResolveFailure-title",
+ "redirectLoop-title",
+ "unknownSocketType-title",
+ "nssFailure2-title",
+ "csp-xfo-error-title",
+ "corruptedContentError-title",
+ "remoteXUL-title",
+ "sslv3Used-title",
+ "inadequateSecurityError-title",
+ "blockedByPolicy-title",
+ "clockSkewError-title",
+ "networkProtocolError-title",
+ "nssBadCert-title",
+ "nssBadCert-sts-title",
+ "certerror-mitm-title",
+]);
+
+/* The error message IDs from nsserror.ftl get processed into
+ * aboutNetErrorCodes.js which is loaded before we are: */
+/* global KNOWN_ERROR_MESSAGE_IDS */
+
+// The following parameters are parsed from the error URL:
+// e - the error code
+// s - custom CSS class to allow alternate styling/favicons
+// d - error description
+// captive - "true" to indicate we're behind a captive portal.
+// Any other value is ignored.
+
+// Note that this file uses document.documentURI to get
+// the URL (with the format from above). This is because
+// document.location.href gets the current URI off the docshell,
+// which is the URL displayed in the location bar, i.e.
+// the URI that the user attempted to load.
+
+let searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
+
+// Set to true on init if the error code is nssBadCert.
+let gIsCertError;
+
+function getErrorCode() {
+ return searchParams.get("e");
+}
+
+function getCSSClass() {
+ return searchParams.get("s");
+}
+
+function getDescription() {
+ return searchParams.get("d");
+}
+
+function isCaptive() {
+ return searchParams.get("captive") == "true";
+}
+
+function retryThis(buttonEl) {
+ RPMSendAsyncMessage("Browser:EnableOnlineMode");
+ buttonEl.disabled = true;
+}
+
+function toggleDisplay(node) {
+ const toggle = {
+ "": "block",
+ none: "block",
+ block: "none",
+ };
+ return (node.style.display = toggle[node.style.display]);
+}
+
+function showBlockingErrorReporting() {
+ // Display blocking error reporting UI for XFO error and CSP error.
+ document.getElementById("blockingErrorReporting").style.display = "block";
+}
+
+function showPrefChangeContainer() {
+ const panel = document.getElementById("prefChangeContainer");
+ panel.style.display = "block";
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+ document
+ .getElementById("prefResetButton")
+ .addEventListener("click", function resetPreferences() {
+ RPMSendAsyncMessage("Browser:ResetSSLPreferences");
+ });
+ addAutofocus("#prefResetButton", "beforeend");
+}
+
+function showTls10Container() {
+ const panel = document.getElementById("enableTls10Container");
+ panel.style.display = "block";
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+ const button = document.getElementById("enableTls10Button");
+ button.addEventListener("click", function enableTls10(e) {
+ RPMSetBoolPref("security.tls.version.enable-deprecated", true);
+ retryThis(button);
+ });
+ addAutofocus("#enableTls10Button", "beforeend");
+}
+
+function setupAdvancedButton() {
+ // Get the hostname and add it to the panel
+ var panel = document.getElementById("badCertAdvancedPanel");
+ for (var span of panel.querySelectorAll("span.hostname")) {
+ span.textContent = HOST_NAME;
+ }
+
+ // Register click handler for the weakCryptoAdvancedPanel
+ document
+ .getElementById("advancedButton")
+ .addEventListener("click", togglePanelVisibility);
+
+ function togglePanelVisibility() {
+ toggleDisplay(panel);
+ if (gIsCertError) {
+ // Toggling the advanced panel must ensure that the debugging
+ // information panel is hidden as well, since it's opened by the
+ // error code link in the advanced panel.
+ var div = document.getElementById("certificateErrorDebugInformation");
+ div.style.display = "none";
+ }
+
+ if (panel.style.display == "block") {
+ // send event to trigger telemetry ping
+ var event = new CustomEvent("AboutNetErrorUIExpanded", { bubbles: true });
+ document.dispatchEvent(event);
+ }
+ }
+
+ if (!gIsCertError) {
+ return;
+ }
+
+ if (getCSSClass() == "expertBadCert") {
+ toggleDisplay(document.getElementById("badCertAdvancedPanel"));
+ // Toggling the advanced panel must ensure that the debugging
+ // information panel is hidden as well, since it's opened by the
+ // error code link in the advanced panel.
+ var div = document.getElementById("certificateErrorDebugInformation");
+ div.style.display = "none";
+ }
+
+ disallowCertOverridesIfNeeded();
+}
+
+function disallowCertOverridesIfNeeded() {
+ var cssClass = getCSSClass();
+ // Disallow overrides if this is a Strict-Transport-Security
+ // host and the cert is bad (STS Spec section 7.3) or if the
+ // certerror is in a frame (bug 633691).
+ if (cssClass == "badStsCert" || window != top) {
+ document
+ .getElementById("exceptionDialogButton")
+ .setAttribute("hidden", "true");
+ }
+ if (cssClass == "badStsCert") {
+ document.getElementById("badStsCertExplanation").removeAttribute("hidden");
+
+ let stsReturnButtonText = document.getElementById("stsReturnButtonText")
+ .textContent;
+ document.getElementById("returnButton").textContent = stsReturnButtonText;
+ document.getElementById(
+ "advancedPanelReturnButton"
+ ).textContent = stsReturnButtonText;
+
+ let stsMitmWhatCanYouDoAboutIt3 = document.getElementById(
+ "stsMitmWhatCanYouDoAboutIt3"
+ ).innerHTML;
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById(
+ "mitmWhatCanYouDoAboutIt3"
+ ).innerHTML = stsMitmWhatCanYouDoAboutIt3;
+ }
+}
+
+function setErrorPageStrings(err) {
+ let title = err + "-title";
+
+ let isCertError = err == "nssBadCert";
+ let className = getCSSClass();
+ if (isCertError && (window !== window.top || className == "badStsCert")) {
+ title = err + "-sts-title";
+ }
+
+ let cspXfoError = err === "cspBlocked" || err === "xfoBlocked";
+ if (cspXfoError) {
+ title = "csp-xfo-error-title";
+ }
+
+ let titleElement = document.querySelector(".title-text");
+
+ if (!KNOWN_ERROR_TITLE_IDS.has(title)) {
+ console.error("No strings exist for error:", title);
+ title = "generic-title";
+ }
+ document.l10n.setAttributes(titleElement, title);
+}
+
+function initPage() {
+ var err = getErrorCode();
+ // List of error pages with an illustration.
+ let illustratedErrors = [
+ "malformedURI",
+ "dnsNotFound",
+ "connectionFailure",
+ "netInterrupt",
+ "netTimeout",
+ "netReset",
+ "netOffline",
+ ];
+ if (illustratedErrors.includes(err)) {
+ document.body.classList.add("illustrated", err);
+ }
+ if (err == "blockedByPolicy") {
+ document.body.classList.add("blocked");
+ }
+
+ gIsCertError = err == "nssBadCert";
+ // Only worry about captive portals if this is a cert error.
+ let showCaptivePortalUI = isCaptive() && gIsCertError;
+ if (showCaptivePortalUI) {
+ err = "captivePortal";
+ }
+
+ let l10nErrId = err;
+ let className = getCSSClass();
+ if (className) {
+ document.body.classList.add(className);
+ }
+
+ if (gIsCertError && (window !== window.top || className == "badStsCert")) {
+ l10nErrId += "_sts";
+ }
+
+ let pageTitle = document.getElementById("ept_" + l10nErrId);
+ if (pageTitle) {
+ document.title = pageTitle.textContent;
+ }
+
+ // if it's an unknown error or there's no title or description
+ // defined, get the generic message
+ var errDesc = document.getElementById("ed_" + l10nErrId);
+ if (!errDesc) {
+ errDesc = document.getElementById("ed_generic");
+ }
+
+ setErrorPageStrings(err);
+
+ var sd = document.getElementById("errorShortDescText");
+ if (sd) {
+ if (gIsCertError) {
+ // eslint-disable-next-line no-unsanitized/property
+ sd.innerHTML = errDesc.innerHTML;
+ } else {
+ sd.textContent = getDescription();
+ }
+ }
+
+ if (gIsCertError) {
+ if (showCaptivePortalUI) {
+ initPageCaptivePortal();
+ } else {
+ initPageCertError();
+ updateContainerPosition();
+ }
+
+ initCertErrorPageActions();
+ setTechnicalDetailsOnCertError();
+ return;
+ }
+
+ addAutofocus("#netErrorButtonContainer > .try-again");
+
+ document.body.classList.add("neterror");
+
+ var ld = document.getElementById("errorLongDesc");
+ if (ld) {
+ // eslint-disable-next-line no-unsanitized/property
+ ld.innerHTML = errDesc.innerHTML;
+ }
+
+ if (err == "sslv3Used") {
+ document.getElementById("learnMoreContainer").style.display = "block";
+ document.body.className = "certerror";
+ }
+
+ // remove undisplayed errors to avoid bug 39098
+ var errContainer = document.getElementById("errorContainer");
+ errContainer.remove();
+
+ if (err == "remoteXUL") {
+ // Remove the "Try again" button for remote XUL errors given that
+ // it is useless.
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+ }
+
+ let learnMoreLink = document.getElementById("learnMoreLink");
+ let baseURL = RPMGetFormatURLPref("app.support.baseURL");
+ learnMoreLink.setAttribute("href", baseURL + "connection-not-secure");
+
+ if (err == "cspBlocked" || err == "xfoBlocked") {
+ // Remove the "Try again" button for XFO and CSP violations,
+ // since it's almost certainly useless. (Bug 553180)
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+
+ // Adding a button for opening websites blocked for CSP and XFO violations
+ // in a new window. (Bug 1461195)
+ document.getElementById("errorShortDesc").style.display = "none";
+
+ let hostString = document.location.hostname;
+ let longDescription = document.getElementById("errorLongDesc");
+ document.l10n.setAttributes(longDescription, "csp-xfo-blocked-long-desc", {
+ hostname: hostString,
+ });
+
+ document.getElementById("openInNewWindowContainer").style.display = "block";
+
+ let openInNewWindowButton = document.getElementById(
+ "openInNewWindowButton"
+ );
+ openInNewWindowButton.href = document.location.href;
+
+ // Add a learn more link
+ document.getElementById("learnMoreContainer").style.display = "block";
+ learnMoreLink.setAttribute("href", baseURL + "xframe-neterror-page");
+
+ setupBlockingReportingUI();
+ }
+
+ setNetErrorMessageFromCode();
+
+ // Pinning errors are of type nssFailure2
+ if (err == "nssFailure2") {
+ document.getElementById("learnMoreContainer").style.display = "block";
+
+ const errorCode = document.getNetErrorInfo().errorCodeString;
+ const isTlsVersionError =
+ errorCode == "SSL_ERROR_UNSUPPORTED_VERSION" ||
+ errorCode == "SSL_ERROR_PROTOCOL_VERSION_ALERT";
+ const tls10OverrideEnabled = RPMGetBoolPref(
+ "security.tls.version.enable-deprecated"
+ );
+
+ if (
+ isTlsVersionError &&
+ !tls10OverrideEnabled &&
+ !RPMPrefIsLocked("security.tls.version.min")
+ ) {
+ // security.tls.* prefs may be reset by the user when they
+ // encounter an error, so it's important that this has a
+ // different pref branch.
+ const showOverride = RPMGetBoolPref(
+ "security.certerrors.tls.version.show-override",
+ true
+ );
+
+ // This is probably a TLS 1.0 server; offer to re-enable.
+ if (showOverride) {
+ showTls10Container();
+ }
+ } else {
+ const hasPrefStyleError = [
+ "interrupted", // This happens with subresources that are above the max tls
+ "SSL_ERROR_NO_CIPHERS_SUPPORTED",
+ "SSL_ERROR_NO_CYPHER_OVERLAP",
+ "SSL_ERROR_PROTOCOL_VERSION_ALERT",
+ "SSL_ERROR_SSL_DISABLED",
+ "SSL_ERROR_UNSUPPORTED_VERSION",
+ ].some(substring => {
+ return substring == errorCode;
+ });
+
+ if (hasPrefStyleError) {
+ RPMAddMessageListener("HasChangedCertPrefs", msg => {
+ if (msg.data.hasChangedCertPrefs) {
+ // Configuration overrides might have caused this; offer to reset.
+ showPrefChangeContainer();
+ }
+ });
+ RPMSendAsyncMessage("GetChangedCertPrefs");
+ }
+ }
+ }
+
+ if (err == "sslv3Used") {
+ document.getElementById("advancedButton").style.display = "none";
+ }
+
+ if (err == "inadequateSecurityError" || err == "blockedByPolicy") {
+ // Remove the "Try again" button from pages that don't need it.
+ // For HTTP/2 inadequate security or pages blocked by policy, trying
+ // again won't help.
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+
+ var container = document.getElementById("errorLongDesc");
+ for (var span of container.querySelectorAll("span.hostname")) {
+ span.textContent = HOST_NAME;
+ }
+ }
+}
+
+function setupBlockingReportingUI() {
+ let checkbox = document.getElementById("automaticallyReportBlockingInFuture");
+
+ let reportingAutomatic = RPMGetBoolPref(
+ "security.xfocsp.errorReporting.automatic"
+ );
+ checkbox.checked = !!reportingAutomatic;
+
+ checkbox.addEventListener("change", function({ target: { checked } }) {
+ onSetBlockingReportAutomatic(checked);
+ });
+
+ let reportingEnabled = RPMGetBoolPref(
+ "security.xfocsp.errorReporting.enabled"
+ );
+
+ if (!reportingEnabled) {
+ return;
+ }
+
+ showBlockingErrorReporting();
+
+ if (reportingAutomatic) {
+ reportBlockingError();
+ }
+}
+
+function reportBlockingError() {
+ // We only report if we are in a frame.
+ if (window === window.top) {
+ return;
+ }
+
+ let err = getErrorCode();
+ // Ensure we only deal with XFO and CSP here.
+ if (!["xfoBlocked", "cspBlocked"].includes(err)) {
+ return;
+ }
+
+ let xfo_header = RPMGetHttpResponseHeader("X-Frame-Options");
+ let csp_header = RPMGetHttpResponseHeader("Content-Security-Policy");
+
+ // Extract the 'CSP: frame-ancestors' from the CSP header.
+ let reg = /(?:^|\s)frame-ancestors\s([^;]*)[$]*/i;
+ let match = reg.exec(csp_header);
+ csp_header = match ? match[1] : "";
+
+ // If it's the csp error page without the CSP: frame-ancestors, this means
+ // this error page is not triggered by CSP: frame-ancestors. So, we bail out
+ // early.
+ if (err === "cspBlocked" && !csp_header) {
+ return;
+ }
+
+ let xfoAndCspInfo = {
+ error_type: err === "xfoBlocked" ? "xfo" : "csp",
+ xfo_header,
+ csp_header,
+ };
+
+ RPMSendAsyncMessage("ReportBlockingError", {
+ scheme: document.location.protocol,
+ host: document.location.host,
+ port: parseInt(document.location.port) || -1,
+ path: document.location.pathname,
+ xfoAndCspInfo,
+ });
+}
+
+function onSetBlockingReportAutomatic(checked) {
+ RPMSetBoolPref("security.xfocsp.errorReporting.automatic", checked);
+
+ // If we're enabling reports, send a report for this failure.
+ if (checked) {
+ reportBlockingError();
+ }
+}
+
+async function setNetErrorMessageFromCode() {
+ let hostString = HOST_NAME;
+
+ let port = document.location.port;
+ if (port && port != 443) {
+ hostString += ":" + port;
+ }
+
+ let securityInfo;
+ try {
+ securityInfo = document.getNetErrorInfo();
+ } catch (ex) {
+ // We don't have a securityInfo when this is for example a DNS error.
+ return;
+ }
+
+ let desc = document.getElementById("errorShortDescText");
+ let errorCodeStr = securityInfo.errorCodeString || "";
+ let errorCodeStrId = errorCodeStr
+ .split("_")
+ .join("-")
+ .toLowerCase();
+ let errorCodeMsg = "";
+
+ if (KNOWN_ERROR_MESSAGE_IDS.has(errorCodeStrId)) {
+ [errorCodeMsg] = await document.l10n.formatValues([errorCodeStrId]);
+ }
+
+ if (!errorCodeMsg) {
+ console.warn("This error page has no error code in its security info");
+ document.l10n.setAttributes(desc, "ssl-connection-error", {
+ errorMessage: errorCodeStr,
+ hostname: hostString,
+ });
+ return;
+ }
+
+ document.l10n.setAttributes(desc, "ssl-connection-error", {
+ errorMessage: errorCodeMsg,
+ hostname: hostString,
+ });
+
+ let desc2 = document.getElementById("errorShortDescText2");
+ document.l10n.setAttributes(desc2, "cert-error-code-prefix", {
+ error: errorCodeStr,
+ });
+}
+
+// This function centers the error container after its content updates.
+// It is currently duplicated in NetErrorChild.jsm to avoid having to do
+// async communication to the page that would result in flicker.
+// TODO(johannh): Get rid of this duplication.
+function updateContainerPosition() {
+ let textContainer = document.getElementById("text-container");
+ // Using the vh CSS property our margin adapts nicely to window size changes.
+ // Unfortunately, this doesn't work correctly in iframes, which is why we need
+ // to manually compute the height there.
+ if (window.parent == window) {
+ textContainer.style.marginTop = `calc(50vh - ${textContainer.clientHeight /
+ 2}px)`;
+ } else {
+ let offset =
+ document.documentElement.clientHeight / 2 -
+ textContainer.clientHeight / 2;
+ if (offset > 0) {
+ textContainer.style.marginTop = `${offset}px`;
+ }
+ }
+}
+
+function initPageCaptivePortal() {
+ document.body.className = "captiveportal";
+ document
+ .getElementById("openPortalLoginPageButton")
+ .addEventListener("click", () => {
+ RPMSendAsyncMessage("Browser:OpenCaptivePortalPage");
+ });
+
+ addAutofocus("#openPortalLoginPageButton");
+ setupAdvancedButton();
+
+ // When the portal is freed, an event is sent by the parent process
+ // that we can pick up and attempt to reload the original page.
+ RPMAddMessageListener("AboutNetErrorCaptivePortalFreed", () => {
+ document.location.reload();
+ });
+}
+
+function initPageCertError() {
+ document.body.classList.add("certerror");
+ for (let host of document.querySelectorAll(".hostname")) {
+ host.textContent = HOST_NAME;
+ }
+
+ addAutofocus("#returnButton");
+ setupAdvancedButton();
+ document.getElementById("learnMoreContainer").style.display = "block";
+
+ let hideAddExceptionButton = RPMGetBoolPref(
+ "security.certerror.hideAddException",
+ false
+ );
+ if (hideAddExceptionButton) {
+ document.querySelector(".exceptionDialogButtonContainer").hidden = true;
+ }
+
+ let els = document.querySelectorAll("[data-telemetry-id]");
+ for (let el of els) {
+ el.addEventListener("click", recordClickTelemetry);
+ }
+
+ let failedCertInfo = document.getFailedCertSecurityInfo();
+ // Truncate the error code to avoid going over the allowed
+ // string size limit for telemetry events.
+ let errorCode = failedCertInfo.errorCodeString.substring(0, 40);
+ RPMRecordTelemetryEvent(
+ "security.ui.certerror",
+ "load",
+ "aboutcerterror",
+ errorCode,
+ {
+ has_sts: (getCSSClass() == "badStsCert").toString(),
+ is_frame: (window.parent != window).toString(),
+ }
+ );
+
+ setCertErrorDetails();
+}
+
+function recordClickTelemetry(e) {
+ let target = e.originalTarget;
+ let telemetryId = target.dataset.telemetryId;
+ let failedCertInfo = document.getFailedCertSecurityInfo();
+ // Truncate the error code to avoid going over the allowed
+ // string size limit for telemetry events.
+ let errorCode = failedCertInfo.errorCodeString.substring(0, 40);
+ RPMRecordTelemetryEvent(
+ "security.ui.certerror",
+ "click",
+ telemetryId,
+ errorCode,
+ {
+ has_sts: (getCSSClass() == "badStsCert").toString(),
+ is_frame: (window.parent != window).toString(),
+ }
+ );
+}
+
+function initCertErrorPageActions() {
+ document
+ .getElementById("returnButton")
+ .addEventListener("click", onReturnButtonClick);
+ document
+ .getElementById("advancedPanelReturnButton")
+ .addEventListener("click", onReturnButtonClick);
+ document
+ .getElementById("copyToClipboardTop")
+ .addEventListener("click", copyPEMToClipboard);
+ document
+ .getElementById("copyToClipboardBottom")
+ .addEventListener("click", copyPEMToClipboard);
+ document
+ .getElementById("exceptionDialogButton")
+ .addEventListener("click", addCertException);
+}
+
+function addCertException() {
+ const isPermanent =
+ !RPMIsWindowPrivate() &&
+ RPMGetBoolPref("security.certerrors.permanentOverride");
+ document.addCertException(!isPermanent).then(
+ () => {
+ location.reload();
+ },
+ err => {}
+ );
+}
+
+function onReturnButtonClick(e) {
+ RPMSendAsyncMessage("Browser:SSLErrorGoBack");
+}
+
+async function copyPEMToClipboard(e) {
+ let details = await getFailedCertificatesAsPEMString();
+ navigator.clipboard.writeText(details);
+}
+
+async function getFailedCertificatesAsPEMString() {
+ let location = document.location.href;
+ let failedCertInfo = document.getFailedCertSecurityInfo();
+ let errorMessage = failedCertInfo.errorMessage;
+ let hasHSTS = failedCertInfo.hasHSTS.toString();
+ let hasHPKP = failedCertInfo.hasHPKP.toString();
+ let [
+ hstsLabel,
+ hpkpLabel,
+ failedChainLabel,
+ ] = await document.l10n.formatValues([
+ { id: "cert-error-details-hsts-label", args: { hasHSTS } },
+ { id: "cert-error-details-key-pinning-label", args: { hasHPKP } },
+ { id: "cert-error-details-cert-chain-label" },
+ ]);
+
+ let certStrings = failedCertInfo.certChainStrings;
+ let failedChainCertificates = "";
+ for (let der64 of certStrings) {
+ let wrapped = der64.replace(/(\S{64}(?!$))/g, "$1\r\n");
+ failedChainCertificates +=
+ "-----BEGIN CERTIFICATE-----\r\n" +
+ wrapped +
+ "\r\n-----END CERTIFICATE-----\r\n";
+ }
+
+ let details =
+ location +
+ "\r\n\r\n" +
+ errorMessage +
+ "\r\n\r\n" +
+ hstsLabel +
+ "\r\n" +
+ hpkpLabel +
+ "\r\n\r\n" +
+ failedChainLabel +
+ "\r\n\r\n" +
+ failedChainCertificates;
+ return details;
+}
+
+function setCertErrorDetails(event) {
+ // Check if the connection is being man-in-the-middled. When the parent
+ // detects an intercepted connection, the page may be reloaded with a new
+ // error code (MOZILLA_PKIX_ERROR_MITM_DETECTED).
+ let failedCertInfo = document.getFailedCertSecurityInfo();
+ let mitmPrimingEnabled = RPMGetBoolPref(
+ "security.certerrors.mitm.priming.enabled"
+ );
+ if (
+ mitmPrimingEnabled &&
+ failedCertInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" &&
+ // Only do this check for top-level failures.
+ window.parent == window
+ ) {
+ RPMSendAsyncMessage("Browser:PrimeMitm");
+ }
+
+ let learnMoreLink = document.getElementById("learnMoreLink");
+ let baseURL = RPMGetFormatURLPref("app.support.baseURL");
+ learnMoreLink.setAttribute("href", baseURL + "connection-not-secure");
+ let errWhatToDo = document.getElementById(
+ "es_nssBadCert_" + failedCertInfo.errorCodeString
+ );
+ let es = document.getElementById("errorWhatToDoText");
+ let errWhatToDoTitle = document.getElementById("edd_nssBadCert");
+ let est = document.getElementById("errorWhatToDoTitleText");
+ let error = getErrorCode();
+
+ if (error == "sslv3Used") {
+ learnMoreLink.setAttribute("href", baseURL + "sslv3-error-messages");
+ }
+
+ if (error == "nssFailure2") {
+ let shortDesc = document.getElementById("errorShortDescText").textContent;
+ // nssFailure2 also gets us other non-overrideable errors. Choose
+ // a "learn more" link based on description:
+ if (shortDesc.includes("MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE")) {
+ learnMoreLink.setAttribute(
+ "href",
+ baseURL + "certificate-pinning-reports"
+ );
+ }
+ }
+
+ // This is set to true later if the user's system clock is at fault for this error.
+ let clockSkew = false;
+ document.body.setAttribute("code", failedCertInfo.errorCodeString);
+
+ let titleElement = document.querySelector(".title-text");
+ let desc;
+ switch (failedCertInfo.errorCodeString) {
+ case "SSL_ERROR_BAD_CERT_DOMAIN":
+ case "SEC_ERROR_OCSP_INVALID_SIGNING_CERT":
+ case "SEC_ERROR_UNKNOWN_ISSUER":
+ if (es) {
+ // eslint-disable-next-line no-unsanitized/property
+ es.innerHTML = errWhatToDo.innerHTML;
+ }
+ if (est) {
+ // eslint-disable-next-line no-unsanitized/property
+ est.innerHTML = errWhatToDoTitle.innerHTML;
+ }
+ updateContainerPosition();
+ break;
+
+ // This error code currently only exists for the Symantec distrust
+ // in Firefox 63, so we add copy explaining that to the user.
+ // In case of future distrusts of that scale we might need to add
+ // additional parameters that allow us to identify the affected party
+ // without replicating the complex logic from certverifier code.
+ case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED":
+ desc = document.getElementById("errorShortDescText2");
+ document.l10n.setAttributes(
+ desc,
+ "cert-error-symantec-distrust-description",
+ {
+ HOST_NAME,
+ }
+ );
+
+ let adminDesc = document.createElement("p");
+ document.l10n.setAttributes(
+ adminDesc,
+ "cert-error-symantec-distrust-admin"
+ );
+
+ learnMoreLink.href = baseURL + "symantec-warning";
+ updateContainerPosition();
+ break;
+
+ case "MOZILLA_PKIX_ERROR_MITM_DETECTED":
+ let autoEnabledEnterpriseRoots = RPMGetBoolPref(
+ "security.enterprise_roots.auto-enabled",
+ false
+ );
+ if (mitmPrimingEnabled && autoEnabledEnterpriseRoots) {
+ RPMSendAsyncMessage("Browser:ResetEnterpriseRootsPref");
+ }
+
+ // We don't actually know what the MitM is called (since we don't
+ // maintain a list), so we'll try and display the common name of the
+ // root issuer to the user. In the worst case they are as clueless as
+ // before, in the best case this gives them an actionable hint.
+ // This may be revised in the future.
+ let names = document.querySelectorAll(".mitm-name");
+ for (let span of names) {
+ span.textContent = failedCertInfo.issuerCommonName;
+ }
+
+ learnMoreLink.href = baseURL + "security-error";
+
+ document.l10n.setAttributes(titleElement, "certerror-mitm-title");
+ desc = document.getElementById("ed_mitm");
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("errorShortDescText").innerHTML = desc.innerHTML;
+
+ // eslint-disable-next-line no-unsanitized/property
+ es.innerHTML = errWhatToDo.innerHTML;
+ // eslint-disable-next-line no-unsanitized/property
+ est.innerHTML = errWhatToDoTitle.innerHTML;
+
+ updateContainerPosition();
+ break;
+
+ case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
+ learnMoreLink.href = baseURL + "security-error";
+ break;
+
+ // In case the certificate expired we make sure the system clock
+ // matches the remote-settings service (blocklist via Kinto) ping time
+ // and is not before the build date.
+ case "SEC_ERROR_EXPIRED_CERTIFICATE":
+ case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
+ case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE":
+ case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE":
+ learnMoreLink.href = baseURL + "time-errors";
+ // We check against the remote-settings server time first if available, because that allows us
+ // to give the user an approximation of what the correct time is.
+ let difference = RPMGetIntPref("services.settings.clock_skew_seconds", 0);
+ let lastFetched =
+ RPMGetIntPref("services.settings.last_update_seconds", 0) * 1000;
+
+ let now = Date.now();
+ let certRange = {
+ notBefore: failedCertInfo.certValidityRangeNotBefore,
+ notAfter: failedCertInfo.certValidityRangeNotAfter,
+ };
+ let approximateDate = now - difference * 1000;
+ // If the difference is more than a day, we last fetched the date in the last 5 days,
+ // and adjusting the date per the interval would make the cert valid, warn the user:
+ if (
+ Math.abs(difference) > 60 * 60 * 24 &&
+ now - lastFetched <= 60 * 60 * 24 * 5 * 1000 &&
+ certRange.notBefore < approximateDate &&
+ certRange.notAfter > approximateDate
+ ) {
+ clockSkew = true;
+ // If there is no clock skew with Kinto servers, check against the build date.
+ // (The Kinto ping could have happened when the time was still right, or not at all)
+ } else {
+ let appBuildID = RPMGetAppBuildID();
+ let year = parseInt(appBuildID.substr(0, 4), 10);
+ let month = parseInt(appBuildID.substr(4, 2), 10) - 1;
+ let day = parseInt(appBuildID.substr(6, 2), 10);
+
+ let buildDate = new Date(year, month, day);
+ let systemDate = new Date();
+
+ // We don't check the notBefore of the cert with the build date,
+ // as it is of course almost certain that it is now later than the build date,
+ // so we shouldn't exclude the possibility that the cert has become valid
+ // since the build date.
+ if (
+ buildDate > systemDate &&
+ new Date(certRange.notAfter) > buildDate
+ ) {
+ clockSkew = true;
+ }
+ }
+
+ let systemDate = formatter.format(new Date());
+ document.getElementById(
+ "wrongSystemTime_systemDate1"
+ ).textContent = systemDate;
+ if (clockSkew) {
+ document.body.classList.add("illustrated", "clockSkewError");
+ document.l10n.setAttributes(titleElement, "clockSkewError-title");
+ let clockErrDesc = document.getElementById("ed_clockSkewError");
+ desc = document.getElementById("errorShortDescText");
+ document.getElementById("errorShortDesc").style.display = "block";
+ if (desc) {
+ // eslint-disable-next-line no-unsanitized/property
+ desc.innerHTML = clockErrDesc.innerHTML;
+ }
+ let errorPageContainer = document.getElementById("errorPageContainer");
+ let textContainer = document.getElementById("text-container");
+ errorPageContainer.style.backgroundPosition = `left top calc(50vh - ${textContainer.clientHeight /
+ 2}px)`;
+ } else {
+ let targetElems = document.querySelectorAll(
+ "#wrongSystemTime_systemDate2"
+ );
+ for (let elem of targetElems) {
+ elem.textContent = systemDate;
+ }
+
+ let errDesc = document.getElementById(
+ "ed_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE"
+ );
+ let sd = document.getElementById("errorShortDescText");
+ // eslint-disable-next-line no-unsanitized/property
+ sd.innerHTML = errDesc.innerHTML;
+
+ let span = sd.querySelector(".hostname");
+ span.textContent = HOST_NAME;
+
+ // The secondary description mentions expired certificates explicitly
+ // and should only be shown if the certificate has actually expired
+ // instead of being not yet valid.
+ if (failedCertInfo.errorCodeString == "SEC_ERROR_EXPIRED_CERTIFICATE") {
+ let cssClass = getCSSClass();
+ let stsSuffix = cssClass == "badStsCert" ? "_sts" : "";
+ let errDesc2 = document.getElementById(
+ `ed2_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE${stsSuffix}`
+ );
+ let sd2 = document.getElementById("errorShortDescText2");
+ // eslint-disable-next-line no-unsanitized/property
+ sd2.innerHTML = errDesc2.innerHTML;
+ if (
+ Math.abs(difference) <= 60 * 60 * 24 &&
+ now - lastFetched <= 60 * 60 * 24 * 5 * 1000
+ ) {
+ errWhatToDo = document.getElementById(
+ "es_nssBadCert_SSL_ERROR_BAD_CERT_DOMAIN"
+ );
+ }
+ }
+
+ if (es) {
+ // eslint-disable-next-line no-unsanitized/property
+ es.innerHTML = errWhatToDo.innerHTML;
+ }
+ if (est) {
+ // eslint-disable-next-line no-unsanitized/property
+ est.textContent = errWhatToDoTitle.textContent;
+ est.style.fontWeight = "bold";
+ }
+ updateContainerPosition();
+ }
+ break;
+ }
+
+ // Add slightly more alarming UI unless there are indicators that
+ // show that the error is harmless or can not be skipped anyway.
+ let cssClass = getCSSClass();
+ // Don't alarm users when they can't continue to the website anyway...
+ if (
+ cssClass != "badStsCert" &&
+ // Errors in iframes can't be skipped either...
+ window.parent == window &&
+ // Also don't bother if it's just the user's clock being off...
+ !clockSkew &&
+ // Symantec distrust is likely harmless as well.
+ failedCertInfo.errorCodeString !=
+ "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED"
+ ) {
+ document.body.classList.add("caution");
+ }
+}
+
+// The optional argument is only here for testing purposes.
+async function setTechnicalDetailsOnCertError(
+ failedCertInfo = document.getFailedCertSecurityInfo()
+) {
+ let technicalInfo = document.getElementById("badCertTechnicalInfo");
+
+ function setL10NLabel(l10nId, args = {}, attrs = {}, rewrite = true) {
+ let elem = document.createElement("label");
+ if (rewrite) {
+ technicalInfo.textContent = "";
+ }
+ technicalInfo.appendChild(elem);
+
+ let newLines = document.createTextNode("\n \n");
+ technicalInfo.appendChild(newLines);
+
+ if (attrs) {
+ let link = document.createElement("a");
+ for (let attr of Object.keys(attrs)) {
+ link.setAttribute(attr, attrs[attr]);
+ }
+ elem.appendChild(link);
+ }
+
+ if (args) {
+ document.l10n.setAttributes(elem, l10nId, args);
+ } else {
+ document.l10n.setAttributes(elem, l10nId);
+ }
+ }
+
+ let cssClass = getCSSClass();
+ let error = getErrorCode();
+
+ let hostString = HOST_NAME;
+ let port = document.location.port;
+ if (port && port != 443) {
+ hostString += ":" + port;
+ }
+
+ let l10nId;
+ let args = {
+ hostname: hostString,
+ };
+ if (failedCertInfo.isUntrusted) {
+ switch (failedCertInfo.errorCodeString) {
+ case "MOZILLA_PKIX_ERROR_MITM_DETECTED":
+ setL10NLabel("cert-error-mitm-intro");
+ setL10NLabel("cert-error-mitm-mozilla", {}, {}, false);
+ setL10NLabel("cert-error-mitm-connection", {}, {}, false);
+ break;
+ case "SEC_ERROR_UNKNOWN_ISSUER":
+ setL10NLabel("cert-error-trust-unknown-issuer-intro");
+ setL10NLabel("cert-error-trust-unknown-issuer", args, {}, false);
+ break;
+ case "SEC_ERROR_CA_CERT_INVALID":
+ setL10NLabel("cert-error-intro", args);
+ setL10NLabel("cert-error-trust-cert-invalid", {}, {}, false);
+ break;
+ case "SEC_ERROR_UNTRUSTED_ISSUER":
+ setL10NLabel("cert-error-intro", args);
+ setL10NLabel("cert-error-trust-untrusted-issuer", {}, {}, false);
+ break;
+ case "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED":
+ setL10NLabel("cert-error-intro", args);
+ setL10NLabel(
+ "cert-error-trust-signature-algorithm-disabled",
+ {},
+ {},
+ false
+ );
+ break;
+ case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
+ setL10NLabel("cert-error-intro", args);
+ setL10NLabel("cert-error-trust-expired-issuer", {}, {}, false);
+ break;
+ case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
+ setL10NLabel("cert-error-intro", args);
+ setL10NLabel("cert-error-trust-self-signed", {}, {}, false);
+ break;
+ case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED":
+ setL10NLabel("cert-error-intro", args);
+ setL10NLabel("cert-error-trust-symantec", {}, {}, false);
+ break;
+ default:
+ setL10NLabel("cert-error-intro", args);
+ setL10NLabel("cert-error-untrusted-default", {}, {}, false);
+ }
+ } else if (failedCertInfo.isDomainMismatch) {
+ let serverCertBase64 = failedCertInfo.certChainStrings[0];
+ let parsed = await parse(pemToDER(serverCertBase64));
+ let subjectAltNamesExtension = parsed.ext.san;
+ let subjectAltNames = [];
+ if (subjectAltNamesExtension) {
+ for (let name of subjectAltNamesExtension.altNames) {
+ if (name[0] == "DNS Name" && name[1].length) {
+ subjectAltNames.push(name[1]);
+ }
+ }
+ }
+ let numSubjectAltNames = subjectAltNames.length;
+ if (numSubjectAltNames != 0) {
+ if (numSubjectAltNames == 1) {
+ args["alt-name"] = subjectAltNames[0];
+
+ // Let's check if we want to make this a link.
+ let okHost = subjectAltNames[0];
+ let href = "";
+ let thisHost = HOST_NAME;
+ let proto = document.location.protocol + "//";
+ // If okHost is a wildcard domain ("*.example.com") let's
+ // use "www" instead. "*.example.com" isn't going to
+ // get anyone anywhere useful. bug 432491
+ okHost = okHost.replace(/^\*\./, "www.");
+ /* case #1:
+ * example.com uses an invalid security certificate.
+ *
+ * The certificate is only valid for www.example.com
+ *
+ * Make sure to include the "." ahead of thisHost so that
+ * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
+ *
+ * We'd normally just use a RegExp here except that we lack a
+ * library function to escape them properly (bug 248062), and
+ * domain names are famous for having '.' characters in them,
+ * which would allow spurious and possibly hostile matches.
+ */
+
+ if (okHost.endsWith("." + thisHost)) {
+ href = proto + okHost;
+ }
+ /* case #2:
+ * browser.garage.maemo.org uses an invalid security certificate.
+ *
+ * The certificate is only valid for garage.maemo.org
+ */
+ if (thisHost.endsWith("." + okHost)) {
+ href = proto + okHost;
+ }
+
+ // If we set a link, meaning there's something helpful for
+ // the user here, expand the section by default
+ if (href && cssClass != "expertBadCert") {
+ document.getElementById("badCertAdvancedPanel").style.display =
+ "block";
+ if (error == "nssBadCert") {
+ // Toggling the advanced panel must ensure that the debugging
+ // information panel is hidden as well, since it's opened by the
+ // error code link in the advanced panel.
+ let div = document.getElementById(
+ "certificateErrorDebugInformation"
+ );
+ div.style.display = "none";
+ }
+ }
+
+ // Set the link if we want it.
+ if (href) {
+ setL10NLabel("cert-error-domain-mismatch-single", args, {
+ href,
+ "data-l10n-name": "domain-mismatch-link",
+ id: "cert_domain_link",
+ });
+ } else {
+ setL10NLabel("cert-error-domain-mismatch-single-nolink", args);
+ }
+ } else {
+ let names = subjectAltNames.join(", ");
+ args["subject-alt-names"] = names;
+ setL10NLabel("cert-error-domain-mismatch-multiple", args);
+ }
+ } else {
+ setL10NLabel("cert-error-domain-mismatch", { hostname: hostString });
+ }
+ } else if (failedCertInfo.isNotValidAtThisTime) {
+ let notBefore = failedCertInfo.validNotBefore;
+ let notAfter = failedCertInfo.validNotAfter;
+ args = {
+ hostname: hostString,
+ };
+ if (notBefore && Date.now() < notAfter) {
+ let notBeforeLocalTime = formatter.format(new Date(notBefore));
+ l10nId = "cert-error-not-yet-valid-now";
+ args["not-before-local-time"] = notBeforeLocalTime;
+ } else {
+ let notAfterLocalTime = formatter.format(new Date(notAfter));
+ l10nId = "cert-error-expired-now";
+ args["not-after-local-time"] = notAfterLocalTime;
+ }
+ setL10NLabel(l10nId, args);
+ }
+
+ setL10NLabel(
+ "cert-error-code-prefix-link",
+ { error: failedCertInfo.errorCodeString },
+ {
+ title: failedCertInfo.errorCodeString,
+ id: "errorCode",
+ "data-l10n-name": "error-code-link",
+ "data-telemetry-id": "error_code_link",
+ },
+ false
+ );
+ let errorCodeLink = document.getElementById("errorCode");
+ if (errorCodeLink) {
+ // We're attaching the event listener to the parent element and not on
+ // the errorCodeLink itself because event listeners cannot be attached
+ // to fluent DOM overlays.
+ technicalInfo.addEventListener("click", handleErrorCodeClick);
+ }
+
+ let div = document.getElementById("certificateErrorText");
+ div.textContent = await getFailedCertificatesAsPEMString();
+}
+
+function handleErrorCodeClick(event) {
+ if (event.target.id !== "errorCode") {
+ return;
+ }
+
+ let debugInfo = document.getElementById("certificateErrorDebugInformation");
+ debugInfo.style.display = "block";
+ debugInfo.scrollIntoView({ block: "start", behavior: "smooth" });
+ recordClickTelemetry(event);
+}
+
+/* Only do autofocus if we're the toplevel frame; otherwise we
+ don't want to call attention to ourselves! The key part is
+ that autofocus happens on insertion into the tree, so we
+ can remove the button, add @autofocus, and reinsert the
+ button.
+*/
+function addAutofocus(selector, position = "afterbegin") {
+ if (window.top == window) {
+ var button = document.querySelector(selector);
+ var parent = button.parentNode;
+ button.remove();
+ button.setAttribute("autofocus", "true");
+ parent.insertAdjacentElement(position, button);
+ }
+}
+
+for (let button of document.querySelectorAll(".try-again")) {
+ button.addEventListener("click", function() {
+ retryThis(this);
+ });
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ // Expose this so tests can call it.
+ window.setTechnicalDetailsOnCertError = setTechnicalDetailsOnCertError;
+
+ initPage();
+ // Dispatch this event so tests can detect that we finished loading the error page.
+ let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true });
+ document.dispatchEvent(event);
+});
diff --git a/browser/base/content/aboutNetError.xhtml b/browser/base/content/aboutNetError.xhtml
new file mode 100644
index 0000000000..f612d4df4c
--- /dev/null
+++ b/browser/base/content/aboutNetError.xhtml
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD
+ SYSTEM "chrome://browser/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+ <title>&loadError.label;</title>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link rel="icon" id="favicon" href="chrome://global/skin/icons/warning.svg"/>
+ <link rel="localization" href="browser/aboutCertError.ftl" />
+ <link rel="localization" href="browser/nsserrors.ftl" />
+ <link rel="localization" href="branding/brand.ftl"/>
+ </head>
+
+ <body dir="&locale.dir;">
+ <!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) -->
+ <div id="errorContainer">
+ <div id="errorPageTitlesContainer">
+ <span id="ept_nssBadCert">&certerror.pagetitle2;</span>
+ <span id="ept_nssBadCert_sts">&certerror.sts.pagetitle;</span>
+ <span id="ept_captivePortal">&captivePortal.title;</span>
+ <span id="ept_dnsNotFound">&dnsNotFound.pageTitle;</span>
+ <span id="ept_malformedURI">&malformedURI.pageTitle;</span>
+ <span id="ept_blockedByPolicy">&blockedByPolicy.title;</span>
+ </div>
+ <div id="errorDescriptionsContainer">
+ <div id="ed_generic">&generic.longDesc;</div>
+ <div id="ed_captivePortal">&captivePortal.longDesc2;</div>
+ <div id="ed_dnsNotFound">&dnsNotFound.longDesc1;</div>
+ <div id="ed_fileNotFound">&fileNotFound.longDesc;</div>
+ <div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div>
+ <div id="ed_malformedURI"></div>
+ <div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div>
+ <div id="ed_connectionFailure">&connectionFailure.longDesc;</div>
+ <div id="ed_netTimeout">&netTimeout.longDesc;</div>
+ <div id="ed_redirectLoop">&redirectLoop.longDesc;</div>
+ <div id="ed_unknownSocketType">&unknownSocketType.longDesc;</div>
+ <div id="ed_netReset">&netReset.longDesc;</div>
+ <div id="ed_notCached">&notCached.longDesc;</div>
+ <div id="ed_netOffline">&netOffline.longDesc2;</div>
+ <div id="ed_netInterrupt">&netInterrupt.longDesc;</div>
+ <div id="ed_deniedPortAccess">&deniedPortAccess.longDesc;</div>
+ <div id="ed_proxyResolveFailure">&proxyResolveFailure.longDesc;</div>
+ <div id="ed_proxyConnectFailure">&proxyConnectFailure.longDesc;</div>
+ <div id="ed_contentEncodingError">&contentEncodingError.longDesc;</div>
+ <div id="ed_unsafeContentType">&unsafeContentType.longDesc;</div>
+ <div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
+ <div id="ed_nssBadCert">&certerror.introPara2;</div>
+ <div id="ed_nssBadCert_sts">&certerror.sts.introPara;</div>
+ <div id="ed_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE">&certerror.expiredCert.introPara;</div>
+ <div id="ed_mitm">&certerror.mitm.longDesc;</div>
+ <div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
+ <div id="ed_xfoBlocked">&xfoBlocked.longDesc;</div>
+ <div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
+ <div id="ed_corruptedContentErrorv2">&corruptedContentErrorv2.longDesc;</div>
+ <div id="ed_sslv3Used">&sslv3Used.longDesc2;</div>
+ <div id="ed_inadequateSecurityError">&inadequateSecurityError.longDesc;</div>
+ <div id="ed_blockedByPolicy"></div>
+ <div id="ed_clockSkewError">&clockSkewError.longDesc;</div>
+ <div id="ed_networkProtocolError">&networkProtocolError.longDesc;</div>
+ </div>
+ <div id="errorDescriptions2Container">
+ <div id="ed2_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE">&certerror.expiredCert.secondPara2;</div>
+ <div id="ed2_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE_sts">&certerror.expiredCert.sts.secondPara;</div>
+ </div>
+ <div id="whatCanYouDoAboutItTitleContainer">
+ <div id="edd_nssBadCert"><strong>&certerror.whatCanYouDoAboutItTitle;</strong></div>
+ </div>
+ <div id="whatCanYouDoAboutItContainer">
+ <div id="es_nssBadCert_SEC_ERROR_UNKNOWN_ISSUER">&certerror.unknownIssuer.whatCanYouDoAboutIt;</div>
+ <div id="es_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE">&certerror.expiredCert.whatCanYouDoAboutIt2;</div>
+ <div id="es_nssBadCert_SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE">&certerror.expiredCert.whatCanYouDoAboutIt2;</div>
+ <div id="es_nssBadCert_MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE">&certerror.expiredCert.whatCanYouDoAboutIt2;</div>
+ <div id="es_nssBadCert_MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE">&certerror.expiredCert.whatCanYouDoAboutIt2;</div>
+ <div id="es_nssBadCert_SSL_ERROR_BAD_CERT_DOMAIN">&certerror.badCertDomain.whatCanYouDoAboutIt;</div>
+ <div id="es_nssBadCert_MOZILLA_PKIX_ERROR_MITM_DETECTED">
+ <ul>
+ <li>&certerror.mitm.whatCanYouDoAboutIt1;</li>
+ <li>&certerror.mitm.whatCanYouDoAboutIt2;</li>
+ <li id="mitmWhatCanYouDoAboutIt3">&certerror.mitm.whatCanYouDoAboutIt3;</li>
+ </ul>
+ </div>
+ </div>
+ <!-- Stores an alternative text for when we don't want to add "Recommended" to the
+ return button. This is one of many l10n atrocities in this file and should be
+ removed when we finally switch to Fluent. -->
+ <span id="stsReturnButtonText">&returnToPreviousPage.label;</span>
+ <span id="stsMitmWhatCanYouDoAboutIt3">&certerror.mitm.sts.whatCanYouDoAboutIt3;</span>
+ </div>
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer" class="container">
+ <div id="text-container">
+ <!-- Error Title -->
+ <div class="title">
+ <h1 class="title-text"/>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText" />
+ </div>
+
+ <div id="errorShortDesc2">
+ <p id="errorShortDescText2" />
+ </div>
+
+ <div id="errorWhatToDoTitle">
+ <p id="errorWhatToDoTitleText" />
+ </div>
+
+ <div id="errorWhatToDo">
+ <p id="badStsCertExplanation" hidden="true">&certerror.whatShouldIDo.badStsCertExplanation1;</p>
+ <p id="errorWhatToDoText" />
+ </div>
+
+ <div id="errorWhatToDo2">
+ <p id="errorWhatToDoText2" />
+ <p id="badStsCertExplanation" hidden="true">&certerror.whatShouldIDo.badStsCertExplanation1;</p>
+ </div>
+
+ <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
+ <div id="errorLongDesc" />
+
+ <div id="learnMoreContainer">
+ <p><a id="learnMoreLink" target="new" data-telemetry-id="learn_more_link">&errorReporting.learnMore;</a></p>
+ </div>
+
+ <div id="openInNewWindowContainer" class="button-container">
+ <p><a id="openInNewWindowButton" target="_blank" rel="noopener noreferrer">
+ <button class="primary" data-l10n-id="open-in-new-window-for-csp-or-xfo-error"></button></a></p>
+ </div>
+ </div>
+
+ <!-- UI to temporarily re-enable TLS 1.0 and 1.1.
+ This should be removed after March 2020, see bug 1579285. -->
+ <div id="enableTls10Container" class="button-container">
+ <p>&enableTls10.longDesc;</p>
+ <p>&enableTls10.note;</p>
+ <button id="enableTls10Button" class="primary">&enableTls10.label;</button>
+ </div>
+
+ <!-- UI for option to report certificate errors to Mozilla. Removed on
+ init for other error types .-->
+ <div id="prefChangeContainer" class="button-container">
+ <p>&prefReset.longDesc;</p>
+ <button id="prefResetButton" class="primary">&prefReset.label;</button>
+ </div>
+
+ <div id="certErrorAndCaptivePortalButtonContainer" class="button-container">
+ <button id="returnButton" class="primary" data-telemetry-id="return_button_top">&returnToPreviousPage1.label;</button>
+ <button id="openPortalLoginPageButton" class="primary">&openPortalLoginPage.label2;</button>
+ <button class="primary try-again">&retry.label;</button>
+ <button id="advancedButton" data-telemetry-id="advanced_button">&advanced2.label;</button>
+ </div>
+ </div>
+
+ <div id="netErrorButtonContainer" class="button-container">
+ <button class="primary try-again">&retry.label;</button>
+ </div>
+
+ <div id="advancedPanelContainer">
+ <div id="badCertAdvancedPanel" class="advanced-panel">
+ <p id="badCertTechnicalInfo"/>
+ <a id="viewCertificate" href="javascript:void(0)">&viewCertificate.label;</a>
+ <div id="advancedPanelButtonContainer" class="button-container">
+ <button id="advancedPanelReturnButton" class="primary" data-telemetry-id="return_button_adv">&returnToPreviousPage1.label;</button>
+ <button class="primary try-again">&retry.label;</button>
+ <div class="exceptionDialogButtonContainer">
+ <button id="exceptionDialogButton" data-telemetry-id="exception_button">&securityOverride.exceptionButton1Label;</button>
+ </div>
+ </div>
+ </div>
+
+ <div id="blockingErrorReporting">
+ <p class="toggle-container-with-text">
+ <input type="checkbox" id="automaticallyReportBlockingInFuture" role="checkbox"/>
+ <label for="automaticallyReportBlockingInFuture" >&errorReporting.automatic2;</label>
+ </p>
+ </div>
+
+ <div id="certificateErrorDebugInformation">
+ <button id="copyToClipboardTop" data-telemetry-id="clipboard_button_top">&certerror.copyToClipboard.label;</button>
+ <div id="certificateErrorText"/>
+ <button id="copyToClipboardBottom" data-telemetry-id="clipboard_button_bot">&certerror.copyToClipboard.label;</button>
+ </div>
+ </div>
+ </div>
+ </body>
+ <script src="chrome://browser/content/aboutNetErrorCodes.js"/>
+ <script src="chrome://global/content/certviewer/pvutils_bundle.js"></script>
+ <script src="chrome://global/content/certviewer/asn1js_bundle.js"></script>
+ <script src="chrome://global/content/certviewer/pkijs_bundle.js"></script>
+ <script type="module" src="chrome://browser/content/aboutNetError.js"/>
+</html>
diff --git a/browser/base/content/aboutRestartRequired.js b/browser/base/content/aboutRestartRequired.js
new file mode 100644
index 0000000000..d9ad24f6dd
--- /dev/null
+++ b/browser/base/content/aboutRestartRequired.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/. */
+
+/* eslint-env mozilla/frame-script */
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var AboutRestartRequired = {
+ /* Only do autofocus if we're the toplevel frame; otherwise we
+ don't want to call attention to ourselves! The key part is
+ that autofocus happens on insertion into the tree, so we
+ can remove the button, add @autofocus, and reinsert the
+ button.
+ */
+ addAutofocus() {
+ if (window.top == window) {
+ var button = document.getElementById("restart");
+ var parent = button.parentNode;
+ button.remove();
+ button.setAttribute("autofocus", "true");
+ parent.insertAdjacentElement("afterbegin", button);
+ }
+ },
+ restart() {
+ Services.startup.quit(
+ Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit
+ );
+ },
+ init() {
+ this.addAutofocus();
+ },
+};
+
+AboutRestartRequired.init();
+
+let restartButton = document.getElementById("restart");
+restartButton.onclick = function() {
+ AboutRestartRequired.restart();
+};
diff --git a/browser/base/content/aboutRestartRequired.xhtml b/browser/base/content/aboutRestartRequired.xhtml
new file mode 100644
index 0000000000..c7e7b9dcbb
--- /dev/null
+++ b/browser/base/content/aboutRestartRequired.xhtml
@@ -0,0 +1,42 @@
+<?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/. -->
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+ <title data-l10n-id="restart-required-title"></title>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/skin/aboutRestartRequired.css"/>
+ <!-- If the location of the favicon is changed here, the
+ FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link rel="icon" type="image/png" id="favicon"
+ href="chrome://global/skin/icons/warning.svg"/>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="browser/aboutRestartRequired.ftl"/>
+ </head>
+ <body>
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+ <div id="text-container">
+ <div id="title">
+ <h1 id="title-text" data-l10n-id="restart-required-header"></h1>
+ </div>
+ <div id="errorLongContent">
+ <div id="errorLongDesc">
+ <p data-l10n-id="restart-required-intro-brand"></p>
+ <p data-l10n-id="restart-required-description"></p>
+ </div>
+ </div>
+ </div>
+ <!-- Restart Button -->
+ <div id="restartButtonContainer" class="button-container">
+ <button id="restart" data-l10n-id="restart-button-label" class="primary" autocomplete="off"></button>
+ </div>
+ </div>
+ </body>
+ <script src="chrome://browser/content/aboutRestartRequired.js"/>
+</html>
diff --git a/browser/base/content/aboutRobots-icon.png b/browser/base/content/aboutRobots-icon.png
new file mode 100644
index 0000000000..e94c4e3621
--- /dev/null
+++ b/browser/base/content/aboutRobots-icon.png
Binary files differ
diff --git a/browser/base/content/aboutRobots.css b/browser/base/content/aboutRobots.css
new file mode 100644
index 0000000000..7ef0a58848
--- /dev/null
+++ b/browser/base/content/aboutRobots.css
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.title {
+ background-image: url("chrome://browser/content/aboutRobots-icon.png");
+}
diff --git a/browser/base/content/aboutRobots.js b/browser/base/content/aboutRobots.js
new file mode 100644
index 0000000000..836079df15
--- /dev/null
+++ b/browser/base/content/aboutRobots.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 buttonClicked = false;
+var button = document.getElementById("errorTryAgain");
+button.onclick = function() {
+ if (buttonClicked) {
+ button.style.visibility = "hidden";
+ } else {
+ var newLabel = button.getAttribute("label2");
+ button.textContent = newLabel;
+ buttonClicked = true;
+ }
+};
diff --git a/browser/base/content/aboutRobots.xhtml b/browser/base/content/aboutRobots.xhtml
new file mode 100644
index 0000000000..c533e0c9db
--- /dev/null
+++ b/browser/base/content/aboutRobots.xhtml
@@ -0,0 +1,62 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+ <title data-l10n-id="page-title"></title>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" media="all"/>
+ <link rel="icon" type="image/png" id="favicon" href="chrome://browser/content/robot.ico"/>
+ <link rel="stylesheet" href="chrome://browser/content/aboutRobots.css"/>
+ <linkset>
+ <link rel="localization" href="browser/aboutRobots.ftl"/>
+ </linkset>
+ </head>
+
+ <body>
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div class="container">
+
+ <!-- Error Title -->
+ <div class="title">
+ <h1 class="title-text" data-l10n-id="error-title-text"></h1>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div class="description">
+
+ <!-- Short Description -->
+ <div>
+ <p id="errorShortDescText" data-l10n-id="error-short-desc-text"></p>
+ </div>
+
+ <!-- Long Description -->
+ <div>
+ <ul>
+ <li data-l10n-id="error-long-desc1"></li>
+ <li data-l10n-id="error-long-desc2"></li>
+ <li data-l10n-id="error-long-desc3"></li>
+ <li data-l10n-id="error-long-desc4"></li>
+ </ul>
+ </div>
+
+ <!-- Short Description -->
+ <div>
+ <small data-l10n-id="error-trailer-desc-text"></small>
+ </div>
+
+ </div>
+
+ <!-- Button -->
+ <div class="button-container">
+ <button id="errorTryAgain"
+ data-l10n-id="error-try-again"
+ data-l10n-attrs="label2"></button>
+ </div>
+ </div>
+ </body>
+ <script src="chrome://browser/content/aboutRobots.js"/>
+</html>
diff --git a/browser/base/content/aboutTabCrashed.css b/browser/base/content/aboutTabCrashed.css
new file mode 100644
index 0000000000..1d7663c9d2
--- /dev/null
+++ b/browser/base/content/aboutTabCrashed.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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:not(.crashDumpSubmitted) #reportSent,
+html:not(.crashDumpAvailable) #reportBox,
+.container[multiple="true"] > .offers > #offerHelpMessageSingle,
+.container[multiple="false"] > .offers > #offerHelpMessageMultiple,
+.container[multiple="false"] > .button-container > #restoreAll {
+ display: none;
+}
diff --git a/browser/base/content/aboutTabCrashed.js b/browser/base/content/aboutTabCrashed.js
new file mode 100644
index 0000000000..f1b1234c1a
--- /dev/null
+++ b/browser/base/content/aboutTabCrashed.js
@@ -0,0 +1,306 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/frame-script */
+
+var AboutTabCrashed = {
+ /**
+ * This can be set to true once this page receives a message from the
+ * parent saying whether or not a crash report is available.
+ */
+ hasReport: false,
+
+ /**
+ * The messages that we might receive from the parent.
+ */
+ MESSAGES: ["SetCrashReportAvailable", "CrashReportSent", "UpdateCount"],
+
+ /**
+ * Items for which we will listen for click events.
+ */
+ CLICK_TARGETS: ["closeTab", "restoreTab", "restoreAll", "sendReport"],
+
+ /**
+ * Returns information about this crashed tab.
+ *
+ * @return (Object) An object with the following properties:
+ * title (String):
+ * The title of the page that crashed.
+ * URL (String):
+ * The URL of the page that crashed.
+ */
+ get pageData() {
+ delete this.pageData;
+
+ let URL = document.documentURI;
+ let queryString = URL.replace(/^about:tabcrashed?e=tabcrashed/, "");
+
+ let titleMatch = queryString.match(/d=([^&]*)/);
+ let URLMatch = queryString.match(/u=([^&]*)/);
+
+ return (this.pageData = {
+ title:
+ titleMatch && titleMatch[1] ? decodeURIComponent(titleMatch[1]) : "",
+ URL: URLMatch && URLMatch[1] ? decodeURIComponent(URLMatch[1]) : "",
+ });
+ },
+
+ init() {
+ addEventListener("DOMContentLoaded", this);
+
+ document.title = this.pageData.title;
+ },
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "UpdateCount": {
+ this.setMultiple(message.data.count > 1);
+ break;
+ }
+ case "SetCrashReportAvailable": {
+ this.onSetCrashReportAvailable(message);
+ break;
+ }
+ case "CrashReportSent": {
+ this.onCrashReportSent();
+ break;
+ }
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.onDOMContentLoaded();
+ break;
+ }
+ case "click": {
+ this.onClick(event);
+ break;
+ }
+ case "input": {
+ this.onInput(event);
+ break;
+ }
+ }
+ },
+
+ onDOMContentLoaded() {
+ this.MESSAGES.forEach(msg =>
+ RPMAddMessageListener(msg, this.receiveMessage.bind(this))
+ );
+
+ this.CLICK_TARGETS.forEach(targetID => {
+ let el = document.getElementById(targetID);
+ el.addEventListener("click", this);
+ });
+
+ // For setting "emailMe" checkbox automatically on email value change.
+ document.getElementById("email").addEventListener("input", this);
+
+ // Error pages are loaded as LOAD_BACKGROUND, so they don't get load events.
+ let event = new CustomEvent("AboutTabCrashedLoad", { bubbles: true });
+ document.dispatchEvent(event);
+
+ RPMSendAsyncMessage("Load");
+ },
+
+ onClick(event) {
+ switch (event.target.id) {
+ case "closeTab": {
+ this.sendMessage("closeTab");
+ break;
+ }
+
+ case "restoreTab": {
+ this.sendMessage("restoreTab");
+ break;
+ }
+
+ case "restoreAll": {
+ this.sendMessage("restoreAll");
+ break;
+ }
+
+ case "sendReport": {
+ this.showCrashReportUI(event.target.checked);
+ break;
+ }
+ }
+ },
+
+ onInput(event) {
+ switch (event.target.id) {
+ case "email": {
+ document.getElementById("emailMe").checked = !!event.target.value;
+ break;
+ }
+ }
+ },
+ /**
+ * After this page tells the parent that it has loaded, the parent
+ * will respond with whether or not a crash report is available. This
+ * method handles that message.
+ *
+ * @param message
+ * The message from the parent, which should contain a data
+ * Object property with the following properties:
+ *
+ * hasReport (bool):
+ * Whether or not there is a crash report.
+ *
+ * sendReport (bool):
+ * Whether or not the the user prefers to send the report
+ * by default.
+ *
+ * includeURL (bool):
+ * Whether or not the user prefers to send the URL of
+ * the tab that crashed.
+ *
+ * emailMe (bool):
+ * Whether or not to send the email address of the user
+ * in the report.
+ *
+ * email (String):
+ * The email address of the user (empty if emailMe is false).
+ *
+ * requestAutoSubmit (bool):
+ * Whether or not we should ask the user to automatically
+ * submit backlogged crash reports.
+ *
+ */
+ onSetCrashReportAvailable(message) {
+ let data = message.data;
+
+ if (data.hasReport) {
+ this.hasReport = true;
+ document.documentElement.classList.add("crashDumpAvailable");
+
+ document.getElementById("sendReport").checked = data.sendReport;
+ document.getElementById("includeURL").checked = data.includeURL;
+
+ if (data.requestEmail) {
+ document.getElementById("requestEmail").hidden = false;
+ document.getElementById("emailMe").checked = data.emailMe;
+ if (data.emailMe) {
+ document.getElementById("email").value = data.email;
+ }
+ }
+
+ this.showCrashReportUI(data.sendReport);
+ } else {
+ this.showCrashReportUI(false);
+ }
+
+ if (data.requestAutoSubmit) {
+ document.getElementById("requestAutoSubmit").hidden = false;
+ }
+
+ let event = new CustomEvent("AboutTabCrashedReady", { bubbles: true });
+ document.dispatchEvent(event);
+ },
+
+ /**
+ * Handler for when the parent reports that the crash report associated
+ * with this about:tabcrashed page has been sent.
+ */
+ onCrashReportSent() {
+ document.documentElement.classList.remove("crashDumpAvailable");
+ document.documentElement.classList.add("crashDumpSubmitted");
+ },
+
+ /**
+ * Toggles the display of the crash report form.
+ *
+ * @param shouldShow (bool)
+ * True if the crash report form should be shown
+ */
+ showCrashReportUI(shouldShow) {
+ let options = document.getElementById("options");
+ options.hidden = !shouldShow;
+ },
+
+ /**
+ * Toggles whether or not the page is one of several visible pages
+ * showing the crash reporter. This controls some of the language
+ * on the page, along with what the "primary" button is.
+ *
+ * @param hasMultiple (bool)
+ * True if there are multiple crash report pages being shown.
+ */
+ setMultiple(hasMultiple) {
+ let main = document.getElementById("main");
+ main.setAttribute("multiple", hasMultiple);
+
+ let restoreTab = document.getElementById("restoreTab");
+
+ // The "Restore All" button has the "primary" class by default, so
+ // we only need to modify the "Restore Tab" button.
+ if (hasMultiple) {
+ restoreTab.classList.remove("primary");
+ } else {
+ restoreTab.classList.add("primary");
+ }
+ },
+
+ /**
+ * Sends a message to the parent in response to the user choosing
+ * one of the actions available on the page. This might also send up
+ * crash report information if the user has chosen to submit a crash
+ * report.
+ *
+ * @param messageName (String)
+ * The message to send to the parent
+ */
+ sendMessage(messageName) {
+ let comments = "";
+ let email = "";
+ let URL = "";
+ let sendReport = false;
+ let emailMe = false;
+ let includeURL = false;
+ let autoSubmit = false;
+
+ if (this.hasReport) {
+ sendReport = document.getElementById("sendReport").checked;
+ if (sendReport) {
+ comments = document.getElementById("comments").value.trim();
+
+ includeURL = document.getElementById("includeURL").checked;
+ if (includeURL) {
+ URL = this.pageData.URL.trim();
+ }
+
+ if (!document.getElementById("requestEmail").hidden) {
+ emailMe = document.getElementById("emailMe").checked;
+ if (emailMe) {
+ email = document.getElementById("email").value.trim();
+ }
+ }
+ }
+ }
+
+ let requestAutoSubmit = document.getElementById("requestAutoSubmit");
+ if (requestAutoSubmit.hidden) {
+ // The checkbox is hidden if the user has already opted in to sending
+ // backlogged crash reports.
+ autoSubmit = true;
+ } else {
+ autoSubmit = document.getElementById("autoSubmit").checked;
+ }
+
+ RPMSendAsyncMessage(messageName, {
+ sendReport,
+ comments,
+ email,
+ emailMe,
+ includeURL,
+ URL,
+ autoSubmit,
+ hasReport: this.hasReport,
+ });
+ },
+};
+
+AboutTabCrashed.init();
diff --git a/browser/base/content/aboutTabCrashed.xhtml b/browser/base/content/aboutTabCrashed.xhtml
new file mode 100644
index 0000000000..0eb430bedc
--- /dev/null
+++ b/browser/base/content/aboutTabCrashed.xhtml
@@ -0,0 +1,86 @@
+<?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/. -->
+
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://global/skin/in-content/info-pages.css"/>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/content/aboutTabCrashed.css"/>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/skin/aboutTabCrashed.css"/>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="browser/aboutTabCrashed.ftl"/>
+
+ <title data-l10n-id="crashed-title"></title>
+ </head>
+
+ <body>
+ <div id="main" class="container" multiple="false">
+
+ <div class="title">
+ <h1 class="title-text" data-l10n-id="crashed-header"></h1>
+ </div>
+
+ <div class="offers">
+ <h2 data-l10n-id="crashed-offer-help"></h2>
+ <p id="offerHelpMessageSingle" data-l10n-id="crashed-single-offer-help-message"></p>
+ <p id="offerHelpMessageMultiple" data-l10n-id="crashed-multiple-offer-help-message"></p>
+ </div>
+
+ <div id="reportBox">
+ <h2 data-l10n-id="crashed-request-help"></h2>
+ <p data-l10n-id="crashed-request-help-message"></p>
+
+ <h2 data-l10n-id="crashed-request-report-title"></h2>
+
+ <div class="checkbox-with-label">
+ <input type="checkbox" id="sendReport" role="checkbox"/>
+ <label for="sendReport" data-l10n-id="crashed-send-report-2"></label>
+ </div>
+
+ <ul id="options">
+ <li>
+ <textarea id="comments" data-l10n-id="crashed-comment" rows="4"></textarea>
+ </li>
+
+ <li class="checkbox-with-label">
+ <input type="checkbox" id="includeURL" role="checkbox"/>
+ <label for="includeURL" data-l10n-id="crashed-include-URL-2"></label>
+ </li>
+
+ <li id="requestEmail" hidden="true">
+ <div class="checkbox-with-label">
+ <input type="checkbox" id="emailMe" role="checkbox"/>
+ <label for="emailMe" data-l10n-id="crashed-email-me"></label>
+ </div>
+ <input type="text" id="email" data-l10n-id="crashed-email-placeholder"/>
+ </li>
+ </ul>
+
+ <div id="requestAutoSubmit" hidden="true">
+ <h2 data-l10n-id="crashed-request-auto-submit-title"></h2>
+ <div class="checkbox-with-label">
+ <input type="checkbox" id="autoSubmit" role="checkbox"/>
+ <label for="autoSubmit" data-l10n-id="crashed-auto-submit-checkbox-2"></label>
+ </div>
+ </div>
+ </div>
+
+ <p id="reportSent" data-l10n-id="crashed-report-sent"></p>
+
+ <div class="button-container">
+ <button id="closeTab" data-l10n-id="crashed-close-tab-button"></button>
+ <button id="restoreTab" class="primary" data-l10n-id="crashed-restore-tab-button"></button>
+ <button id="restoreAll" autofocus="true" data-l10n-id="crashed-restore-all-button"/>
+ </div>
+ </div>
+ </body>
+ <script src="chrome://browser/content/aboutTabCrashed.js"/>
+</html>
diff --git a/browser/base/content/blockedSite.js b/browser/base/content/blockedSite.js
new file mode 100644
index 0000000000..9a1a9d8595
--- /dev/null
+++ b/browser/base/content/blockedSite.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Error url MUST be formatted like this:
+// about:blocked?e=error_code&u=url(&o=1)?
+// (o=1 when user overrides are allowed)
+
+// Note that this file uses document.documentURI to get
+// the URL (with the format from above). This is because
+// document.location.href gets the current URI off the docshell,
+// which is the URL displayed in the location bar, i.e.
+// the URI that the user attempted to load.
+
+function getErrorCode() {
+ var url = document.documentURI;
+ var error = url.search(/e\=/);
+ var duffUrl = url.search(/\&u\=/);
+ return decodeURIComponent(url.slice(error + 2, duffUrl));
+}
+
+function getURL() {
+ var url = document.documentURI;
+ var match = url.match(/&u=([^&]+)&/);
+
+ // match == null if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (!match) {
+ return "";
+ }
+
+ url = decodeURIComponent(match[1]);
+
+ // If this is a view-source page, then get then real URI of the page
+ if (url.startsWith("view-source:")) {
+ url = url.slice(12);
+ }
+ return url;
+}
+
+/**
+ * Check whether this warning page is overridable or not, in which case
+ * the "ignore the risk" suggestion in the error description
+ * should not be shown.
+ */
+function getOverride() {
+ var url = document.documentURI;
+ var match = url.match(/&o=1&/);
+ return !!match;
+}
+
+/**
+ * Attempt to get the hostname via document.location. Fail back
+ * to getURL so that we always return something meaningful.
+ */
+function getHostString() {
+ try {
+ return document.location.hostname;
+ } catch (e) {
+ return getURL();
+ }
+}
+
+function onClickSeeDetails() {
+ let details = document.getElementById("errorDescriptionContainer");
+ if (details.hidden) {
+ details.removeAttribute("hidden");
+ } else {
+ details.setAttribute("hidden", "true");
+ }
+}
+
+function initPage() {
+ var error = "";
+ switch (getErrorCode()) {
+ case "malwareBlocked":
+ error = "malware";
+ break;
+ case "deceptiveBlocked":
+ error = "phishing";
+ break;
+ case "unwantedBlocked":
+ error = "unwanted";
+ break;
+ case "harmfulBlocked":
+ error = "harmful";
+ break;
+ default:
+ return;
+ }
+
+ // Set page contents depending on type of blocked page
+ // Prepare the title and short description text
+ let titleText = document.getElementById("errorTitleText");
+ document.l10n.setAttributes(
+ titleText,
+ "safeb-blocked-" + error + "-page-title"
+ );
+ let shortDesc = document.getElementById("errorShortDescText");
+ document.l10n.setAttributes(
+ shortDesc,
+ "safeb-blocked-" + error + "-page-short-desc"
+ );
+
+ // Prepare the inner description, ensuring any redundant inner elements are removed.
+ let innerDesc = document.getElementById("errorInnerDescription");
+ let innerDescL10nID = "safeb-blocked-" + error + "-page-error-desc-";
+ if (!getOverride()) {
+ innerDescL10nID += "no-override";
+ document.getElementById("ignore_warning_link").remove();
+ } else {
+ innerDescL10nID += "override";
+ }
+ if (error == "unwanted" || error == "harmful") {
+ document.getElementById("report_detection").remove();
+ }
+
+ // Add the inner description:
+ // Map specific elements to a different message ID, to allow updates to
+ // existing labels
+ let descriptionMapping = {
+ malware: innerDescL10nID + "-sumo",
+ };
+ document.l10n.setAttributes(
+ innerDesc,
+ descriptionMapping[error] || innerDescL10nID,
+ {
+ sitename: getHostString(),
+ }
+ );
+
+ // Add the learn more content:
+ // Map specific elements to a different message ID, to allow updates to
+ // existing labels
+ let stringMapping = {
+ malware: "safeb-blocked-malware-page-learn-more-sumo",
+ };
+
+ let learnMore = document.getElementById("learn_more");
+ document.l10n.setAttributes(
+ learnMore,
+ stringMapping[error] || `safeb-blocked-${error}-page-learn-more`
+ );
+
+ // Set sitename to bold by adding class
+ let errorSitename = document.getElementById("error_desc_sitename");
+ errorSitename.setAttribute("class", "sitename");
+
+ let titleEl = document.createElement("title");
+ document.l10n.setAttributes(
+ titleEl,
+ "safeb-blocked-" + error + "-page-title"
+ );
+ document.head.appendChild(titleEl);
+
+ // Inform the test harness that we're done loading the page.
+ var event = new CustomEvent("AboutBlockedLoaded", {
+ bubbles: true,
+ detail: {
+ url: this.getURL(),
+ err: error,
+ },
+ });
+ document.dispatchEvent(event);
+}
+
+let seeDetailsButton = document.getElementById("seeDetailsButton");
+seeDetailsButton.addEventListener("click", onClickSeeDetails);
+// Note: It is important to run the script this way, instead of using
+// an onload handler. This is because error pages are loaded as
+// LOAD_BACKGROUND, which means that onload handlers will not be executed.
+initPage();
diff --git a/browser/base/content/blockedSite.xhtml b/browser/base/content/blockedSite.xhtml
new file mode 100644
index 0000000000..4601b0db82
--- /dev/null
+++ b/browser/base/content/blockedSite.xhtml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!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 xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+ <link rel="stylesheet" href="chrome://browser/skin/blockedSite.css" type="text/css" media="all" />
+ <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/blocklist_favicon.png"/>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="browser/safebrowsing/blockedSite.ftl"/>
+ </head>
+ <body>
+ <div id="errorPageContainer" class="container">
+
+ <!-- Error Title -->
+ <div id="errorTitle" class="title">
+ <h1 class="title-text" id="errorTitleText"></h1>
+ </div>
+
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText"></p>
+ </div>
+
+ <!-- Advisory -->
+ <div id="advisoryDesc">
+ <p id="advisoryDescText">
+ <a id="advisory_provider" data-l10n-name="advisory_provider"></a>
+ </p>
+ </div>
+
+ <!-- Action buttons -->
+ <div id="buttons" class="button-container">
+ <!-- Commands handled in browser.js -->
+ <button id="goBackButton" class="primary" data-l10n-id="safeb-palm-accept-label"></button>
+ <button id="seeDetailsButton" data-l10n-id="safeb-palm-see-details-label"></button>
+ </div>
+ </div>
+ <div id="errorDescriptionContainer" hidden="true">
+ <!-- Error Descriptions Handled in blockedSite.js -->
+ <div class="error-description" id="errorLongDesc">
+ <p id="errorInnerDescription">
+ <span id="error_desc_sitename" data-l10n-name="sitename"></span>
+ <a id="error_desc_link" data-l10n-name="error_desc_link"></a>
+ <a id="report_detection" data-l10n-name="report_detection"></a>
+ <a id="ignore_warning_link" data-l10n-name="ignore_warning_link"></a>
+ </p>
+ <p id="learn_more">
+ <a id="learn_more_link" data-l10n-name="learn_more_link"></a>
+ <a id="firefox_support" data-l10n-name="firefox_support"></a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </body>
+ <script src="chrome://browser/content/blockedSite.js"/>
+</html>
diff --git a/browser/base/content/browser-a11yUtils.js b/browser/base/content/browser-a11yUtils.js
new file mode 100644
index 0000000000..9bedb9238c
--- /dev/null
+++ b/browser/base/content/browser-a11yUtils.js
@@ -0,0 +1,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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Utility functions for UI accessibility.
+ */
+
+var A11yUtils = {
+ /**
+ * Announce a message to the user.
+ * This should only be used when something happens that is important to the
+ * user and will be noticed visually, but is not related to the focused
+ * control and is not a pop-up such as a doorhanger.
+ * For example, this could be used to indicate that Reader View is available
+ * or that Firefox is making a recommendation via the toolbar.
+ * This must be used with caution, as it can create unwanted verbosity and
+ * can thus hinder rather than help users if used incorrectly.
+ * Please only use this after consultation with the Mozilla accessibility
+ * team.
+ * @param {string} [options.id] The Fluent id of the message to announce. The
+ * ftl file must already be included in browser.xhtml. This must be
+ * specified unless a raw message is specified instead.
+ * @param {object} [options.args] Arguments for the Fluent message.
+ * @param {string} [options.raw] The raw, already localized message to
+ * announce. You should generally prefer a Fluent id instead, but in
+ * rare cases, this might not be feasible.
+ * @param {Element} [options.source] The element with which the announcement
+ * is associated. This should generally be something the user can
+ * interact with to respond to the announcement. For example, for an
+ * announcement indicating that Reader View is available, this should
+ * be the Reader View button on the toolbar.
+ */
+ async announce({ id = null, args = {}, raw = null, source = document } = {}) {
+ if ((!id && !raw) || (id && raw)) {
+ throw new Error("One of raw or id must be specified.");
+ }
+
+ // Cancel a previous pending call if any.
+ if (this._cancelAnnounce) {
+ this._cancelAnnounce();
+ this._cancelAnnounce = null;
+ }
+
+ let message;
+ if (id) {
+ let cancel = false;
+ this._cancelAnnounce = () => (cancel = true);
+ message = await document.l10n.formatValue(id, args);
+ if (cancel) {
+ // announce() was called again while we were waiting for translation.
+ return;
+ }
+ // No more async operations from this point.
+ this._cancelAnnounce = null;
+ } else {
+ // We run fully synchronously if a raw message is provided.
+ message = raw;
+ }
+
+ // For now, we don't use source, but it might be useful in future.
+ // For example, we might use it when we support announcement events on
+ // more platforms or it could be used to have a keyboard shortcut which
+ // focuses the last element to announce a message.
+ let live = document.getElementById("a11y-announcement");
+ // We use role="alert" because JAWS doesn't support aria-live in browser
+ // chrome.
+ // Gecko a11y needs an insertion to trigger an alert event. This is why
+ // we can't just use aria-label on the alert.
+ if (live.firstChild) {
+ live.firstChild.remove();
+ }
+ let label = document.createElement("label");
+ label.setAttribute("aria-label", message);
+ live.appendChild(label);
+ },
+};
diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js
new file mode 100644
index 0000000000..78b94a68d6
--- /dev/null
+++ b/browser/base/content/browser-addons.js
@@ -0,0 +1,1110 @@
+/* -*- 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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+customElements.define(
+ "addon-progress-notification",
+ class MozAddonProgressNotification extends customElements.get(
+ "popupnotification"
+ ) {
+ show() {
+ super.show();
+ this.progressmeter = document.getElementById(
+ "addon-progress-notification-progressmeter"
+ );
+
+ this.progresstext = document.getElementById(
+ "addon-progress-notification-progresstext"
+ );
+
+ if (!this.notification) {
+ return;
+ }
+
+ this.notification.options.installs.forEach(function(aInstall) {
+ aInstall.addListener(this);
+ }, this);
+
+ // Calling updateProgress can sometimes cause this notification to be
+ // removed in the middle of refreshing the notification panel which
+ // makes the panel get refreshed again. Just initialise to the
+ // undetermined state and then schedule a proper check at the next
+ // opportunity
+ this.setProgress(0, -1);
+ this._updateProgressTimeout = setTimeout(
+ this.updateProgress.bind(this),
+ 0
+ );
+ }
+
+ disconnectedCallback() {
+ this.destroy();
+ }
+
+ destroy() {
+ if (!this.notification) {
+ return;
+ }
+ this.notification.options.installs.forEach(function(aInstall) {
+ aInstall.removeListener(this);
+ }, this);
+
+ clearTimeout(this._updateProgressTimeout);
+ }
+
+ setProgress(aProgress, aMaxProgress) {
+ if (aMaxProgress == -1) {
+ this.progressmeter.removeAttribute("value");
+ } else {
+ this.progressmeter.setAttribute(
+ "value",
+ (aProgress * 100) / aMaxProgress
+ );
+ }
+
+ let now = Date.now();
+
+ if (!this.notification.lastUpdate) {
+ this.notification.lastUpdate = now;
+ this.notification.lastProgress = aProgress;
+ return;
+ }
+
+ let delta = now - this.notification.lastUpdate;
+ if (delta < 400 && aProgress < aMaxProgress) {
+ return;
+ }
+
+ // Set min. time delta to avoid division by zero in the upcoming speed calculation
+ delta = Math.max(delta, 400);
+ delta /= 1000;
+
+ // This algorithm is the same used by the downloads code.
+ let speed = (aProgress - this.notification.lastProgress) / delta;
+ if (this.notification.speed) {
+ speed = speed * 0.9 + this.notification.speed * 0.1;
+ }
+
+ this.notification.lastUpdate = now;
+ this.notification.lastProgress = aProgress;
+ this.notification.speed = speed;
+
+ let status = null;
+ [status, this.notification.last] = DownloadUtils.getDownloadStatus(
+ aProgress,
+ aMaxProgress,
+ speed,
+ this.notification.last
+ );
+ this.progresstext.setAttribute("value", status);
+ this.progresstext.setAttribute("tooltiptext", status);
+ }
+
+ cancel() {
+ let installs = this.notification.options.installs;
+ installs.forEach(function(aInstall) {
+ try {
+ aInstall.cancel();
+ } catch (e) {
+ // Cancel will throw if the download has already failed
+ }
+ }, this);
+
+ PopupNotifications.remove(this.notification);
+ }
+
+ updateProgress() {
+ if (!this.notification) {
+ return;
+ }
+
+ let downloadingCount = 0;
+ let progress = 0;
+ let maxProgress = 0;
+
+ this.notification.options.installs.forEach(function(aInstall) {
+ if (aInstall.maxProgress == -1) {
+ maxProgress = -1;
+ }
+ progress += aInstall.progress;
+ if (maxProgress >= 0) {
+ maxProgress += aInstall.maxProgress;
+ }
+ if (aInstall.state < AddonManager.STATE_DOWNLOADED) {
+ downloadingCount++;
+ }
+ });
+
+ if (downloadingCount == 0) {
+ this.destroy();
+ this.progressmeter.removeAttribute("value");
+ let status = gNavigatorBundle.getString("addonDownloadVerifying");
+ this.progresstext.setAttribute("value", status);
+ this.progresstext.setAttribute("tooltiptext", status);
+ } else {
+ this.setProgress(progress, maxProgress);
+ }
+ }
+
+ onDownloadProgress() {
+ this.updateProgress();
+ }
+
+ onDownloadFailed() {
+ this.updateProgress();
+ }
+
+ onDownloadCancelled() {
+ this.updateProgress();
+ }
+
+ onDownloadEnded() {
+ this.updateProgress();
+ }
+ }
+);
+
+// Removes a doorhanger notification if all of the installs it was notifying
+// about have ended in some way.
+function removeNotificationOnEnd(notification, installs) {
+ let count = installs.length;
+
+ function maybeRemove(install) {
+ install.removeListener(this);
+
+ if (--count == 0) {
+ // Check that the notification is still showing
+ let current = PopupNotifications.getNotification(
+ notification.id,
+ notification.browser
+ );
+ if (current === notification) {
+ notification.remove();
+ }
+ }
+ }
+
+ for (let install of installs) {
+ install.addListener({
+ onDownloadCancelled: maybeRemove,
+ onDownloadFailed: maybeRemove,
+ onInstallFailed: maybeRemove,
+ onInstallEnded: maybeRemove,
+ });
+ }
+}
+
+var gXPInstallObserver = {
+ _findChildShell(aDocShell, aSoughtShell) {
+ if (aDocShell == aSoughtShell) {
+ return aDocShell;
+ }
+
+ var node = aDocShell.QueryInterface(Ci.nsIDocShellTreeItem);
+ for (var i = 0; i < node.childCount; ++i) {
+ var docShell = node.getChildAt(i);
+ docShell = this._findChildShell(docShell, aSoughtShell);
+ if (docShell == aSoughtShell) {
+ return docShell;
+ }
+ }
+ return null;
+ },
+
+ _getBrowser(aDocShell) {
+ for (let browser of gBrowser.browsers) {
+ if (this._findChildShell(browser.docShell, aDocShell)) {
+ return browser;
+ }
+ }
+ return null;
+ },
+
+ pendingInstalls: new WeakMap(),
+
+ showInstallConfirmation(browser, installInfo, height = undefined) {
+ // If the confirmation notification is already open cache the installInfo
+ // and the new confirmation will be shown later
+ if (
+ PopupNotifications.getNotification("addon-install-confirmation", browser)
+ ) {
+ let pending = this.pendingInstalls.get(browser);
+ if (pending) {
+ pending.push(installInfo);
+ } else {
+ this.pendingInstalls.set(browser, [installInfo]);
+ }
+ return;
+ }
+
+ let showNextConfirmation = () => {
+ // Make sure the browser is still alive.
+ if (!gBrowser.browsers.includes(browser)) {
+ return;
+ }
+
+ let pending = this.pendingInstalls.get(browser);
+ if (pending && pending.length) {
+ this.showInstallConfirmation(browser, pending.shift());
+ }
+ };
+
+ // If all installs have already been cancelled in some way then just show
+ // the next confirmation
+ if (
+ installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)
+ ) {
+ showNextConfirmation();
+ return;
+ }
+
+ const anchorID = "addons-notification-icon";
+
+ // Make notifications persistent
+ var options = {
+ displayURI: installInfo.originatingURI,
+ persistent: true,
+ hideClose: true,
+ };
+
+ let acceptInstallation = () => {
+ for (let install of installInfo.installs) {
+ install.install();
+ }
+ installInfo = null;
+
+ Services.telemetry
+ .getHistogramById("SECURITY_UI")
+ .add(
+ Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH
+ );
+ };
+
+ let cancelInstallation = () => {
+ if (installInfo) {
+ for (let install of installInfo.installs) {
+ // The notification may have been closed because the add-ons got
+ // cancelled elsewhere, only try to cancel those that are still
+ // pending install.
+ if (install.state != AddonManager.STATE_CANCELLED) {
+ install.cancel();
+ }
+ }
+ }
+
+ showNextConfirmation();
+ };
+
+ let unsigned = installInfo.installs.filter(
+ i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
+ );
+ let someUnsigned =
+ !!unsigned.length && unsigned.length < installInfo.installs.length;
+
+ options.eventCallback = aEvent => {
+ switch (aEvent) {
+ case "removed":
+ cancelInstallation();
+ break;
+ case "shown":
+ let addonList = document.getElementById(
+ "addon-install-confirmation-content"
+ );
+ while (addonList.firstChild) {
+ addonList.firstChild.remove();
+ }
+
+ for (let install of installInfo.installs) {
+ let container = document.createXULElement("hbox");
+
+ let name = document.createXULElement("label");
+ name.setAttribute("value", install.addon.name);
+ name.setAttribute("class", "addon-install-confirmation-name");
+ container.appendChild(name);
+
+ if (
+ someUnsigned &&
+ install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING
+ ) {
+ let unsignedLabel = document.createXULElement("label");
+ unsignedLabel.setAttribute(
+ "value",
+ gNavigatorBundle.getString("addonInstall.unsigned")
+ );
+ unsignedLabel.setAttribute(
+ "class",
+ "addon-install-confirmation-unsigned"
+ );
+ container.appendChild(unsignedLabel);
+ }
+
+ addonList.appendChild(container);
+ }
+ break;
+ }
+ };
+
+ options.learnMoreURL = Services.urlFormatter.formatURLPref(
+ "app.support.baseURL"
+ );
+
+ let messageString;
+ let notification = document.getElementById(
+ "addon-install-confirmation-notification"
+ );
+ if (unsigned.length == installInfo.installs.length) {
+ // None of the add-ons are verified
+ messageString = gNavigatorBundle.getString(
+ "addonConfirmInstallUnsigned.message"
+ );
+ notification.setAttribute("warning", "true");
+ options.learnMoreURL += "unsigned-addons";
+ } else if (!unsigned.length) {
+ // All add-ons are verified or don't need to be verified
+ messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
+ notification.removeAttribute("warning");
+ options.learnMoreURL += "find-and-install-add-ons";
+ } else {
+ // Some of the add-ons are unverified, the list of names will indicate
+ // which
+ messageString = gNavigatorBundle.getString(
+ "addonConfirmInstallSomeUnsigned.message"
+ );
+ notification.setAttribute("warning", "true");
+ options.learnMoreURL += "unsigned-addons";
+ }
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+
+ messageString = PluralForm.get(installInfo.installs.length, messageString);
+ messageString = messageString.replace("#1", brandShortName);
+ messageString = messageString.replace("#2", installInfo.installs.length);
+
+ let action = {
+ label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
+ accessKey: gNavigatorBundle.getString(
+ "addonInstall.acceptButton2.accesskey"
+ ),
+ callback: acceptInstallation,
+ };
+
+ let secondaryAction = {
+ label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
+ accessKey: gNavigatorBundle.getString(
+ "addonInstall.cancelButton.accesskey"
+ ),
+ callback: () => {},
+ };
+
+ if (height) {
+ notification.style.minHeight = height + "px";
+ }
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (tab) {
+ gBrowser.selectedTab = tab;
+ }
+
+ let popup = PopupNotifications.show(
+ browser,
+ "addon-install-confirmation",
+ messageString,
+ anchorID,
+ action,
+ [secondaryAction],
+ options
+ );
+
+ removeNotificationOnEnd(popup, installInfo.installs);
+
+ Services.telemetry
+ .getHistogramById("SECURITY_UI")
+ .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
+ },
+
+ // IDs of addon install related notifications
+ NOTIFICATION_IDS: [
+ "addon-install-blocked",
+ "addon-install-complete",
+ "addon-install-confirmation",
+ "addon-install-failed",
+ "addon-install-origin-blocked",
+ "addon-progress",
+ "addon-webext-permissions",
+ "xpinstall-disabled",
+ ],
+
+ /**
+ * Remove all opened addon installation notifications
+ *
+ * @param {*} browser - Browser to remove notifications for
+ * @returns {boolean} - true if notifications have been removed.
+ */
+ removeAllNotifications(browser) {
+ let notifications = this.NOTIFICATION_IDS.map(id =>
+ PopupNotifications.getNotification(id, browser)
+ ).filter(notification => notification != null);
+
+ PopupNotifications.remove(notifications, true);
+
+ return !!notifications.length;
+ },
+
+ logWarningFullScreenInstallBlocked() {
+ // If notifications have been removed, log a warning to the website console
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ let message = gBrowserBundle.GetStringFromName(
+ "addonInstallFullScreenBlocked"
+ );
+ consoleMsg.initWithWindowID(
+ message,
+ gBrowser.currentURI.spec,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.warningFlag,
+ "FullScreen",
+ gBrowser.selectedBrowser.innerWindowID
+ );
+ Services.console.logMessage(consoleMsg);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ var brandBundle = document.getElementById("bundle_brand");
+ var installInfo = aSubject.wrappedJSObject;
+ var browser = installInfo.browser;
+
+ // Make sure the browser is still alive.
+ if (!browser || !gBrowser.browsers.includes(browser)) {
+ return;
+ }
+
+ const anchorID = "addons-notification-icon";
+ var messageString, action;
+ var brandShortName = brandBundle.getString("brandShortName");
+
+ var notificationID = aTopic;
+ // Make notifications persistent
+ var options = {
+ displayURI: installInfo.originatingURI,
+ persistent: true,
+ hideClose: true,
+ timeout: Date.now() + 30000,
+ };
+
+ switch (aTopic) {
+ case "addon-install-disabled": {
+ notificationID = "xpinstall-disabled";
+ let secondaryActions = null;
+
+ if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
+ messageString = gNavigatorBundle.getString(
+ "xpinstallDisabledMessageLocked"
+ );
+ } else {
+ messageString = gNavigatorBundle.getString(
+ "xpinstallDisabledMessage"
+ );
+
+ action = {
+ label: gNavigatorBundle.getString("xpinstallDisabledButton"),
+ accessKey: gNavigatorBundle.getString(
+ "xpinstallDisabledButton.accesskey"
+ ),
+ callback: function editPrefs() {
+ Services.prefs.setBoolPref("xpinstall.enabled", true);
+ },
+ };
+
+ secondaryActions = [
+ {
+ label: gNavigatorBundle.getString(
+ "addonInstall.cancelButton.label"
+ ),
+ accessKey: gNavigatorBundle.getString(
+ "addonInstall.cancelButton.accesskey"
+ ),
+ callback: () => {},
+ },
+ ];
+ }
+
+ PopupNotifications.show(
+ browser,
+ notificationID,
+ messageString,
+ anchorID,
+ action,
+ secondaryActions,
+ options
+ );
+ break;
+ }
+ case "addon-install-fullscreen-blocked": {
+ // AddonManager denied installation because we are in DOM fullscreen
+ this.logWarningFullScreenInstallBlocked();
+ break;
+ }
+ case "addon-install-origin-blocked": {
+ messageString = gNavigatorBundle.getFormattedString(
+ "xpinstallPromptMessage",
+ [brandShortName]
+ );
+
+ if (Services.policies) {
+ let extensionSettings = Services.policies.getExtensionSettings("*");
+ if (
+ extensionSettings &&
+ "blocked_install_message" in extensionSettings
+ ) {
+ messageString += " " + extensionSettings.blocked_install_message;
+ }
+ }
+
+ options.removeOnDismissal = true;
+ options.persistent = false;
+
+ let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
+ secHistogram.add(
+ Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
+ );
+ let popup = PopupNotifications.show(
+ browser,
+ notificationID,
+ messageString,
+ anchorID,
+ null,
+ null,
+ options
+ );
+ removeNotificationOnEnd(popup, installInfo.installs);
+ break;
+ }
+ case "addon-install-blocked": {
+ // Dismiss the progress notification. Note that this is bad if
+ // there are multiple simultaneous installs happening, see
+ // bug 1329884 for a longer explanation.
+ let progressNotification = PopupNotifications.getNotification(
+ "addon-progress",
+ browser
+ );
+ if (progressNotification) {
+ progressNotification.remove();
+ }
+
+ let hasHost = !!options.displayURI;
+ if (hasHost) {
+ messageString = gNavigatorBundle.getFormattedString(
+ "xpinstallPromptMessage.header",
+ ["<>"]
+ );
+ options.name = options.displayURI.displayHost;
+ } else {
+ messageString = gNavigatorBundle.getString(
+ "xpinstallPromptMessage.header.unknown"
+ );
+ }
+ // displayURI becomes it's own label, so we unset it for this panel. It will become part of the
+ // messageString above.
+ options.displayURI = undefined;
+
+ options.eventCallback = topic => {
+ if (topic !== "showing") {
+ return;
+ }
+ let doc = browser.ownerDocument;
+ let message = doc.getElementById("addon-install-blocked-message");
+ // We must remove any prior use of this panel message in this window.
+ while (message.firstChild) {
+ message.firstChild.remove();
+ }
+ if (hasHost) {
+ let text = gNavigatorBundle.getString(
+ "xpinstallPromptMessage.message"
+ );
+ let b = doc.createElementNS("http://www.w3.org/1999/xhtml", "b");
+ b.textContent = options.name;
+ let fragment = BrowserUtils.getLocalizedFragment(doc, text, b);
+ message.appendChild(fragment);
+ } else {
+ message.textContent = gNavigatorBundle.getString(
+ "xpinstallPromptMessage.message.unknown"
+ );
+ }
+ let learnMore = doc.getElementById("addon-install-blocked-info");
+ learnMore.textContent = gNavigatorBundle.getString(
+ "xpinstallPromptMessage.learnMore"
+ );
+ learnMore.setAttribute(
+ "href",
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "unlisted-extensions-risks"
+ );
+ };
+
+ let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
+ action = {
+ label: gNavigatorBundle.getString("xpinstallPromptMessage.install"),
+ accessKey: gNavigatorBundle.getString(
+ "xpinstallPromptMessage.install.accesskey"
+ ),
+ callback() {
+ secHistogram.add(
+ Ci.nsISecurityUITelemetry
+ .WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
+ );
+ installInfo.install();
+ },
+ };
+ let dontAllowAction = {
+ label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
+ accessKey: gNavigatorBundle.getString(
+ "xpinstallPromptMessage.dontAllow.accesskey"
+ ),
+ callback: () => {
+ for (let install of installInfo.installs) {
+ if (install.state != AddonManager.STATE_CANCELLED) {
+ install.cancel();
+ }
+ }
+ if (installInfo.cancel) {
+ installInfo.cancel();
+ }
+ },
+ };
+ let neverAllowAction = {
+ label: gNavigatorBundle.getString(
+ "xpinstallPromptMessage.neverAllow"
+ ),
+ accessKey: gNavigatorBundle.getString(
+ "xpinstallPromptMessage.neverAllow.accesskey"
+ ),
+ callback: () => {
+ SitePermissions.setForPrincipal(
+ browser.contentPrincipal,
+ "install",
+ SitePermissions.BLOCK
+ );
+ for (let install of installInfo.installs) {
+ if (install.state != AddonManager.STATE_CANCELLED) {
+ install.cancel();
+ }
+ }
+ if (installInfo.cancel) {
+ installInfo.cancel();
+ }
+ },
+ };
+
+ secHistogram.add(
+ Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
+ );
+ let popup = PopupNotifications.show(
+ browser,
+ notificationID,
+ messageString,
+ anchorID,
+ action,
+ [dontAllowAction, neverAllowAction],
+ options
+ );
+ removeNotificationOnEnd(popup, installInfo.installs);
+ break;
+ }
+ case "addon-install-started": {
+ let needsDownload = function needsDownload(aInstall) {
+ return aInstall.state != AddonManager.STATE_DOWNLOADED;
+ };
+ // If all installs have already been downloaded then there is no need to
+ // show the download progress
+ if (!installInfo.installs.some(needsDownload)) {
+ return;
+ }
+ notificationID = "addon-progress";
+ messageString = gNavigatorBundle.getString(
+ "addonDownloadingAndVerifying"
+ );
+ messageString = PluralForm.get(
+ installInfo.installs.length,
+ messageString
+ );
+ messageString = messageString.replace(
+ "#1",
+ installInfo.installs.length
+ );
+ options.installs = installInfo.installs;
+ options.contentWindow = browser.contentWindow;
+ options.sourceURI = browser.currentURI;
+ options.eventCallback = function(aEvent) {
+ switch (aEvent) {
+ case "removed":
+ options.contentWindow = null;
+ options.sourceURI = null;
+ break;
+ }
+ };
+ action = {
+ label: gNavigatorBundle.getString("addonInstall.acceptButton2.label"),
+ accessKey: gNavigatorBundle.getString(
+ "addonInstall.acceptButton2.accesskey"
+ ),
+ disabled: true,
+ callback: () => {},
+ };
+ let secondaryAction = {
+ label: gNavigatorBundle.getString("addonInstall.cancelButton.label"),
+ accessKey: gNavigatorBundle.getString(
+ "addonInstall.cancelButton.accesskey"
+ ),
+ callback: () => {
+ for (let install of installInfo.installs) {
+ if (install.state != AddonManager.STATE_CANCELLED) {
+ install.cancel();
+ }
+ }
+ },
+ };
+ let notification = PopupNotifications.show(
+ browser,
+ notificationID,
+ messageString,
+ anchorID,
+ action,
+ [secondaryAction],
+ options
+ );
+ notification._startTime = Date.now();
+
+ break;
+ }
+ case "addon-install-failed": {
+ options.removeOnDismissal = true;
+ options.persistent = false;
+
+ // TODO This isn't terribly ideal for the multiple failure case
+ for (let install of installInfo.installs) {
+ let host;
+ try {
+ host = options.displayURI.host;
+ } catch (e) {
+ // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
+ }
+
+ if (!host) {
+ host =
+ install.sourceURI instanceof Ci.nsIStandardURL &&
+ install.sourceURI.host;
+ }
+
+ let error =
+ host || install.error == 0
+ ? "addonInstallError"
+ : "addonLocalInstallError";
+ let args;
+ if (install.error < 0) {
+ error += install.error;
+ args = [brandShortName, install.name];
+ } else if (
+ install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED
+ ) {
+ error += "Blocklisted";
+ args = [install.name];
+ } else {
+ error += "Incompatible";
+ args = [brandShortName, Services.appinfo.version, install.name];
+ }
+
+ if (
+ install.addon &&
+ !Services.policies.mayInstallAddon(install.addon)
+ ) {
+ error = "addonInstallBlockedByPolicy";
+ let extensionSettings = Services.policies.getExtensionSettings(
+ install.addon.id
+ );
+ let message = "";
+ if (
+ extensionSettings &&
+ "blocked_install_message" in extensionSettings
+ ) {
+ message = " " + extensionSettings.blocked_install_message;
+ }
+ args = [install.name, install.addon.id, message];
+ }
+
+ // Add Learn More link when refusing to install an unsigned add-on
+ if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
+ options.learnMoreURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "unsigned-addons";
+ }
+
+ messageString = gNavigatorBundle.getFormattedString(error, args);
+
+ PopupNotifications.show(
+ browser,
+ notificationID,
+ messageString,
+ anchorID,
+ action,
+ null,
+ options
+ );
+
+ // Can't have multiple notifications with the same ID, so stop here.
+ break;
+ }
+ this._removeProgressNotification(browser);
+ break;
+ }
+ case "addon-install-confirmation": {
+ let showNotification = () => {
+ let height = undefined;
+
+ if (PopupNotifications.isPanelOpen) {
+ let rect = document
+ .getElementById("addon-progress-notification")
+ .getBoundingClientRect();
+ height = rect.height;
+ }
+
+ this._removeProgressNotification(browser);
+ this.showInstallConfirmation(browser, installInfo, height);
+ };
+
+ let progressNotification = PopupNotifications.getNotification(
+ "addon-progress",
+ browser
+ );
+ if (progressNotification) {
+ let downloadDuration = Date.now() - progressNotification._startTime;
+ let securityDelay =
+ Services.prefs.getIntPref("security.dialog_enable_delay") -
+ downloadDuration;
+ if (securityDelay > 0) {
+ setTimeout(() => {
+ // The download may have been cancelled during the security delay
+ if (
+ PopupNotifications.getNotification("addon-progress", browser)
+ ) {
+ showNotification();
+ }
+ }, securityDelay);
+ break;
+ }
+ }
+ showNotification();
+ break;
+ }
+ case "addon-install-complete": {
+ let secondaryActions = null;
+ let numAddons = installInfo.installs.length;
+
+ if (numAddons == 1) {
+ messageString = gNavigatorBundle.getFormattedString(
+ "addonInstalled",
+ [installInfo.installs[0].name]
+ );
+ } else {
+ messageString = gNavigatorBundle.getString("addonsGenericInstalled");
+ messageString = PluralForm.get(numAddons, messageString);
+ messageString = messageString.replace("#1", numAddons);
+ }
+ action = null;
+
+ options.removeOnDismissal = true;
+ options.persistent = false;
+
+ PopupNotifications.show(
+ browser,
+ notificationID,
+ messageString,
+ anchorID,
+ action,
+ secondaryActions,
+ options
+ );
+ break;
+ }
+ }
+ },
+ _removeProgressNotification(aBrowser) {
+ let notification = PopupNotifications.getNotification(
+ "addon-progress",
+ aBrowser
+ );
+ if (notification) {
+ notification.remove();
+ }
+ },
+};
+
+var gExtensionsNotifications = {
+ initialized: false,
+ init() {
+ this.updateAlerts();
+ this.boundUpdate = this.updateAlerts.bind(this);
+ ExtensionsUI.on("change", this.boundUpdate);
+ this.initialized = true;
+ },
+
+ uninit() {
+ // uninit() can race ahead of init() in some cases, if that happens,
+ // we have no handler to remove.
+ if (!this.initialized) {
+ return;
+ }
+ ExtensionsUI.off("change", this.boundUpdate);
+ },
+
+ _createAddonButton(text, icon, callback) {
+ let button = document.createXULElement("toolbarbutton");
+ button.setAttribute("label", text);
+ button.setAttribute("tooltiptext", text);
+ const DEFAULT_EXTENSION_ICON =
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ button.setAttribute("image", icon || DEFAULT_EXTENSION_ICON);
+ button.className = "addon-banner-item";
+
+ button.addEventListener("command", callback);
+ PanelUI.addonNotificationContainer.appendChild(button);
+ },
+
+ updateAlerts() {
+ let sideloaded = ExtensionsUI.sideloaded;
+ let updates = ExtensionsUI.updates;
+
+ let container = PanelUI.addonNotificationContainer;
+
+ while (container.firstChild) {
+ container.firstChild.remove();
+ }
+
+ let items = 0;
+ for (let update of updates) {
+ if (++items > 4) {
+ break;
+ }
+ let text = gNavigatorBundle.getFormattedString(
+ "webextPerms.updateMenuItem",
+ [update.addon.name]
+ );
+ this._createAddonButton(text, update.addon.iconURL, evt => {
+ ExtensionsUI.showUpdate(gBrowser, update);
+ });
+ }
+
+ let appName;
+ for (let addon of sideloaded) {
+ if (++items > 4) {
+ break;
+ }
+ if (!appName) {
+ let brandBundle = document.getElementById("bundle_brand");
+ appName = brandBundle.getString("brandShortName");
+ }
+
+ let text = gNavigatorBundle.getFormattedString(
+ "webextPerms.sideloadMenuItem",
+ [addon.name, appName]
+ );
+ this._createAddonButton(text, addon.iconURL, evt => {
+ // We need to hide the main menu manually because the toolbarbutton is
+ // removed immediately while processing this event, and PanelUI is
+ // unable to identify which panel should be closed automatically.
+ PanelUI.hide();
+ ExtensionsUI.showSideloaded(gBrowser, addon);
+ });
+ }
+ },
+};
+
+var BrowserAddonUI = {
+ promptRemoveExtension(addon) {
+ let { name } = addon;
+ let brand = document
+ .getElementById("bundle_brand")
+ .getString("brandShorterName");
+ let { getFormattedString, getString } = gNavigatorBundle;
+ let title = getFormattedString("webext.remove.confirmation.title", [name]);
+ let message = getFormattedString("webext.remove.confirmation.message", [
+ name,
+ brand,
+ ]);
+ let btnTitle = getString("webext.remove.confirmation.button");
+ let {
+ BUTTON_TITLE_IS_STRING: titleString,
+ BUTTON_TITLE_CANCEL: titleCancel,
+ BUTTON_POS_0,
+ BUTTON_POS_1,
+ confirmEx,
+ } = Services.prompt;
+ let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
+ let checkboxState = { value: false };
+ let checkboxMessage = null;
+
+ // Enable abuse report checkbox in the remove extension dialog,
+ // if enabled by the about:config prefs and the addon type
+ // is currently supported.
+ if (
+ gAddonAbuseReportEnabled &&
+ ["extension", "theme"].includes(addon.type)
+ ) {
+ checkboxMessage = getFormattedString(
+ "webext.remove.abuseReportCheckbox.message",
+ [document.getElementById("bundle_brand").getString("vendorShortName")]
+ );
+ }
+ const result = confirmEx(
+ null,
+ title,
+ message,
+ btnFlags,
+ btnTitle,
+ null,
+ null,
+ checkboxMessage,
+ checkboxState
+ );
+ return { remove: result === 0, report: checkboxState.value };
+ },
+
+ async reportAddon(addonId, reportEntryPoint) {
+ const win = await BrowserOpenAddonsMgr("addons://list/extension");
+
+ win.openAbuseReport({ addonId, reportEntryPoint });
+ },
+
+ async removeAddon(addonId, eventObject) {
+ let addon = addonId && (await AddonManager.getAddonByID(addonId));
+ if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
+ return;
+ }
+
+ let { remove, report } = this.promptRemoveExtension(addon);
+
+ AMTelemetry.recordActionEvent({
+ object: eventObject,
+ action: "uninstall",
+ value: remove ? "accepted" : "cancelled",
+ extra: { addonId },
+ });
+
+ if (remove) {
+ // Leave the extension in pending uninstall if we are also reporting the
+ // add-on.
+ await addon.uninstall(report);
+
+ if (report) {
+ await this.reportAddon(addon.id, "uninstall");
+ }
+ }
+ },
+};
diff --git a/browser/base/content/browser-allTabsMenu.inc.xhtml b/browser/base/content/browser-allTabsMenu.inc.xhtml
new file mode 100644
index 0000000000..7822de04c6
--- /dev/null
+++ b/browser/base/content/browser-allTabsMenu.inc.xhtml
@@ -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/. -->
+
+<html:template id="allTabsMenu-container">
+ <panelview id="allTabsMenu-allTabsView" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="allTabsMenu-undoCloseTab"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="all-tabs-menu-undo-close-tabs"
+ key="key_undoCloseTab"
+ observes="History:UndoCloseTab"/>
+ <toolbarbutton id="allTabsMenu-searchTabs"
+ class="subviewbutton subviewbutton-iconic"
+ oncommand="gTabsPanel.searchTabs();"
+ data-l10n-id="all-tabs-menu-search-tabs"/>
+ <toolbarbutton id="allTabsMenu-containerTabsButton"
+ class="subviewbutton subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('allTabsMenu-containerTabsView', this);"
+ data-l10n-id="all-tabs-menu-new-user-context"/>
+ <toolbarseparator id="allTabsMenu-hiddenTabsSeparator"/>
+ <toolbarbutton id="allTabsMenu-hiddenTabsButton"
+ class="subviewbutton subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('allTabsMenu-hiddenTabsView', this);"
+ data-l10n-id="all-tabs-menu-hidden-tabs"/>
+ <toolbarseparator id="allTabsMenu-tabsSeparator"/>
+ <vbox id="allTabsMenu-allTabsViewTabs" class="panel-subview-body"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="allTabsMenu-hiddenTabsView" class="PanelUI-subView">
+ <vbox class="panel-subview-body"/>
+ </panelview>
+
+ <panelview id="allTabsMenu-containerTabsView" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarseparator class="container-tabs-submenu-separator"/>
+ <toolbarbutton class="subviewbutton"
+ data-l10n-id="all-tabs-menu-manage-user-context"
+ command="Browser:OpenAboutContainers"/>
+ </vbox>
+ </panelview>
+</html:template>
diff --git a/browser/base/content/browser-allTabsMenu.js b/browser/base/content/browser-allTabsMenu.js
new file mode 100644
index 0000000000..12bb15bc54
--- /dev/null
+++ b/browser/base/content/browser-allTabsMenu.js
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "TabsPanel",
+ "resource:///modules/TabsList.jsm"
+);
+
+var gTabsPanel = {
+ kElements: {
+ allTabsButton: "alltabs-button",
+ allTabsView: "allTabsMenu-allTabsView",
+ allTabsViewTabs: "allTabsMenu-allTabsViewTabs",
+ containerTabsView: "allTabsMenu-containerTabsView",
+ hiddenTabsButton: "allTabsMenu-hiddenTabsButton",
+ hiddenTabsView: "allTabsMenu-hiddenTabsView",
+ },
+ _initialized: false,
+ _initializedElements: false,
+
+ initElements() {
+ if (this._initializedElements) {
+ return;
+ }
+ let template = document.getElementById("allTabsMenu-container");
+ template.replaceWith(template.content);
+
+ for (let [name, id] of Object.entries(this.kElements)) {
+ this[name] = document.getElementById(id);
+ }
+ this._initializedElements = true;
+ },
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+
+ this.initElements();
+
+ this.hiddenAudioTabsPopup = new TabsPanel({
+ view: this.allTabsView,
+ insertBefore: document.getElementById("allTabsMenu-tabsSeparator"),
+ filterFn: tab => tab.hidden && tab.soundPlaying,
+ });
+ this.allTabsPanel = new TabsPanel({
+ view: this.allTabsView,
+ containerNode: this.allTabsViewTabs,
+ filterFn: tab => !tab.pinned && !tab.hidden,
+ });
+
+ this.allTabsView.addEventListener("ViewShowing", e => {
+ PanelUI._ensureShortcutsShown(this.allTabsView);
+ document.getElementById("allTabsMenu-undoCloseTab").disabled =
+ SessionStore.getClosedTabCount(window) == 0;
+
+ let containersEnabled =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ !PrivateBrowsingUtils.isWindowPrivate(window);
+ document.getElementById(
+ "allTabsMenu-containerTabsButton"
+ ).hidden = !containersEnabled;
+
+ let hasHiddenTabs = gBrowser.visibleTabs.length < gBrowser.tabs.length;
+ document.getElementById(
+ "allTabsMenu-hiddenTabsButton"
+ ).hidden = !hasHiddenTabs;
+ document.getElementById(
+ "allTabsMenu-hiddenTabsSeparator"
+ ).hidden = !hasHiddenTabs;
+ });
+
+ this.allTabsView.addEventListener("ViewShown", e => {
+ let selectedRow = this.allTabsView.querySelector(
+ ".all-tabs-item[selected]"
+ );
+ selectedRow.scrollIntoView({ block: "center" });
+ });
+
+ let containerTabsMenuSeparator = this.containerTabsView.querySelector(
+ "toolbarseparator"
+ );
+ this.containerTabsView.addEventListener("ViewShowing", e => {
+ let elements = [];
+ let frag = document.createDocumentFragment();
+
+ ContextualIdentityService.getPublicIdentities().forEach(identity => {
+ let menuitem = document.createXULElement("toolbarbutton");
+ menuitem.setAttribute("class", "subviewbutton subviewbutton-iconic");
+ menuitem.setAttribute(
+ "label",
+ ContextualIdentityService.getUserContextLabel(identity.userContextId)
+ );
+ // The styles depend on this.
+ menuitem.setAttribute("usercontextid", identity.userContextId);
+ // The command handler depends on this.
+ menuitem.setAttribute("data-usercontextid", identity.userContextId);
+ menuitem.classList.add("identity-icon-" + identity.icon);
+ menuitem.classList.add("identity-color-" + identity.color);
+
+ menuitem.setAttribute("command", "Browser:NewUserContextTab");
+
+ frag.appendChild(menuitem);
+ elements.push(menuitem);
+ });
+
+ e.target.addEventListener(
+ "ViewHiding",
+ () => {
+ for (let element of elements) {
+ element.remove();
+ }
+ },
+ { once: true }
+ );
+ containerTabsMenuSeparator.parentNode.insertBefore(
+ frag,
+ containerTabsMenuSeparator
+ );
+ });
+
+ this.hiddenTabsPopup = new TabsPanel({
+ view: this.hiddenTabsView,
+ filterFn: tab => tab.hidden,
+ });
+
+ this._initialized = true;
+ },
+
+ get canOpen() {
+ this.initElements();
+ return isElementVisible(this.allTabsButton);
+ },
+
+ showAllTabsPanel(event) {
+ this.init();
+ if (this.canOpen) {
+ PanelUI.showSubView(
+ this.kElements.allTabsView,
+ this.allTabsButton,
+ event
+ );
+ }
+ },
+
+ hideAllTabsPanel() {
+ if (this.allTabsView) {
+ PanelMultiView.hidePopup(this.allTabsView.closest("panel"));
+ }
+ },
+
+ showHiddenTabsPanel(event) {
+ this.init();
+ if (!this.canOpen) {
+ return;
+ }
+ this.allTabsView.addEventListener(
+ "ViewShown",
+ e => {
+ PanelUI.showSubView(
+ this.kElements.hiddenTabsView,
+ this.hiddenTabsButton
+ );
+ },
+ { once: true }
+ );
+ this.showAllTabsPanel(event);
+ },
+
+ searchTabs() {
+ gURLBar.search(UrlbarTokenizer.RESTRICT.OPENPAGE, {
+ searchModeEntry: "tabmenu",
+ });
+ },
+};
diff --git a/browser/base/content/browser-captivePortal.js b/browser/base/content/browser-captivePortal.js
new file mode 100644
index 0000000000..20d3fc23c2
--- /dev/null
+++ b/browser/base/content/browser-captivePortal.js
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 CaptivePortalWatcher = {
+ // This is the value used to identify the captive portal notification.
+ PORTAL_NOTIFICATION_VALUE: "captive-portal-detected",
+
+ // This holds a weak reference to the captive portal tab so that we
+ // don't leak it if the user closes it.
+ _captivePortalTab: null,
+
+ /**
+ * If a portal is detected when we don't have focus, we first wait for focus
+ * and then add the tab if, after a recheck, the portal is still active. This
+ * is set to true while we wait so that in the unlikely event that we receive
+ * another notification while waiting, we don't do things twice.
+ */
+ _delayedCaptivePortalDetectedInProgress: false,
+
+ // In the situation above, this is set to true while we wait for the recheck.
+ // This flag exists so that tests can appropriately simulate a recheck.
+ _waitingForRecheck: false,
+
+ // This holds a weak reference to the captive portal tab so we can close the tab
+ // after successful login if we're redirected to the canonicalURL.
+ _previousCaptivePortalTab: null,
+
+ get _captivePortalNotification() {
+ return gHighPriorityNotificationBox.getNotificationWithValue(
+ this.PORTAL_NOTIFICATION_VALUE
+ );
+ },
+
+ get canonicalURL() {
+ return Services.prefs.getCharPref("captivedetect.canonicalURL");
+ },
+
+ get _browserBundle() {
+ delete this._browserBundle;
+ return (this._browserBundle = Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ ));
+ },
+
+ init() {
+ Services.obs.addObserver(this, "ensure-captive-portal-tab");
+ Services.obs.addObserver(this, "captive-portal-login");
+ Services.obs.addObserver(this, "captive-portal-login-abort");
+ Services.obs.addObserver(this, "captive-portal-login-success");
+
+ this._cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
+ Ci.nsICaptivePortalService
+ );
+
+ if (this._cps.state == this._cps.LOCKED_PORTAL) {
+ // A captive portal has already been detected.
+ this._captivePortalDetected();
+
+ // Automatically open a captive portal tab if there's no other browser window.
+ if (BrowserWindowTracker.windowCount == 1) {
+ this.ensureCaptivePortalTab();
+ }
+ } else if (this._cps.state == this._cps.UNKNOWN) {
+ // We trigger a portal check after delayed startup to avoid doing a network
+ // request before first paint.
+ this._delayedRecheckPending = true;
+ }
+
+ // This constant is chosen to be large enough for a portal recheck to complete,
+ // and small enough that the delay in opening a tab isn't too noticeable.
+ // Please see comments for _delayedCaptivePortalDetected for more details.
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "PORTAL_RECHECK_DELAY_MS",
+ "captivedetect.portalRecheckDelayMS",
+ 500
+ );
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "ensure-captive-portal-tab");
+ Services.obs.removeObserver(this, "captive-portal-login");
+ Services.obs.removeObserver(this, "captive-portal-login-abort");
+ Services.obs.removeObserver(this, "captive-portal-login-success");
+
+ this._cancelDelayedCaptivePortal();
+ },
+
+ delayedStartup() {
+ if (this._delayedRecheckPending) {
+ delete this._delayedRecheckPending;
+ this._cps.recheckCaptivePortal();
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "ensure-captive-portal-tab":
+ this.ensureCaptivePortalTab();
+ break;
+ case "captive-portal-login":
+ this._captivePortalDetected();
+ break;
+ case "captive-portal-login-abort":
+ case "captive-portal-login-success":
+ this._captivePortalGone();
+ break;
+ case "delayed-captive-portal-handled":
+ this._cancelDelayedCaptivePortal();
+ break;
+ }
+ },
+
+ onLocationChange(browser) {
+ if (!this._previousCaptivePortalTab) {
+ return;
+ }
+
+ let tab = this._previousCaptivePortalTab.get();
+ if (!tab || !tab.linkedBrowser) {
+ return;
+ }
+
+ if (browser != tab.linkedBrowser) {
+ return;
+ }
+
+ // There is a race between the release of captive portal i.e.
+ // the time when success/abort events are fired and the time when
+ // the captive portal tab redirects to the canonicalURL. We check for
+ // both conditions to be true and also check that we haven't already removed
+ // the captive portal tab in the success/abort event handlers before we remove
+ // it in the callback below. A tick is added to avoid removing the tab before
+ // onLocationChange handlers across browser code are executed.
+ Services.tm.dispatchToMainThread(() => {
+ if (!this._previousCaptivePortalTab) {
+ return;
+ }
+
+ tab = this._previousCaptivePortalTab.get();
+ let canonicalURI = Services.io.newURI(this.canonicalURL);
+ if (
+ tab &&
+ tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) &&
+ (this._cps.state == this._cps.UNLOCKED_PORTAL ||
+ this._cps.state == this._cps.UNKNOWN)
+ ) {
+ gBrowser.removeTab(tab);
+ }
+ });
+ },
+
+ _captivePortalDetected() {
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ return;
+ }
+
+ let win = BrowserWindowTracker.getTopWindow();
+
+ // Used by tests: ignore the main test window in order to enable testing of
+ // the case where we have no open windows.
+ if (win.document.documentElement.getAttribute("ignorecaptiveportal")) {
+ win = null;
+ }
+
+ // If no browser window has focus, open and show the tab when we regain focus.
+ // This is so that if a different application was focused, when the user
+ // (re-)focuses a browser window, we open the tab immediately in that window
+ // so they can log in before continuing to browse.
+ if (win != Services.focus.activeWindow) {
+ this._delayedCaptivePortalDetectedInProgress = true;
+ window.addEventListener("activate", this, { once: true });
+ Services.obs.addObserver(this, "delayed-captive-portal-handled");
+ }
+
+ this._showNotification();
+ },
+
+ /**
+ * Called after we regain focus if we detect a portal while a browser window
+ * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
+ * the tab if needed after a short delay to allow the recheck to complete.
+ */
+ _delayedCaptivePortalDetected() {
+ if (!this._delayedCaptivePortalDetectedInProgress) {
+ return;
+ }
+
+ // Used by tests: ignore the main test window in order to enable testing of
+ // the case where we have no open windows.
+ if (window.document.documentElement.getAttribute("ignorecaptiveportal")) {
+ return;
+ }
+
+ Services.obs.notifyObservers(null, "delayed-captive-portal-handled");
+
+ // Trigger a portal recheck. The user may have logged into the portal via
+ // another client, or changed networks.
+ this._cps.recheckCaptivePortal();
+ this._waitingForRecheck = true;
+ let requestTime = Date.now();
+
+ let observer = () => {
+ let time = Date.now() - requestTime;
+ Services.obs.removeObserver(observer, "captive-portal-check-complete");
+ this._waitingForRecheck = false;
+ if (this._cps.state != this._cps.LOCKED_PORTAL) {
+ // We're free of the portal!
+ return;
+ }
+
+ if (time <= this.PORTAL_RECHECK_DELAY_MS) {
+ // The amount of time elapsed since we requested a recheck (i.e. since
+ // the browser window was focused) was small enough that we can add and
+ // focus a tab with the login page with no noticeable delay.
+ this.ensureCaptivePortalTab();
+ }
+ };
+ Services.obs.addObserver(observer, "captive-portal-check-complete");
+ },
+
+ _captivePortalGone() {
+ this._cancelDelayedCaptivePortal();
+ this._removeNotification();
+
+ if (!this._captivePortalTab) {
+ return;
+ }
+
+ let tab = this._captivePortalTab.get();
+ let canonicalURI = Services.io.newURI(this.canonicalURL);
+ if (
+ tab &&
+ tab.linkedBrowser &&
+ tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)
+ ) {
+ this._previousCaptivePortalTab = null;
+ gBrowser.removeTab(tab);
+ }
+ this._captivePortalTab = null;
+ },
+
+ _cancelDelayedCaptivePortal() {
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ this._delayedCaptivePortalDetectedInProgress = false;
+ Services.obs.removeObserver(this, "delayed-captive-portal-handled");
+ window.removeEventListener("activate", this);
+ }
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "activate":
+ this._delayedCaptivePortalDetected();
+ break;
+ case "TabSelect":
+ if (!this._captivePortalTab || !this._captivePortalNotification) {
+ break;
+ }
+
+ let tab = this._captivePortalTab.get();
+ let n = this._captivePortalNotification;
+ if (!tab || !n) {
+ break;
+ }
+
+ let doc = tab.ownerDocument;
+ let button = n.querySelector("button.notification-button");
+ if (doc.defaultView.gBrowser.selectedTab == tab) {
+ button.style.visibility = "hidden";
+ } else {
+ button.style.visibility = "visible";
+ }
+ break;
+ }
+ },
+
+ _showNotification() {
+ if (this._captivePortalNotification) {
+ return;
+ }
+
+ let buttons = [
+ {
+ label: this._browserBundle.GetStringFromName(
+ "captivePortal.showLoginPage2"
+ ),
+ callback: () => {
+ this.ensureCaptivePortalTab();
+
+ // Returning true prevents the notification from closing.
+ return true;
+ },
+ },
+ ];
+
+ let message = this._browserBundle.GetStringFromName(
+ "captivePortal.infoMessage3"
+ );
+
+ let closeHandler = aEventName => {
+ if (aEventName != "removed") {
+ return;
+ }
+ gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ };
+
+ gHighPriorityNotificationBox.appendNotification(
+ message,
+ this.PORTAL_NOTIFICATION_VALUE,
+ "",
+ gHighPriorityNotificationBox.PRIORITY_INFO_MEDIUM,
+ buttons,
+ closeHandler
+ );
+
+ gBrowser.tabContainer.addEventListener("TabSelect", this);
+ },
+
+ _removeNotification() {
+ let n = this._captivePortalNotification;
+ if (!n || !n.parentNode) {
+ return;
+ }
+ n.close();
+ },
+
+ ensureCaptivePortalTab() {
+ let tab;
+ if (this._captivePortalTab) {
+ tab = this._captivePortalTab.get();
+ }
+
+ // If the tab is gone or going, we need to open a new one.
+ if (!tab || tab.closing || !tab.parentNode) {
+ tab = gBrowser.addWebTab(this.canonicalURL, {
+ ownerTab: gBrowser.selectedTab,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {
+ userContextId: gBrowser.contentPrincipal.userContextId,
+ }
+ ),
+ disableTRR: true,
+ });
+ this._captivePortalTab = Cu.getWeakReference(tab);
+ this._previousCaptivePortalTab = Cu.getWeakReference(tab);
+ }
+
+ gBrowser.selectedTab = tab;
+ },
+};
diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc
new file mode 100644
index 0000000000..2960450019
--- /dev/null
+++ b/browser/base/content/browser-context.inc
@@ -0,0 +1,396 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ <menugroup id="context-navigation">
+ <menuitem id="context-back"
+ data-l10n-id="main-context-menu-back"
+ class="menuitem-iconic"
+ command="Browser:BackOrBackDuplicate"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-forward"
+ data-l10n-id="main-context-menu-forward"
+ class="menuitem-iconic"
+ command="Browser:ForwardOrForwardDuplicate"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-reload"
+ class="menuitem-iconic"
+ tooltip="dynamic-shortcut-tooltip"
+ data-l10n-id="main-context-menu-reload"
+ command="Browser:ReloadOrDuplicate"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-stop"
+ class="menuitem-iconic"
+ tooltip="dynamic-shortcut-tooltip"
+ data-l10n-id="main-context-menu-stop"
+ command="Browser:Stop"/>
+ <menuitem id="context-bookmarkpage"
+ class="menuitem-iconic"
+ data-l10n-id="main-context-menu-bookmark-add"
+ oncommand="gContextMenu.bookmarkThisPage();"/>
+ </menugroup>
+ <menuseparator id="context-sep-navigation"/>
+ <menuseparator id="page-menu-separator"/>
+ <menuitem id="spell-no-suggestions"
+ disabled="true"
+ label="&spellNoSuggestions.label;"/>
+ <menuitem id="spell-add-to-dictionary"
+ label="&spellAddToDictionary.label;"
+ accesskey="&spellAddToDictionary.accesskey;"
+ oncommand="InlineSpellCheckerUI.addToDictionary();"/>
+ <menuitem id="spell-undo-add-to-dictionary"
+ label="&spellUndoAddToDictionary.label;"
+ accesskey="&spellUndoAddToDictionary.accesskey;"
+ oncommand="InlineSpellCheckerUI.undoAddToDictionary();" />
+ <menuseparator id="spell-suggestions-separator"/>
+ <menuitem id="context-openlinkincurrent"
+ data-l10n-id="main-context-menu-open-link"
+ oncommand="gContextMenu.openLinkInCurrent();"/>
+# label and data-usercontextid are dynamically set.
+ <menuitem id="context-openlinkincontainertab"
+ accesskey="&openLinkCmdInTab.accesskey;"
+ oncommand="gContextMenu.openLinkInTab(event);"/>
+ <menuitem id="context-openlinkintab"
+ data-l10n-id="main-context-menu-open-link-new-tab"
+ data-usercontextid="0"
+ oncommand="gContextMenu.openLinkInTab(event);"/>
+
+ <menu id="context-openlinkinusercontext-menu"
+ data-l10n-id="main-context-menu-open-link-container-tab"
+ hidden="true">
+ <menupopup oncommand="gContextMenu.openLinkInTab(event);"
+ onpopupshowing="return gContextMenu.createContainerMenu(event);" />
+ </menu>
+
+ <menuitem id="context-openlink"
+ data-l10n-id="main-context-menu-open-link-new-window"
+ oncommand="gContextMenu.openLink();"/>
+ <menuitem id="context-openlinkprivate"
+ data-l10n-id="main-context-menu-open-link-new-private-window"
+ oncommand="gContextMenu.openLinkInPrivateWindow();"/>
+ <menuseparator id="context-sep-open"/>
+ <menuitem id="context-bookmarklink"
+ data-l10n-id="main-context-menu-bookmark-this-link"
+ oncommand="gContextMenu.bookmarkLink();"/>
+ <menuitem id="context-savelink"
+ data-l10n-id="main-context-menu-save-link"
+ oncommand="gContextMenu.saveLink();"/>
+ <menuitem id="context-savelinktopocket"
+ data-l10n-id="main-context-menu-save-link-to-pocket"
+ oncommand= "Pocket.savePage(gContextMenu.browser, gContextMenu.linkURL);"/>
+ <menuitem id="context-copyemail"
+ data-l10n-id="main-context-menu-copy-email"
+ oncommand="gContextMenu.copyEmail();"/>
+ <menuitem id="context-copylink"
+ data-l10n-id="main-context-menu-copy-link"
+ oncommand="gContextMenu.copyLink();"/>
+ <menuseparator id="context-sep-copylink"/>
+ <menuitem id="context-media-play"
+ data-l10n-id="main-context-menu-media-play"
+ oncommand="gContextMenu.mediaCommand('play');"/>
+ <menuitem id="context-media-pause"
+ data-l10n-id="main-context-menu-media-pause"
+ oncommand="gContextMenu.mediaCommand('pause');"/>
+ <menuitem id="context-media-mute"
+ data-l10n-id="main-context-menu-media-mute"
+ oncommand="gContextMenu.mediaCommand('mute');"/>
+ <menuitem id="context-media-unmute"
+ data-l10n-id="main-context-menu-media-unmute"
+ oncommand="gContextMenu.mediaCommand('unmute');"/>
+ <menu id="context-media-playbackrate" data-l10n-id="main-context-menu-media-play-speed">
+ <menupopup>
+ <menuitem id="context-media-playbackrate-050x"
+ data-l10n-id="main-context-menu-media-play-speed-slow"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 0.5);"/>
+ <menuitem id="context-media-playbackrate-100x"
+ data-l10n-id="main-context-menu-media-play-speed-normal"
+ type="radio"
+ name="playbackrate"
+ checked="true"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 1.0);"/>
+ <menuitem id="context-media-playbackrate-125x"
+ data-l10n-id="main-context-menu-media-play-speed-fast"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 1.25);"/>
+ <menuitem id="context-media-playbackrate-150x"
+ data-l10n-id="main-context-menu-media-play-speed-faster"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 1.5);"/>
+ <menuitem id="context-media-playbackrate-200x"
+ data-l10n-id="main-context-menu-media-play-speed-fastest"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 2.0);"/>
+ </menupopup>
+ </menu>
+ <menuitem id="context-media-loop"
+ data-l10n-id="main-context-menu-media-loop"
+ type="checkbox"
+ oncommand="gContextMenu.mediaCommand('loop');"/>
+ <menuitem id="context-media-showcontrols"
+ data-l10n-id="main-context-menu-media-show-controls"
+ oncommand="gContextMenu.mediaCommand('showcontrols');"/>
+ <menuitem id="context-media-hidecontrols"
+ data-l10n-id="main-context-menu-media-hide-controls"
+ oncommand="gContextMenu.mediaCommand('hidecontrols');"/>
+ <menuitem id="context-video-fullscreen"
+ data-l10n-id="main-context-menu-media-video-fullscreen"
+ oncommand="gContextMenu.mediaCommand('fullscreen');"/>
+ <menuitem id="context-leave-dom-fullscreen"
+ data-l10n-id="main-context-menu-media-video-leave-fullscreen"
+ oncommand="gContextMenu.leaveDOMFullScreen();"/>
+ <menuitem id="context-video-pictureinpicture"
+ data-l10n-id="main-context-menu-media-pip"
+ type="checkbox"
+ oncommand="gContextMenu.mediaCommand('pictureinpicture');"/>
+ <menuseparator id="context-media-sep-commands"/>
+ <menuitem id="context-reloadimage"
+ data-l10n-id="main-context-menu-image-reload"
+ oncommand="gContextMenu.reloadImage();"/>
+ <menuitem id="context-viewimage"
+ data-l10n-id="main-context-menu-image-view"
+ oncommand="gContextMenu.viewMedia(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-viewvideo"
+ data-l10n-id="main-context-menu-video-view"
+ oncommand="gContextMenu.viewMedia(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+#ifdef CONTEXT_COPY_IMAGE_CONTENTS
+ <menuitem id="context-copyimage-contents"
+ data-l10n-id="main-context-menu-image-copy"
+ oncommand="goDoCommand('cmd_copyImage');"/>
+#endif
+ <menuitem id="context-copyimage"
+ data-l10n-id="main-context-menu-image-copy-location"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuitem id="context-copyvideourl"
+ data-l10n-id="main-context-menu-video-copy-location"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuitem id="context-copyaudiourl"
+ data-l10n-id="main-context-menu-audio-copy-location"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuseparator id="context-sep-copyimage"/>
+ <menuitem id="context-saveimage"
+ data-l10n-id="main-context-menu-image-save-as"
+ oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-sendimage"
+ data-l10n-id="main-context-menu-image-email"
+ oncommand="gContextMenu.sendMedia();"/>
+ <menuitem id="context-setDesktopBackground"
+ data-l10n-id="main-context-menu-image-set-as-background"
+ oncommand="gContextMenu.setDesktopBackground();"/>
+ <menuitem id="context-viewimageinfo"
+ data-l10n-id="main-context-menu-image-info"
+ oncommand="gContextMenu.viewImageInfo();"/>
+ <menuitem id="context-viewimagedesc"
+ data-l10n-id="main-context-menu-image-desc"
+ oncommand="gContextMenu.viewImageDesc(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-savevideo"
+ data-l10n-id="main-context-menu-video-save-as"
+ oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-saveaudio"
+ data-l10n-id="main-context-menu-audio-save-as"
+ oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-video-saveimage"
+ data-l10n-id="main-context-menu-video-image-save-as"
+ oncommand="gContextMenu.saveVideoFrameAsImage();"/>
+ <menuitem id="context-sendvideo"
+ data-l10n-id="main-context-menu-video-email"
+ oncommand="gContextMenu.sendMedia();"/>
+ <menuitem id="context-sendaudio"
+ data-l10n-id="main-context-menu-audio-email"
+ oncommand="gContextMenu.sendMedia();"/>
+ <menuitem id="context-ctp-play"
+ data-l10n-id="main-context-menu-plugin-play"
+ oncommand="gContextMenu.playPlugin();"/>
+ <menuitem id="context-ctp-hide"
+ data-l10n-id="main-context-menu-plugin-hide"
+ oncommand="gContextMenu.hidePlugin();"/>
+ <menuseparator id="context-sep-ctp"/>
+ <menuitem id="context-savepage"
+ data-l10n-id="main-context-menu-page-save"
+ oncommand="gContextMenu.savePageAs();"/>
+ <menuitem id="context-pocket"
+ data-l10n-id="main-context-menu-save-to-pocket"
+ oncommand="Pocket.savePage(gContextMenu.browser, gContextMenu.browser.currentURI.spec, gContextMenu.browser.contentTitle);"/>
+ <menuseparator id="context-sep-sendpagetodevice" class="sync-ui-item"
+ hidden="true"/>
+ <menu id="context-sendpagetodevice"
+ class="sync-ui-item"
+ data-l10n-id="main-context-menu-send-to-device"
+ hidden="true">
+ <menupopup id="context-sendpagetodevice-popup"
+ onpopupshowing="(() => { gSync.populateSendTabToDevicesMenu(event.target, gBrowser.currentURI.spec, gBrowser.contentTitle); })()"/>
+ </menu>
+ <menuseparator id="context-sep-viewbgimage"/>
+ <menuitem id="context-viewbgimage"
+ data-l10n-id="main-context-menu-view-background-image"
+ oncommand="gContextMenu.viewBGImage(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menu id="fill-login"
+ label="&fillLoginMenu.label;"
+ label-login="&fillLoginMenu.label;"
+ label-password="&fillPasswordMenu.label;"
+ label-username="&fillUsernameMenu.label;"
+ accesskey="&fillLoginMenu.accesskey;"
+ accesskey-login="&fillLoginMenu.accesskey;"
+ accesskey-password="&fillPasswordMenu.accesskey;"
+ accesskey-username="&fillUsernameMenu.accesskey;"
+ hidden="true">
+ <menupopup id="fill-login-popup">
+ <menuitem id="fill-login-no-logins"
+ label="&noLoginSuggestions.label;"
+ disabled="true"
+ hidden="true"/>
+ <menuseparator id="saved-logins-separator"/>
+ <menuitem id="fill-login-saved-passwords"
+ label="&viewSavedLogins.label;"
+ oncommand="gContextMenu.openPasswordManager();"/>
+ </menupopup>
+ </menu>
+ <menuitem id="fill-login-generated-password"
+ data-l10n-id="main-context-menu-generate-new-password"
+ hidden="true"
+ oncommand="gContextMenu.useGeneratedPassword();"/>
+ <menuseparator id="fill-login-and-generated-password-separator"/>
+ <menuitem id="context-undo"
+ data-l10n-id="text-action-undo"
+ command="cmd_undo"/>
+ <menuseparator id="context-sep-undo"/>
+ <menuitem id="context-cut"
+ data-l10n-id="text-action-cut"
+ command="cmd_cut"/>
+ <menuitem id="context-copy"
+ data-l10n-id="text-action-copy"
+ command="cmd_copy"/>
+ <menuitem id="context-paste"
+ data-l10n-id="text-action-paste"
+ command="cmd_paste"/>
+ <menuitem id="context-delete"
+ data-l10n-id="text-action-delete"
+ command="cmd_delete"/>
+ <menuseparator id="context-sep-paste"/>
+ <menuitem id="context-selectall"
+ data-l10n-id="text-action-select-all"
+ command="cmd_selectAll"/>
+ <menuseparator id="context-sep-selectall"/>
+ <menuitem id="context-keywordfield"
+ data-l10n-id="main-context-menu-keyword"
+ oncommand="AddKeywordForSearchField();"/>
+ <menuitem id="context-searchselect"
+ oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms, this.usePrivate, this.principal, this.csp);"/>
+ <menuitem id="context-searchselect-private"
+ oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms, true, this.principal, this.csp);"/>
+ <menuseparator id="context-sep-sendlinktodevice" class="sync-ui-item"
+ hidden="true"/>
+ <menu id="context-sendlinktodevice"
+ class="sync-ui-item"
+ data-l10n-id="main-context-menu-link-send-to-device"
+ hidden="true">
+ <menupopup id="context-sendlinktodevice-popup"
+ onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
+ </menu>
+ <menuseparator id="frame-sep"/>
+ <menu id="frame" data-l10n-id="main-context-menu-frame">
+ <menupopup>
+ <menuitem id="context-showonlythisframe"
+ data-l10n-id="main-context-menu-frame-show-this"
+ oncommand="gContextMenu.showOnlyThisFrame();"/>
+ <menuitem id="context-openframeintab"
+ data-l10n-id="main-context-menu-frame-open-tab"
+ oncommand="gContextMenu.openFrameInTab();"/>
+ <menuitem id="context-openframe"
+ data-l10n-id="main-context-menu-frame-open-window"
+ oncommand="gContextMenu.openFrame();"/>
+ <menuseparator id="open-frame-sep"/>
+ <menuitem id="context-reloadframe"
+ data-l10n-id="main-context-menu-frame-reload"
+ oncommand="gContextMenu.reloadFrame(event);"/>
+ <menuseparator/>
+ <menuitem id="context-bookmarkframe"
+ data-l10n-id="main-context-menu-frame-bookmark"
+ oncommand="gContextMenu.addBookmarkForFrame();"/>
+ <menuitem id="context-saveframe"
+ data-l10n-id="main-context-menu-frame-save-as"
+ oncommand="gContextMenu.saveFrame();"/>
+ <menuseparator/>
+ <menuitem id="context-printframe"
+ data-l10n-id="main-context-menu-frame-print"
+ oncommand="gContextMenu.printFrame();"/>
+ <menuseparator/>
+ <menuitem id="context-viewframesource"
+ data-l10n-id="main-context-menu-frame-view-source"
+ oncommand="gContextMenu.viewFrameSource();"/>
+ <menuitem id="context-viewframeinfo"
+ data-l10n-id="main-context-menu-frame-view-info"
+ oncommand="gContextMenu.viewFrameInfo();"/>
+#ifdef NIGHTLY_BUILD
+ <menuitem id="context-frameOsPid"
+ label="PID: Unknown"
+ disabled="true"/>
+#endif
+ </menupopup>
+ </menu>
+ <menuitem id="context-print-selection"
+ data-l10n-id="main-context-menu-print-selection"
+ oncommand="gContextMenu.printSelection();"/>
+ <menuitem id="context-viewpartialsource-selection"
+ data-l10n-id="main-context-menu-view-selection-source"
+ oncommand="gContextMenu.viewPartialSource();"/>
+ <menuseparator id="context-sep-viewsource"/>
+ <menuitem id="context-viewsource"
+ data-l10n-id="main-context-menu-view-page-source"
+ oncommand="BrowserViewSource(gContextMenu.browser);"/>
+ <menuitem id="context-viewinfo"
+ data-l10n-id="main-context-menu-view-page-info"
+ oncommand="gContextMenu.viewInfo();"/>
+ <menuseparator id="spell-separator"/>
+ <menuitem id="spell-check-enabled"
+ label="&spellCheckToggle.label;"
+ type="checkbox"
+ accesskey="&spellCheckToggle.accesskey;"
+ oncommand="InlineSpellCheckerUI.toggleEnabled(window);"/>
+ <menuitem id="spell-add-dictionaries-main"
+ label="&spellAddDictionaries.label;"
+ accesskey="&spellAddDictionaries.accesskey;"
+ oncommand="gContextMenu.addDictionaries();"/>
+ <menu id="spell-dictionaries"
+ label="&spellDictionaries.label;"
+ accesskey="&spellDictionaries.accesskey;">
+ <menupopup id="spell-dictionaries-menu">
+ <menuseparator id="spell-language-separator"/>
+ <menuitem id="spell-add-dictionaries"
+ label="&spellAddDictionaries.label;"
+ accesskey="&spellAddDictionaries.accesskey;"
+ oncommand="gContextMenu.addDictionaries();"/>
+ </menupopup>
+ </menu>
+ <menuseparator hidden="true" id="context-sep-bidi"/>
+ <menuitem hidden="true" id="context-bidi-text-direction-toggle"
+ data-l10n-id="main-context-menu-bidi-switch-text"
+ command="cmd_switchTextDirection"/>
+ <menuitem hidden="true" id="context-bidi-page-direction-toggle"
+ data-l10n-id="main-context-menu-bidi-switch-page"
+ oncommand="gContextMenu.switchPageDirection();"/>
+ <menuseparator id="inspect-separator" hidden="true"/>
+ <menuitem id="context-inspect-a11y"
+ hidden="true"
+ data-l10n-id="main-context-menu-inspect-a11y-properties"
+ oncommand="gContextMenu.inspectA11Y();"/>
+ <menuitem id="context-inspect"
+ hidden="true"
+ data-l10n-id="main-context-menu-inspect-element"
+ oncommand="gContextMenu.inspectNode();"/>
+ <menuseparator id="context-media-eme-separator" hidden="true"/>
+ <menuitem id="context-media-eme-learnmore"
+ class="menuitem-iconic"
+ hidden="true"
+ data-l10n-id="main-context-menu-eme-learn-more"
+ oncommand="gContextMenu.drmLearnMore(event);"
+ onclick="checkForMiddleClick(this, event);"/>
diff --git a/browser/base/content/browser-ctrlTab.js b/browser/base/content/browser-ctrlTab.js
new file mode 100644
index 0000000000..c05a474fd5
--- /dev/null
+++ b/browser/base/content/browser-ctrlTab.js
@@ -0,0 +1,684 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Tab previews utility, produces thumbnails
+ */
+var tabPreviews = {
+ get aspectRatio() {
+ let { PageThumbUtils } = ChromeUtils.import(
+ "resource://gre/modules/PageThumbUtils.jsm"
+ );
+ let [width, height] = PageThumbUtils.getThumbnailSize(window);
+ delete this.aspectRatio;
+ return (this.aspectRatio = height / width);
+ },
+
+ get: function tabPreviews_get(aTab) {
+ let uri = aTab.linkedBrowser.currentURI.spec;
+
+ if (aTab.__thumbnail_lastURI && aTab.__thumbnail_lastURI != uri) {
+ aTab.__thumbnail = null;
+ aTab.__thumbnail_lastURI = null;
+ }
+
+ if (aTab.__thumbnail) {
+ return aTab.__thumbnail;
+ }
+
+ if (aTab.getAttribute("pending") == "true") {
+ let img = new Image();
+ img.src = PageThumbs.getThumbnailURL(uri);
+ return img;
+ }
+
+ return this.capture(aTab, !aTab.hasAttribute("busy"));
+ },
+
+ capture: function tabPreviews_capture(aTab, aShouldCache) {
+ let browser = aTab.linkedBrowser;
+ let uri = browser.currentURI.spec;
+ let canvas = PageThumbs.createCanvas(window);
+ PageThumbs.shouldStoreThumbnail(browser).then(aDoStore => {
+ if (aDoStore && aShouldCache) {
+ PageThumbs.captureAndStore(browser).then(function() {
+ let img = new Image();
+ img.src = PageThumbs.getThumbnailURL(uri);
+ aTab.__thumbnail = img;
+ aTab.__thumbnail_lastURI = uri;
+ canvas.getContext("2d").drawImage(img, 0, 0);
+ });
+ } else {
+ PageThumbs.captureToCanvas(browser, canvas)
+ .then(() => {
+ if (aShouldCache) {
+ aTab.__thumbnail = canvas;
+ aTab.__thumbnail_lastURI = uri;
+ }
+ })
+ .catch(e => Cu.reportError(e));
+ }
+ });
+ return canvas;
+ },
+};
+
+var tabPreviewPanelHelper = {
+ opening(host) {
+ host.panel.hidden = false;
+
+ var handler = this._generateHandler(host);
+ host.panel.addEventListener("popupshown", handler);
+ host.panel.addEventListener("popuphiding", handler);
+
+ host._prevFocus = document.commandDispatcher.focusedElement;
+ },
+ _generateHandler(host) {
+ var self = this;
+ return function listener(event) {
+ if (event.target == host.panel) {
+ host.panel.removeEventListener(event.type, listener);
+ self["_" + event.type](host);
+ }
+ };
+ },
+ _popupshown(host) {
+ if ("setupGUI" in host) {
+ host.setupGUI();
+ }
+ },
+ _popuphiding(host) {
+ if ("suspendGUI" in host) {
+ host.suspendGUI();
+ }
+
+ if (host._prevFocus) {
+ Services.focus.setFocus(
+ host._prevFocus,
+ Ci.nsIFocusManager.FLAG_NOSCROLL
+ );
+ host._prevFocus = null;
+ } else {
+ gBrowser.selectedBrowser.focus();
+ }
+
+ if (host.tabToSelect) {
+ gBrowser.selectedTab = host.tabToSelect;
+ host.tabToSelect = null;
+ }
+ },
+};
+
+/**
+ * Ctrl-Tab panel
+ */
+var ctrlTab = {
+ maxTabPreviews: 7,
+ get panel() {
+ delete this.panel;
+ return (this.panel = document.getElementById("ctrlTab-panel"));
+ },
+ get showAllButton() {
+ delete this.showAllButton;
+ this.showAllButton = document.createXULElement("button");
+ this.showAllButton.id = "ctrlTab-showAll";
+ this.showAllButton.addEventListener("mouseover", this);
+ this.showAllButton.addEventListener("command", this);
+ this.showAllButton.addEventListener("click", this);
+ document
+ .getElementById("ctrlTab-showAll-container")
+ .appendChild(this.showAllButton);
+ return this.showAllButton;
+ },
+ get previews() {
+ delete this.previews;
+ this.previews = [];
+ let previewsContainer = document.getElementById("ctrlTab-previews");
+ for (let i = 0; i < this.maxTabPreviews; i++) {
+ let preview = this._makePreview();
+ previewsContainer.appendChild(preview);
+ this.previews.push(preview);
+ }
+ this.previews.push(this.showAllButton);
+ return this.previews;
+ },
+ get keys() {
+ var keys = {};
+ ["close", "find", "selectAll"].forEach(function(key) {
+ keys[key] = document
+ .getElementById("key_" + key)
+ .getAttribute("key")
+ .toLocaleLowerCase()
+ .charCodeAt(0);
+ });
+ delete this.keys;
+ return (this.keys = keys);
+ },
+ _selectedIndex: 0,
+ get selected() {
+ return this._selectedIndex < 0
+ ? document.activeElement
+ : this.previews[this._selectedIndex];
+ },
+ get isOpen() {
+ return (
+ this.panel.state == "open" || this.panel.state == "showing" || this._timer
+ );
+ },
+ get tabCount() {
+ return this.tabList.length;
+ },
+ get tabPreviewCount() {
+ return Math.min(this.maxTabPreviews, this.tabCount);
+ },
+
+ get tabList() {
+ return this._recentlyUsedTabs;
+ },
+
+ init: function ctrlTab_init() {
+ if (!this._recentlyUsedTabs) {
+ this._initRecentlyUsedTabs();
+ this._init(true);
+ }
+ },
+
+ uninit: function ctrlTab_uninit() {
+ if (this._recentlyUsedTabs) {
+ this._recentlyUsedTabs = null;
+ this._init(false);
+ }
+ },
+
+ prefName: "browser.ctrlTab.recentlyUsedOrder",
+ readPref: function ctrlTab_readPref() {
+ var enable =
+ Services.prefs.getBoolPref(this.prefName) &&
+ !Services.prefs.getBoolPref(
+ "browser.ctrlTab.disallowForScreenReaders",
+ false
+ );
+
+ if (enable) {
+ this.init();
+ } else {
+ this.uninit();
+ }
+ },
+ observe(aSubject, aTopic, aPrefName) {
+ this.readPref();
+ },
+
+ _makePreview() {
+ let preview = document.createXULElement("button");
+ preview.className = "ctrlTab-preview";
+ preview.setAttribute("pack", "center");
+ preview.setAttribute("flex", "1");
+ preview.addEventListener("mouseover", this);
+ preview.addEventListener("command", this);
+ preview.addEventListener("click", this);
+
+ let previewInner = document.createXULElement("vbox");
+ previewInner.className = "ctrlTab-preview-inner";
+ preview.appendChild(previewInner);
+
+ let canvas = (preview._canvas = document.createXULElement("hbox"));
+ canvas.className = "ctrlTab-canvas";
+ previewInner.appendChild(canvas);
+
+ let faviconContainer = document.createXULElement("hbox");
+ faviconContainer.className = "ctrlTab-favicon-container";
+ previewInner.appendChild(faviconContainer);
+
+ let favicon = (preview._favicon = document.createXULElement("image"));
+ favicon.className = "ctrlTab-favicon";
+ faviconContainer.appendChild(favicon);
+
+ let label = (preview._label = document.createXULElement("label"));
+ label.className = "ctrlTab-label plain";
+ label.setAttribute("crop", "end");
+ previewInner.appendChild(label);
+
+ return preview;
+ },
+
+ updatePreviews: function ctrlTab_updatePreviews() {
+ for (let i = 0; i < this.previews.length; i++) {
+ this.updatePreview(this.previews[i], this.tabList[i]);
+ }
+
+ var showAllLabel = gNavigatorBundle.getString("ctrlTab.listAllTabs.label");
+ this.showAllButton.label = PluralForm.get(
+ this.tabCount,
+ showAllLabel
+ ).replace("#1", this.tabCount);
+ this.showAllButton.hidden = !gTabsPanel.canOpen;
+ },
+
+ updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
+ if (aPreview == this.showAllButton) {
+ return;
+ }
+
+ aPreview._tab = aTab;
+
+ if (aPreview._canvas.firstElementChild) {
+ aPreview._canvas.firstElementChild.remove();
+ }
+
+ if (aTab) {
+ let canvas = aPreview._canvas;
+ let canvasWidth = this.canvasWidth;
+ let canvasHeight = this.canvasHeight;
+ canvas.setAttribute("width", canvasWidth);
+ canvas.style.minWidth = canvasWidth + "px";
+ canvas.style.maxWidth = canvasWidth + "px";
+ canvas.style.minHeight = canvasHeight + "px";
+ canvas.style.maxHeight = canvasHeight + "px";
+ canvas.appendChild(tabPreviews.get(aTab));
+
+ aPreview._label.setAttribute("value", aTab.label);
+ aPreview.setAttribute("tooltiptext", aTab.label);
+ if (aTab.image) {
+ aPreview._favicon.setAttribute("src", aTab.image);
+ } else {
+ aPreview._favicon.removeAttribute("src");
+ }
+ aPreview.hidden = false;
+ } else {
+ aPreview.hidden = true;
+ aPreview._label.removeAttribute("value");
+ aPreview.removeAttribute("tooltiptext");
+ aPreview._favicon.removeAttribute("src");
+ }
+ },
+
+ advanceFocus: function ctrlTab_advanceFocus(aForward) {
+ let selectedIndex = this.previews.indexOf(this.selected);
+ do {
+ selectedIndex += aForward ? 1 : -1;
+ if (selectedIndex < 0) {
+ selectedIndex = this.previews.length - 1;
+ } else if (selectedIndex >= this.previews.length) {
+ selectedIndex = 0;
+ }
+ } while (this.previews[selectedIndex].hidden);
+
+ if (this._selectedIndex == -1) {
+ // Focus is already in the panel.
+ this.previews[selectedIndex].focus();
+ } else {
+ this._selectedIndex = selectedIndex;
+ }
+
+ if (this.previews[selectedIndex]._tab) {
+ gBrowser.warmupTab(this.previews[selectedIndex]._tab);
+ }
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this._openPanel();
+ }
+ },
+
+ _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
+ if (this._trackMouseOver) {
+ aPreview.focus();
+ }
+ },
+
+ pick: function ctrlTab_pick(aPreview) {
+ if (!this.tabCount) {
+ return;
+ }
+
+ var select = aPreview || this.selected;
+
+ if (select == this.showAllButton) {
+ this.showAllTabs();
+ } else {
+ this.close(select._tab);
+ }
+ },
+
+ showAllTabs: function ctrlTab_showAllTabs(aPreview) {
+ this.close();
+ document.getElementById("Browser:ShowAllTabs").doCommand();
+ },
+
+ remove: function ctrlTab_remove(aPreview) {
+ if (aPreview._tab) {
+ gBrowser.removeTab(aPreview._tab);
+ }
+ },
+
+ attachTab: function ctrlTab_attachTab(aTab, aPos) {
+ if (aTab.closing) {
+ return;
+ }
+
+ if (aPos == 0) {
+ this._recentlyUsedTabs.unshift(aTab);
+ } else if (aPos) {
+ this._recentlyUsedTabs.splice(aPos, 0, aTab);
+ } else {
+ this._recentlyUsedTabs.push(aTab);
+ }
+ },
+
+ detachTab: function ctrlTab_detachTab(aTab) {
+ var i = this._recentlyUsedTabs.indexOf(aTab);
+ if (i >= 0) {
+ this._recentlyUsedTabs.splice(i, 1);
+ }
+ },
+
+ open: function ctrlTab_open() {
+ if (this.isOpen) {
+ return;
+ }
+
+ this.canvasWidth = Math.ceil(
+ (screen.availWidth * 0.85) / this.maxTabPreviews
+ );
+ this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
+ this.updatePreviews();
+ this._selectedIndex = 1;
+ gBrowser.warmupTab(this.selected._tab);
+
+ // Add a slight delay before showing the UI, so that a quick
+ // "ctrl-tab" keypress just flips back to the MRU tab.
+ this._timer = setTimeout(() => {
+ this._timer = null;
+ this._openPanel();
+ }, 200);
+ },
+
+ _openPanel: function ctrlTab_openPanel() {
+ tabPreviewPanelHelper.opening(this);
+
+ this.panel.width = Math.min(
+ screen.availWidth * 0.99,
+ this.canvasWidth * 1.25 * this.tabPreviewCount
+ );
+ var estimateHeight = this.canvasHeight * 1.25 + 75;
+ this.panel.openPopupAtScreen(
+ screen.availLeft + (screen.availWidth - this.panel.width) / 2,
+ screen.availTop + (screen.availHeight - estimateHeight) / 2,
+ false
+ );
+ },
+
+ close: function ctrlTab_close(aTabToSelect) {
+ if (!this.isOpen) {
+ return;
+ }
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this.suspendGUI();
+ if (aTabToSelect) {
+ gBrowser.selectedTab = aTabToSelect;
+ }
+ return;
+ }
+
+ this.tabToSelect = aTabToSelect;
+ this.panel.hidePopup();
+ },
+
+ setupGUI: function ctrlTab_setupGUI() {
+ this.selected.focus();
+ this._selectedIndex = -1;
+
+ // Track mouse movement after a brief delay so that the item that happens
+ // to be under the mouse pointer initially won't be selected unintentionally.
+ this._trackMouseOver = false;
+ setTimeout(
+ function(self) {
+ if (self.isOpen) {
+ self._trackMouseOver = true;
+ }
+ },
+ 0,
+ this
+ );
+ },
+
+ suspendGUI: function ctrlTab_suspendGUI() {
+ for (let preview of this.previews) {
+ this.updatePreview(preview, null);
+ }
+ },
+
+ onKeyDown(event) {
+ let action = ShortcutUtils.getSystemActionForEvent(event);
+ if (action != ShortcutUtils.CYCLE_TABS) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.isOpen) {
+ this.advanceFocus(!event.shiftKey);
+ return;
+ }
+
+ if (event.shiftKey) {
+ this.showAllTabs();
+ return;
+ }
+
+ Services.els.addSystemEventListener(document, "keyup", this, false);
+
+ let tabs = gBrowser.visibleTabs;
+ if (tabs.length > 2) {
+ this.open();
+ } else if (tabs.length == 2) {
+ let index = tabs[0].selected ? 1 : 0;
+ gBrowser.selectedTab = tabs[index];
+ }
+ },
+
+ onKeyPress(event) {
+ if (!this.isOpen || !event.ctrlKey) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (event.keyCode == event.DOM_VK_DELETE) {
+ this.remove(this.selected);
+ return;
+ }
+
+ switch (event.charCode) {
+ case this.keys.close:
+ this.remove(this.selected);
+ break;
+ case this.keys.find:
+ case this.keys.selectAll:
+ this.showAllTabs();
+ break;
+ }
+ },
+
+ removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
+ if (this.tabCount == 2) {
+ this.close();
+ return;
+ }
+
+ this.updatePreviews();
+
+ if (this.selected.hidden) {
+ this.advanceFocus(false);
+ }
+ if (this.selected == this.showAllButton) {
+ this.advanceFocus(false);
+ }
+
+ // If the current tab is removed, another tab can steal our focus.
+ if (aTab.selected && this.panel.state == "open") {
+ setTimeout(
+ function(selected) {
+ selected.focus();
+ },
+ 0,
+ this.selected
+ );
+ }
+ },
+
+ handleEvent: function ctrlTab_handleEvent(event) {
+ switch (event.type) {
+ case "SSWindowRestored":
+ this._initRecentlyUsedTabs();
+ break;
+ case "TabAttrModified":
+ // tab attribute modified (i.e. label, busy, image)
+ // update preview only if tab attribute modified in the list
+ if (
+ event.detail.changed.some((elem, ind, arr) =>
+ ["label", "busy", "image"].includes(elem)
+ )
+ ) {
+ for (let i = this.previews.length - 1; i >= 0; i--) {
+ if (
+ this.previews[i]._tab &&
+ this.previews[i]._tab == event.target
+ ) {
+ this.updatePreview(this.previews[i], event.target);
+ break;
+ }
+ }
+ }
+ break;
+ case "TabSelect":
+ this.detachTab(event.target);
+ this.attachTab(event.target, 0);
+ break;
+ case "TabOpen":
+ this.attachTab(event.target, 1);
+ break;
+ case "TabClose":
+ this.detachTab(event.target);
+ if (this.isOpen) {
+ this.removeClosingTabFromUI(event.target);
+ }
+ break;
+ case "keydown":
+ this.onKeyDown(event);
+ break;
+ case "keypress":
+ this.onKeyPress(event);
+ break;
+ case "keyup":
+ // During cycling tabs, we avoid sending keyup event to content document.
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (event.keyCode === event.DOM_VK_CONTROL) {
+ Services.els.removeSystemEventListener(
+ document,
+ "keyup",
+ this,
+ false
+ );
+
+ if (this.isOpen) {
+ this.pick();
+ }
+ }
+ break;
+ case "popupshowing":
+ if (event.target.id == "menu_viewPopup") {
+ document.getElementById(
+ "menu_showAllTabs"
+ ).hidden = !gTabsPanel.canOpen;
+ }
+ break;
+ case "mouseover":
+ this._mouseOverFocus(event.currentTarget);
+ break;
+ case "command":
+ this.pick(event.currentTarget);
+ break;
+ case "click":
+ if (event.button == 1) {
+ this.remove(event.currentTarget);
+ } else if (AppConstants.platform == "macosx" && event.button == 2) {
+ // Control+click is a right click on macOS, but in this case we want
+ // to handle it like a left click.
+ this.pick(event.currentTarget);
+ }
+ break;
+ }
+ },
+
+ filterForThumbnailExpiration(aCallback) {
+ // Save a few more thumbnails than we actually display, so that when tabs
+ // are closed, the previews we add instead still get thumbnails.
+ const extraThumbnails = 3;
+ const thumbnailCount = Math.min(
+ this.tabPreviewCount + extraThumbnails,
+ this.tabCount
+ );
+
+ let urls = [];
+ for (let i = 0; i < thumbnailCount; i++) {
+ urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
+ }
+
+ aCallback(urls);
+ },
+
+ _initRecentlyUsedTabs() {
+ this._recentlyUsedTabs = Array.prototype.filter
+ .call(gBrowser.tabs, tab => !tab.closing)
+ .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
+ },
+
+ _init: function ctrlTab__init(enable) {
+ var toggleEventListener = enable
+ ? "addEventListener"
+ : "removeEventListener";
+
+ window[toggleEventListener]("SSWindowRestored", this);
+
+ var tabContainer = gBrowser.tabContainer;
+ tabContainer[toggleEventListener]("TabOpen", this);
+ tabContainer[toggleEventListener]("TabAttrModified", this);
+ tabContainer[toggleEventListener]("TabSelect", this);
+ tabContainer[toggleEventListener]("TabClose", this);
+
+ if (enable) {
+ Services.els.addSystemEventListener(document, "keydown", this, false);
+ } else {
+ Services.els.removeSystemEventListener(document, "keydown", this, false);
+ }
+ document[toggleEventListener]("keypress", this);
+ gBrowser.tabbox.handleCtrlTab = !enable;
+
+ if (enable) {
+ PageThumbs.addExpirationFilter(this);
+ } else {
+ PageThumbs.removeExpirationFilter(this);
+ }
+
+ // If we're not running, hide the "Show All Tabs" menu item,
+ // as Shift+Ctrl+Tab will be handled by the tab bar.
+ document.getElementById("menu_showAllTabs").hidden = !enable;
+ document
+ .getElementById("menu_viewPopup")
+ [toggleEventListener]("popupshowing", this);
+ },
+};
diff --git a/browser/base/content/browser-customization.js b/browser/base/content/browser-customization.js
new file mode 100644
index 0000000000..624a98f70d
--- /dev/null
+++ b/browser/base/content/browser-customization.js
@@ -0,0 +1,181 @@
+/* -*- 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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Customization handler prepares this browser window for entering and exiting
+ * customization mode by handling customizationstarting and aftercustomization
+ * events.
+ */
+var CustomizationHandler = {
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "customizationstarting":
+ this._customizationStarting();
+ break;
+ case "aftercustomization":
+ this._afterCustomization();
+ break;
+ }
+ },
+
+ isCustomizing() {
+ return document.documentElement.hasAttribute("customizing");
+ },
+
+ _customizationStarting() {
+ // Disable the toolbar context menu items
+ let menubar = document.getElementById("main-menubar");
+ for (let childNode of menubar.children) {
+ childNode.setAttribute("disabled", true);
+ }
+
+ UpdateUrlbarSearchSplitterState();
+
+ PlacesToolbarHelper.customizeStart();
+ },
+
+ _afterCustomization() {
+ // Update global UI elements that may have been added or removed
+ if (AppConstants.platform != "macosx") {
+ updateEditUIVisibility();
+ }
+
+ PlacesToolbarHelper.customizeDone();
+
+ XULBrowserWindow.asyncUpdateUI();
+ // Re-enable parts of the UI we disabled during the dialog
+ let menubar = document.getElementById("main-menubar");
+ for (let childNode of menubar.children) {
+ childNode.setAttribute("disabled", false);
+ }
+
+ gBrowser.selectedBrowser.focus();
+
+ // Update the urlbar
+ gURLBar.setURI();
+ UpdateUrlbarSearchSplitterState();
+ },
+};
+
+var AutoHideMenubar = {
+ get _node() {
+ delete this._node;
+ return (this._node = document.getElementById("toolbar-menubar"));
+ },
+
+ _contextMenuListener: {
+ contextMenu: null,
+
+ get active() {
+ return !!this.contextMenu;
+ },
+
+ init(event) {
+ // Ignore mousedowns in <menupopup>s.
+ if (event.target.closest("menupopup")) {
+ return;
+ }
+
+ let contextMenuId = AutoHideMenubar._node.getAttribute("context");
+ this.contextMenu = document.getElementById(contextMenuId);
+ this.contextMenu.addEventListener("popupshown", this);
+ this.contextMenu.addEventListener("popuphiding", this);
+ AutoHideMenubar._node.addEventListener("mousemove", this);
+ },
+ handleEvent(event) {
+ switch (event.type) {
+ case "popupshown":
+ AutoHideMenubar._node.removeEventListener("mousemove", this);
+ break;
+ case "popuphiding":
+ case "mousemove":
+ AutoHideMenubar._setInactiveAsync();
+ AutoHideMenubar._node.removeEventListener("mousemove", this);
+ this.contextMenu.removeEventListener("popuphiding", this);
+ this.contextMenu.removeEventListener("popupshown", this);
+ this.contextMenu = null;
+ break;
+ }
+ },
+ },
+
+ init() {
+ this._node.addEventListener("toolbarvisibilitychange", this);
+ if (this._node.getAttribute("autohide") == "true") {
+ this._enable();
+ }
+ },
+
+ _updateState() {
+ if (this._node.getAttribute("autohide") == "true") {
+ this._enable();
+ } else {
+ this._disable();
+ }
+ },
+
+ _events: [
+ "DOMMenuBarInactive",
+ "DOMMenuBarActive",
+ "popupshowing",
+ "mousedown",
+ ],
+ _enable() {
+ this._node.setAttribute("inactive", "true");
+ for (let event of this._events) {
+ this._node.addEventListener(event, this);
+ }
+ },
+
+ _disable() {
+ this._setActive();
+ for (let event of this._events) {
+ this._node.removeEventListener(event, this);
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "toolbarvisibilitychange":
+ this._updateState();
+ break;
+ case "popupshowing":
+ // fall through
+ case "DOMMenuBarActive":
+ this._setActive();
+ break;
+ case "mousedown":
+ if (event.button == 2) {
+ this._contextMenuListener.init(event);
+ }
+ break;
+ case "DOMMenuBarInactive":
+ if (!this._contextMenuListener.active) {
+ this._setInactiveAsync();
+ }
+ break;
+ }
+ },
+
+ _setInactiveAsync() {
+ this._inactiveTimeout = setTimeout(() => {
+ if (this._node.getAttribute("autohide") == "true") {
+ this._inactiveTimeout = null;
+ this._node.setAttribute("inactive", "true");
+ }
+ }, 0);
+ },
+
+ _setActive() {
+ if (this._inactiveTimeout) {
+ clearTimeout(this._inactiveTimeout);
+ this._inactiveTimeout = null;
+ }
+ this._node.removeAttribute("inactive");
+ },
+};
diff --git a/browser/base/content/browser-data-submission-info-bar.js b/browser/base/content/browser-data-submission-info-bar.js
new file mode 100644
index 0000000000..e94a0ef546
--- /dev/null
+++ b/browser/base/content/browser-data-submission-info-bar.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Represents an info bar that shows a data submission notification.
+ */
+var gDataNotificationInfoBar = {
+ _OBSERVERS: [
+ "datareporting:notify-data-policy:request",
+ "datareporting:notify-data-policy:close",
+ ],
+
+ _DATA_REPORTING_NOTIFICATION: "data-reporting",
+
+ get _log() {
+ let { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+ delete this._log;
+ return (this._log = Log.repository.getLoggerWithMessagePrefix(
+ "Toolkit.Telemetry",
+ "DataNotificationInfoBar::"
+ ));
+ },
+
+ init() {
+ window.addEventListener("unload", () => {
+ for (let o of this._OBSERVERS) {
+ Services.obs.removeObserver(this, o);
+ }
+ });
+
+ for (let o of this._OBSERVERS) {
+ Services.obs.addObserver(this, o, true);
+ }
+ },
+
+ _getDataReportingNotification(name = this._DATA_REPORTING_NOTIFICATION) {
+ return gNotificationBox.getNotificationWithValue(name);
+ },
+
+ _displayDataPolicyInfoBar(request) {
+ if (this._getDataReportingNotification()) {
+ return;
+ }
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let appName = brandBundle.getString("brandShortName");
+ let vendorName = brandBundle.getString("vendorShortName");
+
+ let message = gNavigatorBundle.getFormattedString(
+ "dataReportingNotification.message",
+ [appName, vendorName]
+ );
+
+ this._actionTaken = false;
+
+ let buttons = [
+ {
+ label: gNavigatorBundle.getString(
+ "dataReportingNotification.button.label"
+ ),
+ accessKey: gNavigatorBundle.getString(
+ "dataReportingNotification.button.accessKey"
+ ),
+ popup: null,
+ callback: () => {
+ this._actionTaken = true;
+ window.openPreferences("privacy-reports");
+ },
+ },
+ ];
+
+ this._log.info("Creating data reporting policy notification.");
+ gNotificationBox.appendNotification(
+ message,
+ this._DATA_REPORTING_NOTIFICATION,
+ null,
+ gNotificationBox.PRIORITY_INFO_HIGH,
+ buttons,
+ event => {
+ if (event == "removed") {
+ Services.obs.notifyObservers(
+ null,
+ "datareporting:notify-data-policy:close"
+ );
+ }
+ }
+ );
+ // It is important to defer calling onUserNotifyComplete() until we're
+ // actually sure the notification was displayed. If we ever called
+ // onUserNotifyComplete() without showing anything to the user, that
+ // would be very good for user choice. It may also have legal impact.
+ request.onUserNotifyComplete();
+ },
+
+ _clearPolicyNotification() {
+ let notification = this._getDataReportingNotification();
+ if (notification) {
+ this._log.debug("Closing notification.");
+ notification.close();
+ }
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "datareporting:notify-data-policy:request":
+ let request = subject.wrappedJSObject.object;
+ try {
+ this._displayDataPolicyInfoBar(request);
+ } catch (ex) {
+ request.onUserNotifyFailed(ex);
+ }
+ break;
+
+ case "datareporting:notify-data-policy:close":
+ // If this observer fires, it means something else took care of
+ // responding. Therefore, we don't need to do anything. So, we
+ // act like we took action and clear state.
+ this._actionTaken = true;
+ this._clearPolicyNotification();
+ break;
+
+ default:
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+};
diff --git a/browser/base/content/browser-development-helpers.js b/browser/base/content/browser-development-helpers.js
new file mode 100644
index 0000000000..6ed119b015
--- /dev/null
+++ b/browser/base/content/browser-development-helpers.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Extra features for local development. This file isn't loaded in
+ * non-local builds.
+ */
+
+var DevelopmentHelpers = {
+ init() {
+ this.quickRestart = this.quickRestart.bind(this);
+ this.addRestartShortcut();
+ },
+
+ quickRestart() {
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ env.set("MOZ_DISABLE_SAFE_MODE_KEY", "1");
+
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ },
+
+ addRestartShortcut() {
+ let command = document.createXULElement("command");
+ command.setAttribute("id", "cmd_quickRestart");
+ command.addEventListener("command", this.quickRestart, true);
+ command.setAttribute("oncommand", "void 0;"); // Needed - bug 371900
+ document.getElementById("mainCommandSet").prepend(command);
+
+ let key = document.createXULElement("key");
+ key.setAttribute("id", "key_quickRestart");
+ key.setAttribute("key", "r");
+ key.setAttribute("modifiers", "accel,alt");
+ key.setAttribute("command", "cmd_quickRestart");
+ key.setAttribute("oncommand", "void 0;"); // Needed - bug 371900
+ document.getElementById("mainKeyset").prepend(key);
+
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("id", "menu_FileRestartItem");
+ menuitem.setAttribute("key", "key_quickRestart");
+ menuitem.setAttribute("label", "Restart (Developer)");
+ menuitem.addEventListener("command", this.quickRestart, true);
+ document.getElementById("menu_FilePopup").appendChild(menuitem);
+ },
+};
diff --git a/browser/base/content/browser-doctype.inc b/browser/base/content/browser-doctype.inc
new file mode 100644
index 0000000000..db2242afac
--- /dev/null
+++ b/browser/base/content/browser-doctype.inc
@@ -0,0 +1,14 @@
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" >
+%browserDTD;
+<!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" >
+%charsetDTD;
+<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" >
+%textcontextDTD;
+<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd">
+%placesDTD;
+<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+%syncBrandDTD;
+<!ENTITY % brandingsDTD SYSTEM "chrome://browser/locale/brandings.dtd">
+%brandingsDTD;
diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js
new file mode 100644
index 0000000000..8cba66cfa9
--- /dev/null
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -0,0 +1,861 @@
+/* -*- 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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+var PointerlockFsWarning = {
+ _element: null,
+ _origin: null,
+
+ /**
+ * Timeout object for managing timeout request. If it is started when
+ * the previous call hasn't finished, it would automatically cancelled
+ * the previous one.
+ */
+ Timeout: class {
+ constructor(func, delay) {
+ this._id = 0;
+ this._func = func;
+ this._delay = delay;
+ }
+ start() {
+ this.cancel();
+ this._id = setTimeout(() => this._handle(), this._delay);
+ }
+ cancel() {
+ if (this._id) {
+ clearTimeout(this._id);
+ this._id = 0;
+ }
+ }
+ _handle() {
+ this._id = 0;
+ this._func();
+ }
+ get delay() {
+ return this._delay;
+ }
+ },
+
+ showPointerLock(aOrigin) {
+ if (!document.fullscreen) {
+ let timeout = Services.prefs.getIntPref(
+ "pointer-lock-api.warning.timeout"
+ );
+ this.show(aOrigin, "pointerlock-warning", timeout, 0);
+ }
+ },
+
+ showFullScreen(aOrigin) {
+ let timeout = Services.prefs.getIntPref("full-screen-api.warning.timeout");
+ let delay = Services.prefs.getIntPref("full-screen-api.warning.delay");
+ this.show(aOrigin, "fullscreen-warning", timeout, delay);
+ },
+
+ // Shows a warning that the site has entered fullscreen or
+ // pointer lock for a short duration.
+ show(aOrigin, elementId, timeout, delay) {
+ if (!this._element) {
+ this._element = document.getElementById(elementId);
+ // Setup event listeners
+ this._element.addEventListener("transitionend", this);
+ window.addEventListener("mousemove", this, true);
+ // The timeout to hide the warning box after a while.
+ this._timeoutHide = new this.Timeout(() => {
+ this._state = "hidden";
+ }, timeout);
+ // The timeout to show the warning box when the pointer is at the top
+ this._timeoutShow = new this.Timeout(() => {
+ this._state = "ontop";
+ this._timeoutHide.start();
+ }, delay);
+ }
+
+ // Set the strings on the warning UI.
+ if (aOrigin) {
+ this._origin = aOrigin;
+ }
+ let uri = Services.io.newURI(this._origin);
+ let host = null;
+ // Make an exception for PDF.js - we'll show "This document" instead.
+ if (this._origin != "resource://pdf.js") {
+ try {
+ host = uri.host;
+ } catch (e) {}
+ }
+ let textElem = this._element.querySelector(
+ ".pointerlockfswarning-domain-text"
+ );
+ if (!host) {
+ textElem.setAttribute("hidden", true);
+ } else {
+ textElem.removeAttribute("hidden");
+ // Document's principal's URI has a host. Display a warning including it.
+ let utils = {};
+ ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", utils);
+ let displayHost = utils.DownloadUtils.getURIHost(uri.spec)[0];
+ let l10nString = {
+ "fullscreen-warning": "fullscreen-warning-domain",
+ "pointerlock-warning": "pointerlock-warning-domain",
+ }[elementId];
+ document.l10n.setAttributes(textElem, l10nString, {
+ domain: displayHost,
+ });
+ }
+
+ this._element.dataset.identity =
+ gIdentityHandler.pointerlockFsWarningClassName;
+
+ // User should be allowed to explicitly disable
+ // the prompt if they really want.
+ if (this._timeoutHide.delay <= 0) {
+ return;
+ }
+
+ // Explicitly set the last state to hidden to avoid the warning
+ // box being hidden immediately because of mousemove.
+ this._state = "onscreen";
+ this._lastState = "hidden";
+ this._timeoutHide.start();
+ },
+
+ close() {
+ if (!this._element) {
+ return;
+ }
+ // Cancel any pending timeout
+ this._timeoutHide.cancel();
+ this._timeoutShow.cancel();
+ // Reset state of the warning box
+ this._state = "hidden";
+ // Reset state of the text so we don't persist or retranslate it.
+ this._element
+ .querySelector(".pointerlockfswarning-domain-text")
+ .removeAttribute("data-l10n-id");
+ this._element.setAttribute("hidden", true);
+ // Remove all event listeners
+ this._element.removeEventListener("transitionend", this);
+ window.removeEventListener("mousemove", this, true);
+ // Clear fields
+ this._element = null;
+ this._timeoutHide = null;
+ this._timeoutShow = null;
+
+ // Ensure focus switches away from the (now hidden) warning box.
+ // If the user clicked buttons in the warning box, it would have
+ // been focused, and any key events would be directed at the (now
+ // hidden) chrome document instead of the target document.
+ gBrowser.selectedBrowser.focus();
+ },
+
+ // State could be one of "onscreen", "ontop", "hiding", and
+ // "hidden". Setting the state to "onscreen" and "ontop" takes
+ // effect immediately, while setting it to "hidden" actually
+ // turns the state to "hiding" before the transition finishes.
+ _lastState: null,
+ _STATES: ["hidden", "ontop", "onscreen"],
+ get _state() {
+ for (let state of this._STATES) {
+ if (this._element.hasAttribute(state)) {
+ return state;
+ }
+ }
+ return "hiding";
+ },
+ set _state(newState) {
+ let currentState = this._state;
+ if (currentState == newState) {
+ return;
+ }
+ if (currentState != "hiding") {
+ this._lastState = currentState;
+ this._element.removeAttribute(currentState);
+ }
+ if (newState != "hidden") {
+ if (currentState != "hidden") {
+ this._element.setAttribute(newState, true);
+ } else {
+ // When the previous state is hidden, the display was none,
+ // thus no box was constructed. We need to wait for the new
+ // display value taking effect first, otherwise, there won't
+ // be any transition. Since requestAnimationFrame callback is
+ // generally triggered before any style flush and layout, we
+ // should wait for the second animation frame.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ if (this._element) {
+ this._element.setAttribute(newState, true);
+ }
+ });
+ });
+ }
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "mousemove": {
+ let state = this._state;
+ if (state == "hidden") {
+ // If the warning box is currently hidden, show it after
+ // a short delay if the pointer is at the top.
+ if (event.clientY != 0) {
+ this._timeoutShow.cancel();
+ } else if (this._timeoutShow.delay >= 0) {
+ this._timeoutShow.start();
+ }
+ } else {
+ let elemRect = this._element.getBoundingClientRect();
+ if (state == "hiding" && this._lastState != "hidden") {
+ // If we are on the hiding transition, and the pointer
+ // moved near the box, restore to the previous state.
+ if (event.clientY <= elemRect.bottom + 50) {
+ this._state = this._lastState;
+ this._timeoutHide.start();
+ }
+ } else if (state == "ontop" || this._lastState != "hidden") {
+ // State being "ontop" or the previous state not being
+ // "hidden" indicates this current warning box is shown
+ // in response to user's action. Hide it immediately when
+ // the pointer leaves that area.
+ if (event.clientY > elemRect.bottom + 50) {
+ this._state = "hidden";
+ this._timeoutHide.cancel();
+ }
+ }
+ }
+ break;
+ }
+ case "transitionend": {
+ if (this._state == "hiding") {
+ this._element.setAttribute("hidden", true);
+ }
+ break;
+ }
+ }
+ },
+};
+
+var PointerLock = {
+ entered(originNoSuffix) {
+ PointerlockFsWarning.showPointerLock(originNoSuffix);
+ },
+
+ exited() {
+ PointerlockFsWarning.close();
+ },
+};
+
+var FullScreen = {
+ init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "permissionsFullScreenAllowed",
+ "permissions.fullscreen.allowed"
+ );
+
+ // Called when the Firefox window go into fullscreen.
+ addEventListener("fullscreen", this, true);
+
+ // Called only when fullscreen is requested
+ // by the parent (eg: via the browser-menu).
+ // Should not be called when the request comes from
+ // the content.
+ addEventListener("willenterfullscreen", this, true);
+ addEventListener("willexitfullscreen", this, true);
+
+ if (window.fullScreen) {
+ this.toggle();
+ }
+ },
+
+ uninit() {
+ this.cleanup();
+ },
+
+ willToggle(aWillEnterFullscreen) {
+ if (aWillEnterFullscreen) {
+ document.documentElement.setAttribute("inFullscreen", true);
+ } else {
+ document.documentElement.removeAttribute("inFullscreen");
+ }
+ },
+
+ toggle() {
+ var enterFS = window.fullScreen;
+
+ // Toggle the View:FullScreen command, which controls elements like the
+ // fullscreen menuitem, and menubars.
+ let fullscreenCommand = document.getElementById("View:FullScreen");
+ if (enterFS) {
+ fullscreenCommand.setAttribute("checked", enterFS);
+ } else {
+ fullscreenCommand.removeAttribute("checked");
+ }
+
+ if (AppConstants.platform == "macosx") {
+ // Make sure the menu items are adjusted.
+ document.getElementById("enterFullScreenItem").hidden = enterFS;
+ document.getElementById("exitFullScreenItem").hidden = !enterFS;
+ }
+
+ if (!this._fullScrToggler) {
+ this._fullScrToggler = document.getElementById("fullscr-toggler");
+ this._fullScrToggler.addEventListener("mouseover", this._expandCallback);
+ this._fullScrToggler.addEventListener("dragenter", this._expandCallback);
+ this._fullScrToggler.addEventListener("touchmove", this._expandCallback, {
+ passive: true,
+ });
+ }
+
+ if (enterFS) {
+ gNavToolbox.setAttribute("inFullscreen", true);
+ document.documentElement.setAttribute("inFullscreen", true);
+ let alwaysUsesNativeFullscreen =
+ AppConstants.platform == "macosx" &&
+ Services.prefs.getBoolPref("full-screen-api.macos-native-full-screen");
+ if (
+ (alwaysUsesNativeFullscreen || !document.fullscreenElement) &&
+ AppConstants.platform == "macosx"
+ ) {
+ document.documentElement.setAttribute("OSXLionFullscreen", true);
+ }
+ } else {
+ gNavToolbox.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("OSXLionFullscreen");
+ }
+
+ if (!document.fullscreenElement) {
+ this._updateToolbars(enterFS);
+ }
+
+ if (enterFS) {
+ document.addEventListener("keypress", this._keyToggleCallback);
+ document.addEventListener("popupshown", this._setPopupOpen);
+ document.addEventListener("popuphidden", this._setPopupOpen);
+ gURLBar.controller.addQueryListener(this);
+
+ // In DOM fullscreen mode, we hide toolbars with CSS
+ if (!document.fullscreenElement) {
+ this.hideNavToolbox(true);
+ }
+ } else {
+ this.showNavToolbox(false);
+ // This is needed if they use the context menu to quit fullscreen
+ this._isPopupOpen = false;
+ this.cleanup();
+ }
+ },
+
+ exitDomFullScreen() {
+ if (document.fullscreen) {
+ document.exitFullscreen();
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "willenterfullscreen":
+ this.willToggle(true);
+ break;
+ case "willexitfullscreen":
+ this.willToggle(false);
+ break;
+ case "fullscreen":
+ this.toggle();
+ break;
+ }
+ },
+
+ _logWarningPermissionPromptFS(actionStringKey) {
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ let message = gBrowserBundle.GetStringFromName(
+ `permissions.fullscreen.${actionStringKey}`
+ );
+ consoleMsg.initWithWindowID(
+ message,
+ gBrowser.currentURI.spec,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.warningFlag,
+ "FullScreen",
+ gBrowser.selectedBrowser.innerWindowID
+ );
+ Services.console.logMessage(consoleMsg);
+ },
+
+ _handlePermPromptShow() {
+ if (
+ !FullScreen.permissionsFullScreenAllowed &&
+ window.fullScreen &&
+ PopupNotifications.getNotification(
+ this._permissionNotificationIDs
+ ).filter(n => !n.dismissed).length
+ ) {
+ this.exitDomFullScreen();
+ this._logWarningPermissionPromptFS("fullScreenCanceled");
+ }
+ },
+
+ enterDomFullscreen(aBrowser, aActor) {
+ if (!document.fullscreenElement) {
+ aActor.requestOrigin = null;
+ return;
+ }
+
+ // If we have a current pointerlock warning shown then hide it
+ // before transition.
+ PointerlockFsWarning.close();
+
+ // If it is a remote browser, send a message to ask the content
+ // to enter fullscreen state. We don't need to do so if it is an
+ // in-process browser, since all related document should have
+ // entered fullscreen state at this point.
+ // Additionally, in Fission world, we may need to notify the
+ // frames in the middle (content frames that embbed the oop iframe where
+ // the element requesting fullscreen lives) to enter fullscreen
+ // first.
+ // This should be done before the active tab check below to ensure
+ // that the content document handles the pending request. Doing so
+ // before the check is fine since we also check the activeness of
+ // the requesting document in content-side handling code.
+ if (this._isRemoteBrowser(aBrowser)) {
+ let [targetActor, inProcessBC] = this._getNextMsgRecipientActor(aActor);
+ if (!targetActor) {
+ // If there is no appropriate actor to send the message we have
+ // no way to complete the transition and should abort by exiting
+ // fullscreen.
+ this._abortEnterFullscreen();
+ return;
+ }
+ targetActor.sendAsyncMessage("DOMFullscreen:Entered", {
+ remoteFrameBC: inProcessBC,
+ });
+
+ // Record that the actor is waiting for its child to enter
+ // fullscreen so that if it dies we can abort.
+ targetActor.waitingForChildFullscreen = true;
+ if (inProcessBC) {
+ // We aren't messaging the request origin yet, skip this time.
+ return;
+ }
+ }
+
+ // If we've received a fullscreen notification, we have to ensure that the
+ // element that's requesting fullscreen belongs to the browser that's currently
+ // active. If not, we exit fullscreen since the "full-screen document" isn't
+ // actually visible now.
+ if (
+ !aBrowser ||
+ gBrowser.selectedBrowser != aBrowser ||
+ // The top-level window has lost focus since the request to enter
+ // full-screen was made. Cancel full-screen.
+ Services.focus.activeWindow != window
+ ) {
+ this._abortEnterFullscreen();
+ return;
+ }
+
+ // Remove permission prompts when entering full-screen.
+ if (!FullScreen.permissionsFullScreenAllowed) {
+ let notifications = PopupNotifications.getNotification(
+ this._permissionNotificationIDs
+ ).filter(n => !n.dismissed);
+ PopupNotifications.remove(notifications, true);
+ if (notifications.length) {
+ this._logWarningPermissionPromptFS("promptCanceled");
+ }
+ }
+ document.documentElement.setAttribute("inDOMFullscreen", true);
+
+ if (gFindBarInitialized) {
+ gFindBar.close(true);
+ }
+
+ // Exit DOM full-screen mode when switching to a different tab.
+ gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen);
+
+ // Addon installation should be cancelled when entering DOM fullscreen for security and usability reasons.
+ // Installation prompts in fullscreen can trick the user into installing unwanted addons.
+ // In fullscreen the notification box does not have a clear visual association with its parent anymore.
+ if (gXPInstallObserver.removeAllNotifications(aBrowser)) {
+ // If notifications have been removed, log a warning to the website console
+ gXPInstallObserver.logWarningFullScreenInstallBlocked();
+ }
+
+ PopupNotifications.panel.addEventListener(
+ "popupshowing",
+ () => this._handlePermPromptShow(),
+ true
+ );
+ },
+
+ cleanup() {
+ if (!window.fullScreen) {
+ MousePosTracker.removeListener(this);
+ document.removeEventListener("keypress", this._keyToggleCallback);
+ document.removeEventListener("popupshown", this._setPopupOpen);
+ document.removeEventListener("popuphidden", this._setPopupOpen);
+ gURLBar.controller.removeQueryListener(this);
+ }
+ },
+
+ /**
+ * Clean up full screen, starting from the request origin's first ancestor
+ * frame that is OOP.
+ *
+ * If there are OOP ancestor frames, we notify the first of those and then bail to
+ * be called again in that process when it has dealt with the change. This is
+ * repeated until all ancestor processes have been updated. Once that has happened
+ * we remove our handlers and attributes and notify the request origin to complete
+ * the cleanup.
+ */
+ cleanupDomFullscreen(aActor) {
+ let [target, inProcessBC] = this._getNextMsgRecipientActor(aActor);
+ if (target) {
+ target.sendAsyncMessage("DOMFullscreen:CleanUp", {
+ remoteFrameBC: inProcessBC,
+ });
+ if (inProcessBC) {
+ return;
+ }
+ }
+
+ PopupNotifications.panel.removeEventListener(
+ "popupshowing",
+ () => this._handlePermPromptShow(),
+ true
+ );
+
+ PointerlockFsWarning.close();
+ gBrowser.tabContainer.removeEventListener(
+ "TabSelect",
+ this.exitDomFullScreen
+ );
+
+ document.documentElement.removeAttribute("inDOMFullscreen");
+ },
+
+ _abortEnterFullscreen() {
+ // This function is called synchronously in fullscreen change, so
+ // we have to avoid calling exitFullscreen synchronously here.
+ setTimeout(() => document.exitFullscreen(), 0);
+ if (TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS")) {
+ // Cancel the stopwatch for any fullscreen change to avoid
+ // errors if it is started again.
+ TelemetryStopwatch.cancel("FULLSCREEN_CHANGE_MS");
+ }
+ },
+
+ /**
+ * Search for the first ancestor of aActor that lives in a different process.
+ * If found, that ancestor actor and the browsing context for its child which
+ * was in process are returned. Otherwise [request origin, null].
+ *
+ *
+ * @param {JSWindowActorParent} aActor
+ * The actor that called this function.
+ *
+ * @return {[JSWindowActorParent, BrowsingContext]}
+ * The parent actor which should be sent the next msg and the
+ * in process browsing context which is its child. Will be
+ * [null, null] if there is no OOP parent actor and request origin
+ * is unset. [null, null] is also returned if the intended actor or
+ * the calling actor has been destroyed.
+ */
+ _getNextMsgRecipientActor(aActor) {
+ if (aActor.hasBeenDestroyed()) {
+ return [null, null];
+ }
+
+ let childBC = aActor.browsingContext;
+ let parentBC = childBC.parent;
+
+ // Walk up the browsing context tree from aActor's browsing context
+ // to find the first ancestor browsing context that's in a different process.
+ while (parentBC) {
+ if (!childBC.currentWindowGlobal || !parentBC.currentWindowGlobal) {
+ break;
+ }
+ let childPid = childBC.currentWindowGlobal.osPid;
+ let parentPid = parentBC.currentWindowGlobal.osPid;
+
+ if (childPid == parentPid) {
+ childBC = parentBC;
+ parentBC = childBC.parent;
+ } else {
+ break;
+ }
+ }
+
+ let target = null;
+ let inProcessBC = null;
+
+ if (parentBC && parentBC.currentWindowGlobal) {
+ target = parentBC.currentWindowGlobal.getActor("DOMFullscreen");
+ inProcessBC = childBC;
+ } else {
+ target = aActor.requestOrigin;
+ }
+
+ if (!target || target.hasBeenDestroyed()) {
+ return [null, null];
+ }
+ return [target, inProcessBC];
+ },
+
+ _isRemoteBrowser(aBrowser) {
+ return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true";
+ },
+
+ getMouseTargetRect() {
+ return this._mouseTargetRect;
+ },
+
+ // Event callbacks
+ _expandCallback() {
+ FullScreen.showNavToolbox();
+ },
+
+ onMouseEnter() {
+ this.hideNavToolbox();
+ },
+
+ _keyToggleCallback(aEvent) {
+ // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we
+ // should provide a way to collapse them too.
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ FullScreen.hideNavToolbox();
+ } else if (aEvent.keyCode == aEvent.DOM_VK_F6) {
+ // F6 is another shortcut to the address bar, but its not covered in OpenLocation()
+ FullScreen.showNavToolbox();
+ }
+ },
+
+ // Checks whether we are allowed to collapse the chrome
+ _isPopupOpen: false,
+ _isChromeCollapsed: false,
+
+ _setPopupOpen(aEvent) {
+ // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
+ // Otherwise, they would not affect chrome and the user would expect the chrome to go away.
+ // e.g. we wouldn't want the autoscroll icon firing this event, so when the user
+ // toggles chrome when moving mouse to the top, it doesn't go away again.
+ let target = aEvent.originalTarget;
+ if (target.localName == "tooltip") {
+ return;
+ }
+ if (
+ aEvent.type == "popupshown" &&
+ !FullScreen._isChromeCollapsed &&
+ target.getAttribute("nopreventnavboxhide") != "true"
+ ) {
+ FullScreen._isPopupOpen = true;
+ } else if (aEvent.type == "popuphidden") {
+ FullScreen._isPopupOpen = false;
+ // Try again to hide toolbar when we close the popup.
+ FullScreen.hideNavToolbox(true);
+ }
+ },
+
+ // UrlbarController listener method
+ onViewOpen() {
+ if (!this._isChromeCollapsed) {
+ this._isPopupOpen = true;
+ }
+ },
+
+ // UrlbarController listener method
+ onViewClose() {
+ this._isPopupOpen = false;
+ this.hideNavToolbox(true);
+ },
+
+ get navToolboxHidden() {
+ return this._isChromeCollapsed;
+ },
+
+ // Autohide helpers for the context menu item
+ getAutohide(aItem) {
+ aItem.setAttribute(
+ "checked",
+ Services.prefs.getBoolPref("browser.fullscreen.autohide")
+ );
+ },
+ setAutohide() {
+ Services.prefs.setBoolPref(
+ "browser.fullscreen.autohide",
+ !Services.prefs.getBoolPref("browser.fullscreen.autohide")
+ );
+ // Try again to hide toolbar when we change the pref.
+ FullScreen.hideNavToolbox(true);
+ },
+
+ showNavToolbox(trackMouse = true) {
+ if (BrowserHandler.kiosk) {
+ return;
+ }
+ this._fullScrToggler.hidden = true;
+ gNavToolbox.removeAttribute("fullscreenShouldAnimate");
+ gNavToolbox.style.marginTop = "";
+
+ if (!this._isChromeCollapsed) {
+ return;
+ }
+
+ // Track whether mouse is near the toolbox
+ if (trackMouse && AppConstants.platform != "macosx") {
+ let rect = gBrowser.tabpanels.getBoundingClientRect();
+ this._mouseTargetRect = {
+ top: rect.top + 50,
+ bottom: rect.bottom,
+ left: rect.left,
+ right: rect.right,
+ };
+ MousePosTracker.addListener(this);
+ }
+
+ this._isChromeCollapsed = false;
+ Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "shown");
+ },
+
+ hideNavToolbox(aAnimate = false) {
+ if (this._isChromeCollapsed) {
+ return;
+ }
+ if (!Services.prefs.getBoolPref("browser.fullscreen.autohide")) {
+ return;
+ }
+ // a popup menu is open in chrome: don't collapse chrome
+ if (this._isPopupOpen) {
+ return;
+ }
+ // On macOS we don't want to hide toolbars.
+ if (AppConstants.platform == "macosx") {
+ return;
+ }
+
+ // a textbox in chrome is focused (location bar anyone?): don't collapse chrome
+ // unless we are kiosk mode
+ let focused = document.commandDispatcher.focusedElement;
+ if (
+ focused &&
+ focused.ownerDocument == document &&
+ focused.localName == "input" &&
+ !BrowserHandler.kiosk
+ ) {
+ // But try collapse the chrome again when anything happens which can make
+ // it lose the focus. We cannot listen on "blur" event on focused here
+ // because that event can be triggered by "mousedown", and hiding chrome
+ // would cause the content to move. This combination may split a single
+ // click into two actionless halves.
+ let retryHideNavToolbox = () => {
+ // Wait for at least a frame to give it a chance to be passed down to
+ // the content.
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ // In the meantime, it's possible that we exited fullscreen somehow,
+ // so only hide the toolbox if we're still in fullscreen mode.
+ if (window.fullScreen) {
+ this.hideNavToolbox(aAnimate);
+ }
+ }, 0);
+ });
+ window.removeEventListener("keydown", retryHideNavToolbox);
+ window.removeEventListener("click", retryHideNavToolbox);
+ };
+ window.addEventListener("keydown", retryHideNavToolbox);
+ window.addEventListener("click", retryHideNavToolbox);
+ return;
+ }
+
+ if (!BrowserHandler.kiosk) {
+ this._fullScrToggler.hidden = false;
+ }
+
+ if (
+ aAnimate &&
+ window.matchMedia("(prefers-reduced-motion: no-preference)").matches &&
+ !BrowserHandler.kiosk
+ ) {
+ gNavToolbox.setAttribute("fullscreenShouldAnimate", true);
+ }
+
+ gNavToolbox.style.marginTop =
+ -gNavToolbox.getBoundingClientRect().height + "px";
+ this._isChromeCollapsed = true;
+ Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "hidden");
+
+ MousePosTracker.removeListener(this);
+ },
+
+ _updateToolbars(aEnterFS) {
+ for (let el of document.querySelectorAll(
+ "toolbar[fullscreentoolbar=true]"
+ )) {
+ if (aEnterFS) {
+ // Give the main nav bar and the tab bar the fullscreen context menu,
+ // otherwise remove context menu to prevent breakage
+ el.setAttribute("saved-context", el.getAttribute("context"));
+ if (el.id == "nav-bar" || el.id == "TabsToolbar") {
+ el.setAttribute("context", "autohide-context");
+ } else {
+ el.removeAttribute("context");
+ }
+
+ // Set the inFullscreen attribute to allow specific styling
+ // in fullscreen mode
+ el.setAttribute("inFullscreen", true);
+ } else {
+ if (el.hasAttribute("saved-context")) {
+ el.setAttribute("context", el.getAttribute("saved-context"));
+ el.removeAttribute("saved-context");
+ }
+ el.removeAttribute("inFullscreen");
+ }
+ }
+
+ ToolbarIconColor.inferFromText("fullscreen", aEnterFS);
+
+ // For macOS, we use native full screen, all full screen controls
+ // are hidden, don't bother to touch them. If we don't stop here,
+ // the following code could cause the native full screen button be
+ // shown unexpectedly. See bug 1165570.
+ if (AppConstants.platform == "macosx") {
+ return;
+ }
+
+ var fullscreenctls = document.getElementById("window-controls");
+ var navbar = document.getElementById("nav-bar");
+ var ctlsOnTabbar = window.toolbar.visible;
+ if (fullscreenctls.parentNode == navbar && ctlsOnTabbar) {
+ fullscreenctls.removeAttribute("flex");
+ document.getElementById("TabsToolbar").appendChild(fullscreenctls);
+ } else if (fullscreenctls.parentNode.id == "TabsToolbar" && !ctlsOnTabbar) {
+ fullscreenctls.setAttribute("flex", "1");
+ navbar.appendChild(fullscreenctls);
+ }
+ fullscreenctls.hidden = !aEnterFS;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(FullScreen, "_permissionNotificationIDs", () => {
+ let { PermissionUI } = ChromeUtils.import(
+ "resource:///modules/PermissionUI.jsm",
+ {}
+ );
+ return (
+ Object.values(PermissionUI)
+ .filter(value => value.prototype && value.prototype.notificationID)
+ .map(value => value.prototype.notificationID)
+ // Additionally include webRTC permission prompt which does not use PermissionUI
+ .concat(["webRTC-shareDevices"])
+ );
+});
diff --git a/browser/base/content/browser-fullZoom.js b/browser/base/content/browser-fullZoom.js
new file mode 100644
index 0000000000..bac9ee7715
--- /dev/null
+++ b/browser/base/content/browser-fullZoom.js
@@ -0,0 +1,691 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Controls the "full zoom" setting and its site-specific preferences.
+ */
+var FullZoom = {
+ // Identifies the setting in the content prefs database.
+ name: "browser.content.full-zoom",
+
+ // browser.zoom.siteSpecific preference cache
+ // Enabling privacy.resistFingerprinting implies disabling browser.zoom.siteSpecific.
+ _siteSpecificPref: undefined,
+
+ // browser.zoom.updateBackgroundTabs preference cache
+ updateBackgroundTabs: undefined,
+
+ // This maps the browser to monotonically increasing integer
+ // tokens. _browserTokenMap[browser] is increased each time the zoom is
+ // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
+ _browserTokenMap: new WeakMap(),
+
+ // Stores initial locations if we receive onLocationChange
+ // events before we're initialized.
+ _initialLocations: new WeakMap(),
+
+ get siteSpecific() {
+ if (this._siteSpecificPref === undefined) {
+ this._siteSpecificPref =
+ !Services.prefs.getBoolPref("privacy.resistFingerprinting") &&
+ Services.prefs.getBoolPref("browser.zoom.siteSpecific");
+ }
+ return this._siteSpecificPref;
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIContentPrefObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ // Initialization & Destruction
+
+ init: function FullZoom_init() {
+ gBrowser.addEventListener("DoZoomEnlargeBy10", this);
+ gBrowser.addEventListener("DoZoomReduceBy10", this);
+
+ // Register ourselves with the service so we know when our pref changes.
+ this._cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+ this._cps2.addObserverForName(this.name, this);
+
+ this.updateBackgroundTabs = Services.prefs.getBoolPref(
+ "browser.zoom.updateBackgroundTabs"
+ );
+
+ // Listen for changes to the browser.zoom branch so we can enable/disable
+ // updating background tabs and per-site saving and restoring of zoom levels.
+ Services.prefs.addObserver("browser.zoom.", this, true);
+
+ // Also need to listen to privacy.resistFingerprinting in order to update
+ // this._siteSpecificPref.
+ Services.prefs.addObserver("privacy.resistFingerprinting", this, true);
+
+ // If we received onLocationChange events for any of the current browsers
+ // before we were initialized we want to replay those upon initialization.
+ for (let browser of gBrowser.browsers) {
+ if (this._initialLocations.has(browser)) {
+ this.onLocationChange(...this._initialLocations.get(browser), browser);
+ }
+ }
+
+ // This should be nulled after initialization.
+ this._initialLocations = null;
+ },
+
+ destroy: function FullZoom_destroy() {
+ Services.prefs.removeObserver("browser.zoom.", this);
+ this._cps2.removeObserverForName(this.name, this);
+ gBrowser.removeEventListener("DoZoomEnlargeBy10", this);
+ gBrowser.removeEventListener("DoZoomReduceBy10", this);
+ },
+
+ // Event Handlers
+
+ // EventListener
+
+ handleEvent: function FullZoom_handleEvent(event) {
+ switch (event.type) {
+ case "DoZoomEnlargeBy10":
+ this.changeZoomBy(this._getTargetedBrowser(event), 0.1);
+ break;
+ case "DoZoomReduceBy10":
+ this.changeZoomBy(this._getTargetedBrowser(event), -0.1);
+ break;
+ }
+ },
+
+ // nsIObserver
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ switch (aData) {
+ case "privacy.resistFingerprinting":
+ // fall through
+ case "browser.zoom.siteSpecific":
+ // Invalidate pref cache.
+ this._siteSpecificPref = undefined;
+ break;
+ case "browser.zoom.updateBackgroundTabs":
+ this.updateBackgroundTabs = Services.prefs.getBoolPref(
+ "browser.zoom.updateBackgroundTabs"
+ );
+ break;
+ }
+ break;
+ }
+ },
+
+ // nsIContentPrefObserver
+
+ onContentPrefSet: function FullZoom_onContentPrefSet(
+ aGroup,
+ aName,
+ aValue,
+ aIsPrivate
+ ) {
+ this._onContentPrefChanged(aGroup, aValue, aIsPrivate);
+ },
+
+ onContentPrefRemoved: function FullZoom_onContentPrefRemoved(
+ aGroup,
+ aName,
+ aIsPrivate
+ ) {
+ this._onContentPrefChanged(aGroup, undefined, aIsPrivate);
+ },
+
+ /**
+ * Appropriately updates the zoom level after a content preference has
+ * changed.
+ *
+ * @param aGroup The group of the changed preference.
+ * @param aValue The new value of the changed preference. Pass undefined to
+ * indicate the preference's removal.
+ */
+ _onContentPrefChanged: function FullZoom__onContentPrefChanged(
+ aGroup,
+ aValue,
+ aIsPrivate
+ ) {
+ if (this._isNextContentPrefChangeInternal) {
+ // Ignore changes that FullZoom itself makes. This works because the
+ // content pref service calls callbacks before notifying observers, and it
+ // does both in the same turn of the event loop.
+ delete this._isNextContentPrefChangeInternal;
+ return;
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ if (!browser.currentURI) {
+ return;
+ }
+
+ if (this._isPDFViewer(browser)) {
+ return;
+ }
+
+ let ctxt = this._loadContextFromBrowser(browser);
+ let domain = this._cps2.extractDomain(browser.currentURI.spec);
+ if (aGroup) {
+ if (aGroup == domain && ctxt.usePrivateBrowsing == aIsPrivate) {
+ this._applyPrefToZoom(aValue, browser);
+ }
+ return;
+ }
+
+ // If the current page doesn't have a site-specific preference, then its
+ // zoom should be set to the new global preference now that the global
+ // preference has changed.
+ let hasPref = false;
+ let token = this._getBrowserToken(browser);
+ this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
+ handleResult() {
+ hasPref = true;
+ },
+ handleCompletion: () => {
+ if (!hasPref && token.isCurrent) {
+ this._applyPrefToZoom(undefined, browser);
+ }
+ },
+ });
+ },
+
+ // location change observer
+
+ /**
+ * Called when the location of a tab changes.
+ * When that happens, we need to update the current zoom level if appropriate.
+ *
+ * @param aURI
+ * A URI object representing the new location.
+ * @param aIsTabSwitch
+ * Whether this location change has happened because of a tab switch.
+ * @param aBrowser
+ * (optional) browser object displaying the document
+ */
+ onLocationChange: function FullZoom_onLocationChange(
+ aURI,
+ aIsTabSwitch,
+ aBrowser
+ ) {
+ let browser = aBrowser || gBrowser.selectedBrowser;
+
+ // If we haven't been initialized yet but receive an onLocationChange
+ // notification then let's store and replay it upon initialization.
+ if (this._initialLocations) {
+ this._initialLocations.set(browser, [aURI, aIsTabSwitch]);
+ return;
+ }
+
+ // Ignore all pending async zoom accesses in the browser. Pending accesses
+ // that started before the location change will be prevented from applying
+ // to the new location.
+ this._ignorePendingZoomAccesses(browser);
+
+ if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+
+ if (aURI.spec == "about:blank") {
+ if (
+ !browser.contentPrincipal ||
+ browser.contentPrincipal.isNullPrincipal
+ ) {
+ // For an about:blank with a null principal, zooming any amount does not
+ // make any sense - so simply do 100%.
+ this._applyPrefToZoom(
+ 1,
+ browser,
+ this._notifyOnLocationChange.bind(this, browser)
+ );
+ } else {
+ // If it's not a null principal, there may be content loaded into it,
+ // so use the global pref. This will avoid a cps2 roundtrip if we've
+ // already loaded the global pref once. Really, this should probably
+ // use the contentPrincipal's origin if it's an http(s) principal.
+ // (See bug 1457597)
+ this._applyPrefToZoom(
+ undefined,
+ browser,
+ this._notifyOnLocationChange.bind(this, browser)
+ );
+ }
+ return;
+ }
+
+ // Media documents should always start at 1, and are not affected by prefs.
+ if (!aIsTabSwitch && browser.isSyntheticDocument) {
+ ZoomManager.setZoomForBrowser(browser, 1);
+ // _ignorePendingZoomAccesses already called above, so no need here.
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+
+ // The PDF viewer zooming isn't handled by `ZoomManager`, ensure that the
+ // browser zoom level always gets reset to 100% on load (to prevent the
+ // UI elements of the PDF viewer from being zoomed in/out on load).
+ if (this._isPDFViewer(browser)) {
+ this._applyPrefToZoom(
+ 1,
+ browser,
+ this._notifyOnLocationChange.bind(this, browser)
+ );
+ return;
+ }
+
+ // See if the zoom pref is cached.
+ let ctxt = this._loadContextFromBrowser(browser);
+ let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
+ if (pref) {
+ this._applyPrefToZoom(
+ pref.value,
+ browser,
+ this._notifyOnLocationChange.bind(this, browser)
+ );
+ return;
+ }
+
+ // It's not cached, so we have to asynchronously fetch it.
+ let value = undefined;
+ let token = this._getBrowserToken(browser);
+ this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
+ handleResult(resultPref) {
+ value = resultPref.value;
+ },
+ handleCompletion: () => {
+ if (!token.isCurrent) {
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+ this._applyPrefToZoom(
+ value,
+ browser,
+ this._notifyOnLocationChange.bind(this, browser)
+ );
+ },
+ });
+ },
+
+ // update state of zoom type menu item
+
+ updateMenu: function FullZoom_updateMenu() {
+ var menuItem = document.getElementById("toggle_zoom");
+
+ menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
+ },
+
+ // Setting & Pref Manipulation
+
+ sendMessageToPDFViewer(browser, name) {
+ try {
+ browser.sendMessageToActor(name, {}, "Pdfjs");
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+
+ /**
+ * If browser in reader mode sends message to reader in order to decrease font size,
+ * Otherwise reduces the zoom level of the page in the current browser.
+ */
+ async reduce() {
+ let browser = gBrowser.selectedBrowser;
+ if (browser.currentURI.spec.startsWith("about:reader")) {
+ browser.sendMessageToActor("Reader:ZoomOut", {}, "AboutReader");
+ } else if (this._isPDFViewer(browser)) {
+ this.sendMessageToPDFViewer(browser, "PDFJS:ZoomOut");
+ } else {
+ ZoomManager.reduce();
+ this._ignorePendingZoomAccesses(browser);
+ await this._applyZoomToPref(browser);
+ }
+ },
+
+ /**
+ * If browser in reader mode sends message to reader in order to increase font size,
+ * Otherwise enlarges the zoom level of the page in the current browser.
+ */
+ async enlarge() {
+ let browser = gBrowser.selectedBrowser;
+ if (browser.currentURI.spec.startsWith("about:reader")) {
+ browser.sendMessageToActor("Reader:ZoomIn", {}, "AboutReader");
+ } else if (this._isPDFViewer(browser)) {
+ this.sendMessageToPDFViewer(browser, "PDFJS:ZoomIn");
+ } else {
+ ZoomManager.enlarge();
+ this._ignorePendingZoomAccesses(browser);
+ await this._applyZoomToPref(browser);
+ }
+ },
+
+ /**
+ * If browser in reader mode sends message to reader in order to increase font size,
+ * Otherwise enlarges the zoom level of the page in the current browser.
+ * This function is not async like reduce/enlarge, because it is invoked by our
+ * event handler. This means that the call to _applyZoomToPref is not awaited and
+ * will happen asynchronously.
+ */
+ changeZoomBy(aBrowser, aValue) {
+ if (aBrowser.currentURI.spec.startsWith("about:reader")) {
+ const message = aValue > 0 ? "Reader::ZoomIn" : "Reader:ZoomOut";
+ aBrowser.sendMessageToActor(message, {}, "AboutReader");
+ return;
+ } else if (this._isPDFViewer(aBrowser)) {
+ const message = aValue > 0 ? "PDFJS::ZoomIn" : "PDFJS:ZoomOut";
+ this.sendMessageToPDFViewer(aBrowser, message);
+ return;
+ }
+ let zoom = ZoomManager.getZoomForBrowser(aBrowser);
+ zoom += aValue;
+ if (zoom < ZoomManager.MIN) {
+ zoom = ZoomManager.MIN;
+ } else if (zoom > ZoomManager.MAX) {
+ zoom = ZoomManager.MAX;
+ }
+ ZoomManager.setZoomForBrowser(aBrowser, zoom);
+ this._ignorePendingZoomAccesses(aBrowser);
+ this._applyZoomToPref(aBrowser);
+ },
+
+ /**
+ * Sets the zoom level for the given browser to the given floating
+ * point value, where 1 is the default zoom level.
+ */
+ setZoom(value, browser = gBrowser.selectedBrowser) {
+ if (this._isPDFViewer(browser)) {
+ return;
+ }
+ ZoomManager.setZoomForBrowser(browser, value);
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ },
+
+ /**
+ * Sets the zoom level of the page in the given browser to the global zoom
+ * level.
+ *
+ * @return A promise which resolves when the zoom reset has been applied.
+ */
+ reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) {
+ let forceValue;
+ if (browser.currentURI.spec.startsWith("about:reader")) {
+ browser.sendMessageToActor("Reader:ResetZoom", {}, "AboutReader");
+ } else if (this._isPDFViewer(browser)) {
+ this.sendMessageToPDFViewer(browser, "PDFJS:ZoomReset");
+ // Ensure that the UI elements of the PDF viewer won't be zoomed in/out
+ // on reset, even if/when browser default zoom value is not set to 100%.
+ forceValue = 1;
+ }
+ let token = this._getBrowserToken(browser);
+ let result = ZoomUI.getGlobalValue().then(value => {
+ if (token.isCurrent) {
+ ZoomManager.setZoomForBrowser(browser, forceValue || value);
+ this._ignorePendingZoomAccesses(browser);
+ }
+ });
+ this._removePref(browser);
+ return result;
+ },
+
+ resetScalingZoom: function FullZoom_resetScaling(
+ browser = gBrowser.selectedBrowser
+ ) {
+ browser.browsingContext?.resetScalingZoom();
+ },
+
+ /**
+ * Set the zoom level for a given browser.
+ *
+ * Per nsPresContext::setFullZoom, we can set the zoom to its current value
+ * without significant impact on performance, as the setting is only applied
+ * if it differs from the current setting. In fact getting the zoom and then
+ * checking ourselves if it differs costs more.
+ *
+ * And perhaps we should always set the zoom even if it was more expensive,
+ * since nsDocumentViewer::SetTextZoom claims that child documents can have
+ * a different text zoom (although it would be unusual), and it implies that
+ * those child text zooms should get updated when the parent zoom gets set,
+ * and perhaps the same is true for full zoom
+ * (although nsDocumentViewer::SetFullZoom doesn't mention it).
+ *
+ * So when we apply new zoom values to the browser, we simply set the zoom.
+ * We don't check first to see if the new value is the same as the current
+ * one.
+ *
+ * @param aValue The zoom level value.
+ * @param aBrowser The zoom is set in this browser. Required.
+ * @param aCallback If given, it's asynchronously called when complete.
+ */
+ _applyPrefToZoom: function FullZoom__applyPrefToZoom(
+ aValue,
+ aBrowser,
+ aCallback
+ ) {
+ if (gInPrintPreviewMode) {
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ // The browser is sometimes half-destroyed because this method is called
+ // by content pref service callbacks, which themselves can be called at any
+ // time, even after browsers are closed.
+ if (
+ !aBrowser.mInitialized ||
+ aBrowser.isSyntheticDocument ||
+ (!this.siteSpecific && aBrowser.tabHasCustomZoom)
+ ) {
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ if (aValue !== undefined && this.siteSpecific) {
+ ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
+ this._ignorePendingZoomAccesses(aBrowser);
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ // Above, we check if site-specific zoom is enabled before setting
+ // the tab browser zoom, however global zoom should work independent
+ // of the site-specific pref, so we do no checks here.
+ let token = this._getBrowserToken(aBrowser);
+ ZoomUI.getGlobalValue().then(value => {
+ if (token.isCurrent) {
+ ZoomManager.setZoomForBrowser(aBrowser, value);
+ this._ignorePendingZoomAccesses(aBrowser);
+ }
+ this._executeSoon(aCallback);
+ });
+ },
+
+ /**
+ * Saves the zoom level of the page in the given browser to the content
+ * prefs store.
+ *
+ * @param browser The zoom of this browser will be saved. Required.
+ */
+ _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
+ if (
+ !this.siteSpecific ||
+ gInPrintPreviewMode ||
+ browser.isSyntheticDocument
+ ) {
+ // If site-specific zoom is disabled, we have called this function
+ // to adjust our tab's zoom level. It is now considered "custom"
+ // and we mark that here.
+ browser.tabHasCustomZoom = !this.siteSpecific;
+ return null;
+ }
+
+ return new Promise(resolve => {
+ this._cps2.set(
+ browser.currentURI.spec,
+ this.name,
+ ZoomManager.getZoomForBrowser(browser),
+ this._loadContextFromBrowser(browser),
+ {
+ handleCompletion: () => {
+ this._isNextContentPrefChangeInternal = true;
+ resolve();
+ },
+ }
+ );
+ });
+ },
+
+ /**
+ * Removes from the content prefs store the zoom level of the given browser.
+ *
+ * @param browser The zoom of this browser will be removed. Required.
+ */
+ _removePref: function FullZoom__removePref(browser) {
+ if (browser.isSyntheticDocument) {
+ return;
+ }
+ let ctxt = this._loadContextFromBrowser(browser);
+ this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
+ handleCompletion: () => {
+ this._isNextContentPrefChangeInternal = true;
+ },
+ });
+ },
+
+ // Utilities
+
+ /**
+ * Returns the zoom change token of the given browser. Asynchronous
+ * operations that access the given browser's zoom should use this method to
+ * capture the token before starting and use token.isCurrent to determine if
+ * it's safe to access the zoom when done. If token.isCurrent is false, then
+ * after the async operation started, either the browser's zoom was changed or
+ * the browser was destroyed, and depending on what the operation is doing, it
+ * may no longer be safe to set and get its zoom.
+ *
+ * @param browser The token of this browser will be returned.
+ * @return An object with an "isCurrent" getter.
+ */
+ _getBrowserToken: function FullZoom__getBrowserToken(browser) {
+ let map = this._browserTokenMap;
+ if (!map.has(browser)) {
+ map.set(browser, 0);
+ }
+ return {
+ token: map.get(browser),
+ get isCurrent() {
+ // At this point, the browser may have been destructed and unbound but
+ // its outer ID not removed from the map because outer-window-destroyed
+ // hasn't been received yet. In that case, the browser is unusable, it
+ // has no properties, so return false. Check for this case by getting a
+ // property, say, docShell.
+ return map.get(browser) === this.token && browser.mInitialized;
+ },
+ };
+ },
+
+ /**
+ * Returns the browser that the supplied zoom event is associated with.
+ * @param event The zoom event.
+ * @return The associated browser element, if one exists, otherwise null.
+ */
+ _getTargetedBrowser: function FullZoom__getTargetedBrowser(event) {
+ let target = event.originalTarget;
+
+ // With remote content browsers, the event's target is the browser
+ // we're looking for.
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ if (
+ target instanceof window.XULElement &&
+ target.localName == "browser" &&
+ target.namespaceURI == XUL_NS
+ ) {
+ return target;
+ }
+
+ // With in-process content browsers, the event's target is the content
+ // document.
+ if (target.nodeType == Node.DOCUMENT_NODE) {
+ return target.ownerGlobal.docShell.chromeEventHandler;
+ }
+
+ throw new Error("Unexpected zoom event source");
+ },
+
+ /**
+ * Increments the zoom change token for the given browser so that pending
+ * async operations know that it may be unsafe to access they zoom when they
+ * finish.
+ *
+ * @param browser Pending accesses in this browser will be ignored.
+ */
+ _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(
+ browser
+ ) {
+ let map = this._browserTokenMap;
+ map.set(browser, (map.get(browser) || 0) + 1);
+ },
+
+ _ensureValid: function FullZoom__ensureValid(aValue) {
+ // Note that undefined is a valid value for aValue that indicates a known-
+ // not-to-exist value.
+ if (isNaN(aValue)) {
+ return 1;
+ }
+
+ if (aValue < ZoomManager.MIN) {
+ return ZoomManager.MIN;
+ }
+
+ if (aValue > ZoomManager.MAX) {
+ return ZoomManager.MAX;
+ }
+
+ return aValue;
+ },
+
+ /**
+ * Gets the load context from the given Browser.
+ *
+ * @param Browser The Browser whose load context will be returned.
+ * @return The nsILoadContext of the given Browser.
+ */
+ _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
+ return browser.loadContext;
+ },
+
+ /**
+ * Asynchronously broadcasts "browser-fullZoom:location-change" so that
+ * listeners can be notified when the zoom levels on those pages change.
+ * The notification is always asynchronous so that observers are guaranteed a
+ * consistent behavior.
+ */
+ _notifyOnLocationChange: function FullZoom__notifyOnLocationChange(browser) {
+ this._executeSoon(function() {
+ Services.obs.notifyObservers(browser, "browser-fullZoom:location-change");
+ });
+ },
+
+ _executeSoon: function FullZoom__executeSoon(callback) {
+ if (!callback) {
+ return;
+ }
+ Services.tm.dispatchToMainThread(callback);
+ },
+
+ _isPDFViewer(browser) {
+ return !!(
+ browser.contentPrincipal.spec == "resource://pdf.js/web/viewer.html"
+ );
+ },
+};
diff --git a/browser/base/content/browser-fxaSignout.js b/browser/base/content/browser-fxaSignout.js
new file mode 100644
index 0000000000..0f5f4e2b5e
--- /dev/null
+++ b/browser/base/content/browser-fxaSignout.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/.
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "SyncDisconnect",
+ "resource://services-sync/SyncDisconnect.jsm"
+);
+
+function onLoad() {
+ const hideDeleteLocalDataSections = window.arguments[0].hideDeleteDataOption;
+ document.getElementById(
+ "shouldDeleteLocalData"
+ ).hidden = hideDeleteLocalDataSections;
+ document.getElementById(
+ "fxaSignoutDetail"
+ ).hidden = hideDeleteLocalDataSections;
+
+ document.addEventListener("dialogaccept", () => {
+ window.arguments[1].deleteLocalData = document.getElementById(
+ "shouldDeleteLocalData"
+ ).checked;
+ window.arguments[1].userConfirmedDisconnect = true;
+ });
+}
diff --git a/browser/base/content/browser-fxaSignout.xhtml b/browser/base/content/browser-fxaSignout.xhtml
new file mode 100644
index 0000000000..d3447a6962
--- /dev/null
+++ b/browser/base/content/browser-fxaSignout.xhtml
@@ -0,0 +1,32 @@
+<?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/"?>
+<?xml-stylesheet href="chrome://browser/skin/fxaSignout.css" type="text/css"?>
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ data-l10n-attrs="title,style"
+ onload="onLoad()">
+ <dialog id="fxaSignoutDialog"
+ buttons="accept,cancel"
+ data-l10n-id="fxa-signout-dialog"
+ data-l10n-attrs="style, buttonlabelaccept, buttonaccesskeyaccept">
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl"/>
+ <html:link rel="localization" href="browser/branding/sync-brand.ftl"/>
+ <html:link rel="localization" href="browser/sync.ftl"/>
+ </linkset>
+ <script src="chrome://browser/content/browser-fxaSignout.js"/>
+ <vbox>
+ <description class="sign-out-header" data-l10n-id="fxa-signout-dialog-heading"></description>
+ <label id="fxaSignoutDetail" data-l10n-id="fxa-signout-dialog-body"></label>
+ <separator/>
+ <checkbox id="shouldDeleteLocalData" class="delete-local-data-checkbox" data-l10n-id="fxa-signout-checkbox"/>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/base/content/browser-gestureSupport.js b/browser/base/content/browser-gestureSupport.js
new file mode 100644
index 0000000000..b893bf54c5
--- /dev/null
+++ b/browser/base/content/browser-gestureSupport.js
@@ -0,0 +1,846 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+// Simple gestures support
+//
+// As per bug #412486, web content must not be allowed to receive any
+// simple gesture events. Multi-touch gesture APIs are in their
+// infancy and we do NOT want to be forced into supporting an API that
+// will probably have to change in the future. (The current Mac OS X
+// API is undocumented and was reverse-engineered.) Until support is
+// implemented in the event dispatcher to keep these events as
+// chrome-only, we must listen for the simple gesture events during
+// the capturing phase and call stopPropagation on every event.
+
+var gGestureSupport = {
+ _currentRotation: 0,
+ _lastRotateDelta: 0,
+ _rotateMomentumThreshold: 0.75,
+
+ /**
+ * Add or remove mouse gesture event listeners
+ *
+ * @param aAddListener
+ * True to add/init listeners and false to remove/uninit
+ */
+ init: function GS_init(aAddListener) {
+ const gestureEvents = [
+ "SwipeGestureMayStart",
+ "SwipeGestureStart",
+ "SwipeGestureUpdate",
+ "SwipeGestureEnd",
+ "SwipeGesture",
+ "MagnifyGestureStart",
+ "MagnifyGestureUpdate",
+ "MagnifyGesture",
+ "RotateGestureStart",
+ "RotateGestureUpdate",
+ "RotateGesture",
+ "TapGesture",
+ "PressTapGesture",
+ ];
+
+ let addRemove = aAddListener
+ ? window.addEventListener
+ : window.removeEventListener;
+
+ for (let event of gestureEvents) {
+ addRemove("Moz" + event, this, true);
+ }
+ },
+
+ /**
+ * Dispatch events based on the type of mouse gesture event. For now, make
+ * sure to stop propagation of every gesture event so that web content cannot
+ * receive gesture events.
+ *
+ * @param aEvent
+ * The gesture event to handle
+ */
+ handleEvent: function GS_handleEvent(aEvent) {
+ if (
+ !Services.prefs.getBoolPref(
+ "dom.debug.propagate_gesture_events_through_content"
+ )
+ ) {
+ aEvent.stopPropagation();
+ }
+
+ // Create a preference object with some defaults
+ let def = (aThreshold, aLatched) => ({
+ threshold: aThreshold,
+ latched: !!aLatched,
+ });
+
+ switch (aEvent.type) {
+ case "MozSwipeGestureMayStart":
+ if (this._shouldDoSwipeGesture(aEvent)) {
+ aEvent.preventDefault();
+ }
+ break;
+ case "MozSwipeGestureStart":
+ aEvent.preventDefault();
+ this._setupSwipeGesture();
+ break;
+ case "MozSwipeGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozSwipeGestureEnd":
+ aEvent.preventDefault();
+ this._doEnd(aEvent);
+ break;
+ case "MozSwipeGesture":
+ aEvent.preventDefault();
+ this.onSwipe(aEvent);
+ break;
+ case "MozMagnifyGestureStart":
+ aEvent.preventDefault();
+ this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in");
+ break;
+ case "MozRotateGestureStart":
+ aEvent.preventDefault();
+ this._setupGesture(aEvent, "twist", def(25, 0), "right", "left");
+ break;
+ case "MozMagnifyGestureUpdate":
+ case "MozRotateGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozTapGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["tap"]);
+ break;
+ case "MozRotateGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["twist", "end"]);
+ break;
+ /* case "MozPressTapGesture":
+ break; */
+ }
+ },
+
+ /**
+ * Called at the start of "pinch" and "twist" gestures to setup all of the
+ * information needed to process the gesture
+ *
+ * @param aEvent
+ * The continual motion start event to handle
+ * @param aGesture
+ * Name of the gesture to handle
+ * @param aPref
+ * Preference object with the names of preferences and defaults
+ * @param aInc
+ * Command to trigger for increasing motion (without gesture name)
+ * @param aDec
+ * Command to trigger for decreasing motion (without gesture name)
+ */
+ _setupGesture: function GS__setupGesture(
+ aEvent,
+ aGesture,
+ aPref,
+ aInc,
+ aDec
+ ) {
+ // Try to load user-set values from preferences
+ for (let [pref, def] of Object.entries(aPref)) {
+ aPref[pref] = this._getPref(aGesture + "." + pref, def);
+ }
+
+ // Keep track of the total deltas and latching behavior
+ let offset = 0;
+ let latchDir = aEvent.delta > 0 ? 1 : -1;
+ let isLatched = false;
+
+ // Create the update function here to capture closure state
+ this._doUpdate = function GS__doUpdate(updateEvent) {
+ // Update the offset with new event data
+ offset += updateEvent.delta;
+
+ // Check if the cumulative deltas exceed the threshold
+ if (Math.abs(offset) > aPref.threshold) {
+ // Trigger the action if we don't care about latching; otherwise, make
+ // sure either we're not latched and going the same direction of the
+ // initial motion; or we're latched and going the opposite way
+ let sameDir = (latchDir ^ offset) >= 0;
+ if (!aPref.latched || isLatched ^ sameDir) {
+ this._doAction(updateEvent, [aGesture, offset > 0 ? aInc : aDec]);
+
+ // We must be getting latched or leaving it, so just toggle
+ isLatched = !isLatched;
+ }
+
+ // Reset motion counter to prepare for more of the same gesture
+ offset = 0;
+ }
+ };
+
+ // The start event also contains deltas, so handle an update right away
+ this._doUpdate(aEvent);
+ },
+
+ /**
+ * Checks whether a swipe gesture event can navigate the browser history or
+ * not.
+ *
+ * @param aEvent
+ * The swipe gesture event.
+ * @return true if the swipe event may navigate the history, false othwerwise.
+ */
+ _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) {
+ return (
+ this._getCommand(aEvent, ["swipe", "left"]) ==
+ "Browser:BackOrBackDuplicate" &&
+ this._getCommand(aEvent, ["swipe", "right"]) ==
+ "Browser:ForwardOrForwardDuplicate"
+ );
+ },
+
+ /**
+ * Checks whether we want to start a swipe for aEvent and sets
+ * aEvent.allowedDirections to the right values.
+ *
+ * @param aEvent
+ * The swipe gesture "MayStart" event.
+ * @return true if we're willing to start a swipe for this event, false
+ * otherwise.
+ */
+ _shouldDoSwipeGesture: function GS__shouldDoSwipeGesture(aEvent) {
+ if (!this._swipeNavigatesHistory(aEvent)) {
+ return false;
+ }
+
+ let isVerticalSwipe = false;
+ if (aEvent.direction == aEvent.DIRECTION_UP) {
+ if (gMultiProcessBrowser || window.content.pageYOffset > 0) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
+ if (
+ gMultiProcessBrowser ||
+ window.content.pageYOffset < window.content.scrollMaxY
+ ) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ }
+ if (isVerticalSwipe) {
+ // Vertical overscroll has been temporarily disabled until bug 939480 is
+ // fixed.
+ return false;
+ }
+
+ let canGoBack = gHistorySwipeAnimation.canGoBack();
+ let canGoForward = gHistorySwipeAnimation.canGoForward();
+ let isLTR = gHistorySwipeAnimation.isLTR;
+
+ if (canGoBack) {
+ aEvent.allowedDirections |= isLTR
+ ? aEvent.DIRECTION_LEFT
+ : aEvent.DIRECTION_RIGHT;
+ }
+ if (canGoForward) {
+ aEvent.allowedDirections |= isLTR
+ ? aEvent.DIRECTION_RIGHT
+ : aEvent.DIRECTION_LEFT;
+ }
+
+ return true;
+ },
+
+ /**
+ * Sets up swipe gestures. This includes setting up swipe animations for the
+ * gesture, if enabled.
+ *
+ * @param aEvent
+ * The swipe gesture start event.
+ * @return true if swipe gestures could successfully be set up, false
+ * othwerwise.
+ */
+ _setupSwipeGesture: function GS__setupSwipeGesture() {
+ gHistorySwipeAnimation.startAnimation();
+
+ this._doUpdate = function GS__doUpdate(aEvent) {
+ gHistorySwipeAnimation.updateAnimation(aEvent.delta);
+ };
+
+ this._doEnd = function GS__doEnd(aEvent) {
+ gHistorySwipeAnimation.swipeEndEventReceived();
+
+ this._doUpdate = function() {};
+ this._doEnd = function() {};
+ };
+ },
+
+ /**
+ * Generator producing the powerset of the input array where the first result
+ * is the complete set and the last result (before StopIteration) is empty.
+ *
+ * @param aArray
+ * Source array containing any number of elements
+ * @yield Array that is a subset of the input array from full set to empty
+ */
+ _power: function* GS__power(aArray) {
+ // Create a bitmask based on the length of the array
+ let num = 1 << aArray.length;
+ while (--num >= 0) {
+ // Only select array elements where the current bit is set
+ yield aArray.reduce(function(aPrev, aCurr, aIndex) {
+ if (num & (1 << aIndex)) {
+ aPrev.push(aCurr);
+ }
+ return aPrev;
+ }, []);
+ }
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set, and execute the command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ * @return Name of the executed command. Returns null if no command is
+ * found.
+ */
+ _doAction: function GS__doAction(aEvent, aGesture) {
+ let command = this._getCommand(aEvent, aGesture);
+ return command && this._doCommand(aEvent, command);
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ */
+ _getCommand: function GS__getCommand(aEvent, aGesture) {
+ // Create an array of pressed keys in a fixed order so that a command for
+ // "meta" is preferred over "ctrl" when both buttons are pressed (and a
+ // command for both don't exist)
+ let keyCombos = [];
+ for (let key of ["shift", "alt", "ctrl", "meta"]) {
+ if (aEvent[key + "Key"]) {
+ keyCombos.push(key);
+ }
+ }
+
+ // Try each combination of key presses in decreasing order for commands
+ for (let subCombo of this._power(keyCombos)) {
+ // Convert a gesture and pressed keys into the corresponding command
+ // action where the preference has the gesture before "shift" before
+ // "alt" before "ctrl" before "meta" all separated by periods
+ let command;
+ try {
+ command = this._getPref(aGesture.concat(subCombo).join("."));
+ } catch (e) {}
+
+ if (command) {
+ return command;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Execute the specified command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aCommand
+ * Name of the command found for the event's keys and gesture.
+ */
+ _doCommand: function GS__doCommand(aEvent, aCommand) {
+ let node = document.getElementById(aCommand);
+ if (node) {
+ if (node.getAttribute("disabled") != "true") {
+ let cmdEvent = document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ window,
+ 0,
+ aEvent.ctrlKey,
+ aEvent.altKey,
+ aEvent.shiftKey,
+ aEvent.metaKey,
+ aEvent,
+ aEvent.mozInputSource
+ );
+ node.dispatchEvent(cmdEvent);
+ }
+ } else {
+ goDoCommand(aCommand);
+ }
+ },
+
+ /**
+ * Handle continual motion events. This function will be set by
+ * _setupGesture or _setupSwipe.
+ *
+ * @param aEvent
+ * The continual motion update event to handle
+ */
+ _doUpdate(aEvent) {},
+
+ /**
+ * Handle gesture end events. This function will be set by _setupSwipe.
+ *
+ * @param aEvent
+ * The gesture end event to handle
+ */
+ _doEnd(aEvent) {},
+
+ /**
+ * Convert the swipe gesture into a browser action based on the direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ */
+ onSwipe: function GS_onSwipe(aEvent) {
+ // Figure out which one (and only one) direction was triggered
+ for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) {
+ if (aEvent.direction == aEvent["DIRECTION_" + dir]) {
+ this._coordinateSwipeEventWithAnimation(aEvent, dir);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Process a swipe event based on the given direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) {
+ this._doAction(aEvent, ["swipe", aDir.toLowerCase()]);
+ },
+
+ /**
+ * Coordinates the swipe event with the swipe animation, if any.
+ * If an animation is currently running, the swipe event will be
+ * processed once the animation stops. This will guarantee a fluid
+ * motion of the animation.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ _coordinateSwipeEventWithAnimation: function GS__coordinateSwipeEventWithAnimation(
+ aEvent,
+ aDir
+ ) {
+ gHistorySwipeAnimation.stopAnimation();
+ this.processSwipeEvent(aEvent, aDir);
+ },
+
+ /**
+ * Get a gesture preference or use a default if it doesn't exist
+ *
+ * @param aPref
+ * Name of the preference to load under the gesture branch
+ * @param aDef
+ * Default value if the preference doesn't exist
+ */
+ _getPref: function GS__getPref(aPref, aDef) {
+ // Preferences branch under which all gestures preferences are stored
+ const branch = "browser.gesture.";
+
+ try {
+ // Determine what type of data to load based on default value's type
+ let type = typeof aDef;
+ let getFunc = "Char";
+ if (type == "boolean") {
+ getFunc = "Bool";
+ } else if (type == "number") {
+ getFunc = "Int";
+ }
+ return Services.prefs["get" + getFunc + "Pref"](branch + aPref);
+ } catch (e) {
+ return aDef;
+ }
+ },
+
+ /**
+ * Perform rotation for ImageDocuments
+ *
+ * @param aEvent
+ * The MozRotateGestureUpdate event triggering this call
+ */
+ rotate(aEvent) {
+ if (!(window.content.document instanceof ImageDocument)) {
+ return;
+ }
+
+ let contentElement = window.content.document.body.firstElementChild;
+ if (!contentElement) {
+ return;
+ }
+ // If we're currently snapping, cancel that snap
+ if (contentElement.classList.contains("completeRotation")) {
+ this._clearCompleteRotation();
+ }
+
+ this.rotation = Math.round(this.rotation + aEvent.delta);
+ contentElement.style.transform = "rotate(" + this.rotation + "deg)";
+ this._lastRotateDelta = aEvent.delta;
+ },
+
+ /**
+ * Perform a rotation end for ImageDocuments
+ */
+ rotateEnd() {
+ if (!(window.content.document instanceof ImageDocument)) {
+ return;
+ }
+
+ let contentElement = window.content.document.body.firstElementChild;
+ if (!contentElement) {
+ return;
+ }
+
+ let transitionRotation = 0;
+
+ // The reason that 360 is allowed here is because when rotating between
+ // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong
+ // direction around--spinning wildly.
+ if (this.rotation <= 45) {
+ transitionRotation = 0;
+ } else if (this.rotation > 45 && this.rotation <= 135) {
+ transitionRotation = 90;
+ } else if (this.rotation > 135 && this.rotation <= 225) {
+ transitionRotation = 180;
+ } else if (this.rotation > 225 && this.rotation <= 315) {
+ transitionRotation = 270;
+ } else {
+ transitionRotation = 360;
+ }
+
+ // If we're going fast enough, and we didn't already snap ahead of rotation,
+ // then snap ahead of rotation to simulate momentum
+ if (
+ this._lastRotateDelta > this._rotateMomentumThreshold &&
+ this.rotation > transitionRotation
+ ) {
+ transitionRotation += 90;
+ } else if (
+ this._lastRotateDelta < -1 * this._rotateMomentumThreshold &&
+ this.rotation < transitionRotation
+ ) {
+ transitionRotation -= 90;
+ }
+
+ // Only add the completeRotation class if it is is necessary
+ if (transitionRotation != this.rotation) {
+ contentElement.classList.add("completeRotation");
+ contentElement.addEventListener(
+ "transitionend",
+ this._clearCompleteRotation
+ );
+ }
+
+ contentElement.style.transform = "rotate(" + transitionRotation + "deg)";
+ this.rotation = transitionRotation;
+ },
+
+ /**
+ * Gets the current rotation for the ImageDocument
+ */
+ get rotation() {
+ return this._currentRotation;
+ },
+
+ /**
+ * Sets the current rotation for the ImageDocument
+ *
+ * @param aVal
+ * The new value to take. Can be any value, but it will be bounded to
+ * 0 inclusive to 360 exclusive.
+ */
+ set rotation(aVal) {
+ this._currentRotation = aVal % 360;
+ if (this._currentRotation < 0) {
+ this._currentRotation += 360;
+ }
+ return this._currentRotation;
+ },
+
+ /**
+ * When the location/tab changes, need to reload the current rotation for the
+ * image
+ */
+ restoreRotationState() {
+ // Bug 1108553 - Cannot rotate images in stand-alone image documents with e10s
+ if (gMultiProcessBrowser) {
+ return;
+ }
+
+ if (!(window.content.document instanceof ImageDocument)) {
+ return;
+ }
+
+ let contentElement = window.content.document.body.firstElementChild;
+ let transformValue = window.content.window.getComputedStyle(contentElement)
+ .transform;
+
+ if (transformValue == "none") {
+ this.rotation = 0;
+ return;
+ }
+
+ // transformValue is a rotation matrix--split it and do mathemagic to
+ // obtain the real rotation value
+ transformValue = transformValue
+ .split("(")[1]
+ .split(")")[0]
+ .split(",");
+ this.rotation = Math.round(
+ Math.atan2(transformValue[1], transformValue[0]) * (180 / Math.PI)
+ );
+ },
+
+ /**
+ * Removes the transition rule by removing the completeRotation class
+ */
+ _clearCompleteRotation() {
+ let contentElement =
+ window.content.document &&
+ window.content.document instanceof ImageDocument &&
+ window.content.document.body &&
+ window.content.document.body.firstElementChild;
+ if (!contentElement) {
+ return;
+ }
+ contentElement.classList.remove("completeRotation");
+ contentElement.removeEventListener(
+ "transitionend",
+ this._clearCompleteRotation
+ );
+ },
+};
+
+// History Swipe Animation Support (bug 678392)
+var gHistorySwipeAnimation = {
+ active: false,
+ isLTR: false,
+
+ /**
+ * Initializes the support for history swipe animations, if it is supported
+ * by the platform/configuration.
+ */
+ init: function HSA_init() {
+ if (!this._isSupported()) {
+ return;
+ }
+
+ this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)");
+ this._isStoppingAnimation = false;
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.history_swipe_animation.disabled",
+ false
+ )
+ ) {
+ this.active = true;
+ }
+ },
+
+ /**
+ * Uninitializes the support for history swipe animations.
+ */
+ uninit: function HSA_uninit() {
+ this.active = false;
+ this.isLTR = false;
+ this._removeBoxes();
+ },
+
+ /**
+ * Starts the swipe animation.
+ *
+ * @param aIsVerticalSwipe
+ * Whether we're dealing with a vertical swipe or not.
+ */
+ startAnimation: function HSA_startAnimation() {
+ if (this.isAnimationRunning()) {
+ return;
+ }
+
+ this._isStoppingAnimation = false;
+ this._canGoBack = this.canGoBack();
+ this._canGoForward = this.canGoForward();
+ if (this.active) {
+ this._addBoxes();
+ }
+ this.updateAnimation(0);
+ },
+
+ /**
+ * Stops the swipe animation.
+ */
+ stopAnimation: function HSA_stopAnimation() {
+ if (!this.isAnimationRunning()) {
+ return;
+ }
+ this._isStoppingAnimation = true;
+ let box = this._prevBox.style.opacity > 0 ? this._prevBox : this._nextBox;
+ if (box.style.opacity > 0) {
+ box.style.transition = "opacity 0.2s cubic-bezier(.07,.95,0,1)";
+ box.addEventListener("transitionend", this._completeFadeOut);
+ box.style.opacity = 0;
+ } else {
+ this._removeBoxes();
+ }
+ },
+
+ /**
+ * Updates the animation between two pages in history.
+ *
+ * @param aVal
+ * A floating point value that represents the progress of the
+ * swipe gesture.
+ */
+ updateAnimation: function HSA_updateAnimation(aVal) {
+ if (!this.isAnimationRunning() || this._isStoppingAnimation) {
+ return;
+ }
+
+ // We use the following value to set the opacity of the swipe arrows. It was
+ // determined experimentally that absolute values of 0.25 (or greater)
+ // trigger history navigation, hence the multiplier 4 to set the arrows to
+ // full opacity at 0.25 or greater.
+ let opacity = Math.abs(aVal) * 4;
+ if ((aVal >= 0 && this.isLTR) || (aVal <= 0 && !this.isLTR)) {
+ // The intention is to go back.
+ if (this._canGoBack) {
+ this._prevBox.collapsed = false;
+ this._nextBox.collapsed = true;
+ this._prevBox.style.opacity = opacity > 1 ? 1 : opacity;
+ }
+ } else if (this._canGoForward) {
+ // The intention is to go forward.
+ this._nextBox.collapsed = false;
+ this._prevBox.collapsed = true;
+ this._nextBox.style.opacity = opacity > 1 ? 1 : opacity;
+ }
+ },
+
+ /**
+ * Checks whether the history swipe animation is currently running or not.
+ *
+ * @return true if the animation is currently running, false otherwise.
+ */
+ isAnimationRunning: function HSA_isAnimationRunning() {
+ return !!this._container;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go back to.
+ *
+ * @return true if there is a previous page in history, false otherwise.
+ */
+ canGoBack: function HSA_canGoBack() {
+ return gBrowser.webNavigation.canGoBack;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go forward to.
+ *
+ * @return true if there is a next page in history, false otherwise.
+ */
+ canGoForward: function HSA_canGoForward() {
+ return gBrowser.webNavigation.canGoForward;
+ },
+
+ /**
+ * Used to notify the history swipe animation that the OS sent a swipe end
+ * event and that we should navigate to the page that the user swiped to, if
+ * any. This will also result in the animation overlay to be torn down.
+ */
+ swipeEndEventReceived: function HSA_swipeEndEventReceived() {
+ this.stopAnimation();
+ },
+
+ /**
+ * Checks to see if history swipe animations are supported by this
+ * platform/configuration.
+ *
+ * return true if supported, false otherwise.
+ */
+ _isSupported: function HSA__isSupported() {
+ return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
+ },
+
+ _completeFadeOut: function HSA__completeFadeOut(aEvent) {
+ gHistorySwipeAnimation._removeBoxes();
+ },
+
+ /**
+ * Adds the boxes that contain the arrows used during the swipe animation.
+ */
+ _addBoxes: function HSA__addBoxes() {
+ let browserStack = gBrowser.getPanel().querySelector(".browserStack");
+ this._container = this._createElement(
+ "historySwipeAnimationContainer",
+ "stack"
+ );
+ browserStack.appendChild(this._container);
+
+ this._prevBox = this._createElement(
+ "historySwipeAnimationPreviousArrow",
+ "box"
+ );
+ this._prevBox.collapsed = true;
+ this._prevBox.style.opacity = 0;
+ this._container.appendChild(this._prevBox);
+
+ this._nextBox = this._createElement(
+ "historySwipeAnimationNextArrow",
+ "box"
+ );
+ this._nextBox.collapsed = true;
+ this._nextBox.style.opacity = 0;
+ this._container.appendChild(this._nextBox);
+ },
+
+ /**
+ * Removes the boxes.
+ */
+ _removeBoxes: function HSA__removeBoxes() {
+ this._prevBox = null;
+ this._nextBox = null;
+ if (this._container) {
+ this._container.remove();
+ }
+ this._container = null;
+ },
+
+ /**
+ * Creates an element with a given identifier and tag name.
+ *
+ * @param aID
+ * An identifier to create the element with.
+ * @param aTagName
+ * The name of the tag to create the element for.
+ * @return the newly created element.
+ */
+ _createElement: function HSA__createElement(aID, aTagName) {
+ let element = document.createXULElement(aTagName);
+ element.id = aID;
+ return element;
+ },
+};
diff --git a/browser/base/content/browser-graphics-utils.js b/browser/base/content/browser-graphics-utils.js
new file mode 100644
index 0000000000..fa3620386d
--- /dev/null
+++ b/browser/base/content/browser-graphics-utils.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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Global browser interface with graphics utilities.
+ */
+var gGfxUtils = {
+ _isRecording: false,
+ _isTransactionLogging: false,
+
+ init() {
+ if (Services.prefs.getBoolPref("gfx.webrender.enable-capture")) {
+ document.getElementById("wrCaptureCmd").removeAttribute("disabled");
+ document
+ .getElementById("wrToggleCaptureSequenceCmd")
+ .removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Toggle composition recording for the current window.
+ */
+ toggleWindowRecording() {
+ window.windowUtils.setCompositionRecording(!this._isRecording);
+ this._isRecording = !this._isRecording;
+ },
+ /**
+ * Trigger a WebRender capture of the current state into a local folder.
+ */
+ webrenderCapture() {
+ window.windowUtils.wrCapture();
+ },
+ /**
+ * Trigger a WebRender capture of the current state and future state
+ * into a local folder. If called again, it will stop capturing.
+ */
+ toggleWebrenderCaptureSequence() {
+ window.windowUtils.wrToggleCaptureSequence();
+ },
+};
diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc
new file mode 100644
index 0000000000..99a73e1632
--- /dev/null
+++ b/browser/base/content/browser-menubar.inc
@@ -0,0 +1,528 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ <menubar id="main-menubar"
+ onpopupshowing="if (event.target.parentNode.parentNode == this &amp;&amp;
+ !('@mozilla.org/widget/nativemenuservice;1' in Cc))
+ this.setAttribute('openedwithkey',
+ event.target.parentNode.openedWithKey);">
+ <menu id="file-menu" data-l10n-id="menu-file">
+ <menupopup id="menu_FilePopup"
+ onpopupshowing="updateFileMenuUserContextUIVisibility('menu_newUserContext');
+ updateImportCommandEnabledState();
+ PrintUtils.updatePrintPreviewMenuHiddenState();">
+ <menuitem id="menu_newNavigatorTab"
+ command="cmd_newNavigatorTab"
+ key="key_newNavigatorTab" data-l10n-id="menu-file-new-tab"/>
+ <menu id="menu_newUserContext"
+ hidden="true" data-l10n-id="menu-file-new-container-tab">
+ <menupopup onpopupshowing="return createUserContextMenu(event);" />
+ </menu>
+ <menuitem id="menu_newNavigator"
+ key="key_newNavigator"
+ command="cmd_newNavigator" data-l10n-id="menu-file-new-window"/>
+ <menuitem id="menu_newPrivateWindow"
+ command="Tools:PrivateBrowsing"
+ key="key_privatebrowsing" data-l10n-id="menu-file-new-private-window"/>
+#ifdef NIGHTLY_BUILD
+ <menuitem id="menu_newFissionWindow"
+ command="Tools:FissionWindow"
+ accesskey="s" label="New Fission Window"/>
+ <menuitem id="menu_newNonFissionWindow"
+ command="Tools:NonFissionWindow"
+ accesskey="s" label="New Non-Fission Window"/>
+#endif
+ <menuitem id="menu_openLocation"
+ hidden="true"
+ command="Browser:OpenLocation"
+ key="focusURLBar" data-l10n-id="menu-file-open-location"/>
+ <menuitem id="menu_openFile"
+ command="Browser:OpenFile"
+ key="openFileKb" data-l10n-id="menu-file-open-file"/>
+ <menuitem id="menu_close"
+ class="show-only-for-keyboard"
+ key="key_close"
+ command="cmd_close" data-l10n-id="menu-file-close"/>
+ <menuitem id="menu_closeWindow"
+ class="show-only-for-keyboard"
+ hidden="true"
+ command="cmd_closeWindow"
+ key="key_closeWindow" data-l10n-id="menu-file-close-window"/>
+ <menuseparator/>
+ <menuitem id="menu_savePage"
+ key="key_savePage"
+ command="Browser:SavePage" data-l10n-id="menu-file-save-page"/>
+ <menuitem id="menu_sendLink"
+ command="Browser:SendLink" data-l10n-id="menu-file-email-link"/>
+ <menuseparator/>
+#if !defined(MOZ_WIDGET_GTK)
+ <menuitem id="menu_printSetup"
+ command="cmd_pageSetup" data-l10n-id="menu-file-print-setup" hidden="true"/>
+#endif
+#ifndef XP_MACOSX
+ <menuitem id="menu_printPreview"
+ command="cmd_printPreview" data-l10n-id="menu-file-print-preview" hidden="true"/>
+#endif
+ <menuitem id="menu_print"
+ key="printKb"
+ command="cmd_print" data-l10n-id="menu-file-print"/>
+ <menuseparator/>
+ <menuitem id="menu_importFromAnotherBrowser"
+ command="cmd_file_importFromAnotherBrowser" data-l10n-id="menu-file-import-from-another-browser"/>
+ <menuseparator/>
+ <menuitem id="goOfflineMenuitem"
+ type="checkbox"
+ command="cmd_toggleOfflineStatus" data-l10n-id="menu-file-go-offline"/>
+ <menuitem id="menu_FileQuitItem"
+#ifdef XP_MACOSX
+ data-l10n-id="menu-quit-mac"
+#else
+ data-l10n-id="menu-quit"
+#endif
+ key="key_quitApplication"
+ command="cmd_quitApplication"/>
+ </menupopup>
+ </menu>
+
+ <menu id="edit-menu" data-l10n-id="menu-edit">
+ <menupopup id="menu_EditPopup"
+ onpopupshowing="updateEditUIVisibility()"
+ onpopuphidden="updateEditUIVisibility()">
+ <menuitem id="menu_undo"
+ key="key_undo"
+ command="cmd_undo" data-l10n-id="text-action-undo"/>
+ <menuitem id="menu_redo"
+ key="key_redo"
+ command="cmd_redo" data-l10n-id="text-action-redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"
+ key="key_cut"
+ command="cmd_cut" data-l10n-id="text-action-cut"/>
+ <menuitem id="menu_copy"
+ key="key_copy"
+ command="cmd_copy" data-l10n-id="text-action-copy"/>
+ <menuitem id="menu_paste"
+ key="key_paste"
+ command="cmd_paste" data-l10n-id="text-action-paste"/>
+ <menuitem id="menu_delete"
+ key="key_delete"
+ command="cmd_delete" data-l10n-id="text-action-delete"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"
+ key="key_selectAll"
+ command="cmd_selectAll" data-l10n-id="text-action-select-all"/>
+ <menuseparator/>
+ <menuitem id="menu_find"
+ key="key_find"
+ command="cmd_find" data-l10n-id="menu-edit-find-on"/>
+ <menuitem id="menu_findAgain"
+ class="show-only-for-keyboard"
+ key="key_findAgain"
+ command="cmd_findAgain" data-l10n-id="menu-edit-find-again"/>
+ <menuseparator hidden="true" id="textfieldDirection-separator"/>
+ <menuitem id="textfieldDirection-swap"
+ command="cmd_switchTextDirection"
+ key="key_switchTextDirection"
+ hidden="true" data-l10n-id="menu-edit-bidi-switch-text-direction"/>
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+ <menuseparator/>
+ <menuitem id="menu_preferences"
+ oncommand="openPreferences(undefined);" data-l10n-id="menu-preferences"/>
+#endif
+#endif
+ </menupopup>
+ </menu>
+
+ <menu id="view-menu" data-l10n-id="menu-view">
+ <menupopup id="menu_viewPopup"
+ onpopupshowing="updateCharacterEncodingMenuState();">
+ <menu id="viewToolbarsMenu" data-l10n-id="menu-view-toolbars-menu">
+ <menupopup id="view-menu-popup" onpopupshowing="onViewToolbarsPopupShowing(event);">
+ <menuseparator/>
+ <menuitem id="menu_customizeToolbars"
+ command="cmd_CustomizeToolbars" data-l10n-id="menu-view-customize-toolbar"/>
+ </menupopup>
+ </menu>
+ <menu id="viewSidebarMenuMenu" data-l10n-id="menu-view-sidebar">
+ <menupopup id="viewSidebarMenu">
+ <menuitem id="menu_bookmarksSidebar"
+ type="checkbox"
+ key="viewBookmarksSidebarKb"
+ oncommand="SidebarUI.toggle('viewBookmarksSidebar');" data-l10n-id="menu-view-bookmarks"/>
+ <menuitem id="menu_historySidebar"
+ type="checkbox"
+ key="key_gotoHistory"
+ oncommand="SidebarUI.toggle('viewHistorySidebar');" data-l10n-id="menu-view-history-button"/>
+ <menuitem id="menu_tabsSidebar"
+ type="checkbox"
+ class="sync-ui-item"
+ oncommand="SidebarUI.toggle('viewTabsSidebar');" data-l10n-id="menu-view-synced-tabs-sidebar"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menu id="viewFullZoomMenu"
+ onpopupshowing="FullZoom.updateMenu();" data-l10n-id="menu-view-full-zoom">
+ <menupopup>
+ <menuitem id="menu_zoomEnlarge"
+ key="key_fullZoomEnlarge"
+ command="cmd_fullZoomEnlarge" data-l10n-id="menu-view-full-zoom-enlarge"/>
+ <menuitem id="menu_zoomReduce"
+ key="key_fullZoomReduce"
+ command="cmd_fullZoomReduce" data-l10n-id="menu-view-full-zoom-reduce"/>
+ <menuseparator/>
+ <menuitem id="menu_zoomReset"
+ key="key_fullZoomReset"
+ command="cmd_fullZoomReset" data-l10n-id="menu-view-full-zoom-actual-size"/>
+ <menuseparator/>
+ <menuitem id="toggle_zoom"
+ type="checkbox"
+ command="cmd_fullZoomToggle"
+ checked="false" data-l10n-id="menu-view-full-zoom-toggle"/>
+ </menupopup>
+ </menu>
+ <menu id="pageStyleMenu" data-l10n-id="menu-view-page-style-menu">
+ <menupopup onpopupshowing="gPageStyleMenu.fillPopup(this);">
+ <menuitem id="menu_pageStyleNoStyle"
+ oncommand="gPageStyleMenu.disableStyle();"
+ type="radio" data-l10n-id="menu-view-page-style-no-style"/>
+ <menuitem id="menu_pageStylePersistentOnly"
+ oncommand="gPageStyleMenu.switchStyleSheet(null);"
+ type="radio"
+ checked="true" data-l10n-id="menu-view-page-basic-style"/>
+ <menuseparator/>
+ </menupopup>
+ </menu>
+ <menu id="charsetMenu"
+ oncommand="BrowserSetForcedCharacterSet(event.target.getAttribute('charset'));"
+ onpopupshowing="CharsetMenu.build(event.target); UpdateCurrentCharset(this);" data-l10n-id="menu-view-charset">
+ <menupopup>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+#ifdef XP_MACOSX
+ <menuitem id="enterFullScreenItem"
+ key="key_fullScreen" data-l10n-id="menu-view-enter-full-screen">
+ <observes element="View:FullScreen" attribute="oncommand"/>
+ <observes element="View:FullScreen" attribute="disabled"/>
+ </menuitem>
+ <menuitem id="exitFullScreenItem"
+ key="key_fullScreen"
+ hidden="true" data-l10n-id="menu-view-exit-full-screen">
+ <observes element="View:FullScreen" attribute="oncommand"/>
+ <observes element="View:FullScreen" attribute="disabled"/>
+ </menuitem>
+#else
+ <menuitem id="fullScreenItem"
+ key="key_fullScreen"
+ type="checkbox"
+ observes="View:FullScreen" data-l10n-id="menu-view-full-screen"/>
+#endif
+ <menuitem id="menu_readerModeItem"
+ observes="View:ReaderView"
+ key="key_toggleReaderMode"
+ hidden="true"/>
+ <menuitem id="menu_showAllTabs"
+ hidden="true"
+ command="Browser:ShowAllTabs"
+ key="key_showAllTabs" data-l10n-id="menu-view-show-all-tabs"/>
+ <menuseparator hidden="true" id="documentDirection-separator"/>
+ <menuitem id="documentDirection-swap"
+ hidden="true"
+ oncommand="gBrowser.selectedBrowser.sendMessageToActor('SwitchDocumentDirection', {}, 'SwitchDocumentDirection', 'roots');" data-l10n-id="menu-view-bidi-switch-page-direction"/>
+ </menupopup>
+ </menu>
+
+ <menu id="history-menu" data-l10n-id="menu-history">
+ <menupopup id="goPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+ is="places-popup"
+#endif
+ oncommand="this.parentNode._placesView._onCommand(event);"
+ onclick="checkForMiddleClick(this, event);"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new HistoryMenu(event);"
+ tooltip="bhTooltip"
+ popupsinherittooltip="true">
+ <menuitem id="menu_showAllHistory"
+ key="showAllHistoryKb"
+ command="Browser:ShowAllHistory" data-l10n-id="menu-history-show-all-history"/>
+ <menuitem id="sanitizeItem"
+ key="key_sanitize"
+ command="Tools:Sanitize" data-l10n-id="menu-history-clear-recent-history"/>
+ <menuseparator id="sanitizeSeparator"/>
+ <menuitem id="sync-tabs-menuitem"
+ oncommand="gSync.openSyncedTabsPanel();"
+ hidden="true" data-l10n-id="menu-history-synced-tabs"/>
+ <menuitem id="historyRestoreLastSession"
+ command="Browser:RestoreLastSession" data-l10n-id="menu-history-restore-last-session"/>
+ <menuitem id="hiddenTabsMenu"
+ oncommand="gTabsPanel.showHiddenTabsPanel(event);"
+ hidden="true" data-l10n-id="menu-history-hidden-tabs"/>
+ <menu id="historyUndoMenu"
+ disabled="true" data-l10n-id="menu-history-undo-menu">
+ <menupopup id="historyUndoPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+ is="places-popup"
+#endif
+ onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoSubmenu();"/>
+ </menu>
+ <menu id="historyUndoWindowMenu"
+ disabled="true" data-l10n-id="menu-history-undo-window-menu">
+ <menupopup id="historyUndoWindowPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+ is="places-popup"
+#endif
+ onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoWindowSubmenu();">
+#ifdef HIDDEN_WINDOW
+# This entry is never visible. It's here to make the cmd-shift-n
+# shortcut work in the hidden window when the last window is closed.
+# If the menu is actually opened, we'll clear this out and replace
+# it with a "real" entry.
+# See bug 492320 for the nasty details.
+ <menuitem key="key_undoCloseWindow"
+ oncommand="undoCloseWindow(0)"
+ />
+#endif
+ </menupopup>
+ </menu>
+ <menuseparator id="startHistorySeparator"
+ class="hide-if-empty-places-result"/>
+ </menupopup>
+ </menu>
+
+ <menu id="bookmarksMenu"
+ ondragenter="PlacesMenuDNDHandler.onDragEnter(event);"
+ ondragover="PlacesMenuDNDHandler.onDragOver(event);"
+ ondrop="PlacesMenuDNDHandler.onDrop(event);"
+ data-l10n-id="menu-bookmarks-menu">
+ <menupopup id="bookmarksMenuPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+ is="places-popup"
+#endif
+ context="placesContext"
+ openInTabs="children"
+ onmouseup="BookmarksEventHandler.onMouseUp(event);"
+ oncommand="BookmarksEventHandler.onCommand(event);"
+ onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);"
+ onpopupshowing="BookmarkingUI.onMainMenuPopupShowing(event);
+ if (!this.parentNode._placesView)
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.menuGuid}`);"
+ tooltip="bhTooltip" popupsinherittooltip="true">
+ <menuitem id="bookmarksShowAll"
+ command="Browser:ShowAllBookmarks"
+ key="manBookmarkKb"
+ data-l10n-id="menu-bookmarks-show-all"/>
+ <menuseparator id="organizeBookmarksSeparator"/>
+ <menuitem id="menu_bookmarkThisPage"
+ command="Browser:AddBookmarkAs"
+ key="addBookmarkAsKb"
+ data-l10n-id="menu-bookmark-this-page"/>
+ <menuitem id="menu_bookmarkAllTabs"
+ class="show-only-for-keyboard"
+ command="Browser:BookmarkAllTabs"
+ key="bookmarkAllTabsKb"
+ data-l10n-id="menu-bookmarks-all-tabs"/>
+ <menuseparator id="bookmarksToolbarSeparator"/>
+ <menu id="bookmarksToolbarFolderMenu"
+ class="menu-iconic bookmark-item"
+ container="true"
+ data-l10n-id="menu-bookmarks-toolbar">
+ <menupopup id="bookmarksToolbarFolderPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+ is="places-popup"
+#endif
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`);"/>
+ </menu>
+ <menu id="menu_unsortedBookmarks"
+ class="menu-iconic bookmark-item"
+ container="true"
+ data-l10n-id="menu-bookmarks-other">
+ <menupopup id="otherBookmarksFolderPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+ is="places-popup"
+#endif
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`);"/>
+ </menu>
+ <menu id="menu_mobileBookmarks"
+ class="menu-iconic bookmark-item"
+ hidden="true"
+ container="true"
+ data-l10n-id="menu-bookmarks-mobile">
+ <menupopup id="mobileBookmarksFolderPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+ is="places-popup"
+#endif
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.mobileGuid}`);"/>
+ </menu>
+ <menuseparator id="bookmarksMenuItemsSeparator"/>
+ <!-- Bookmarks menu items -->
+ </menupopup>
+ </menu>
+
+ <menu id="tools-menu" data-l10n-id="menu-tools">
+ <menupopup id="menu_ToolsPopup">
+ <menuitem id="menu_openDownloads"
+ key="key_openDownloads"
+ command="Tools:Downloads" data-l10n-id="menu-tools-downloads"/>
+ <menuitem id="menu_openAddons"
+ key="key_openAddons"
+ command="Tools:Addons" data-l10n-id="menu-tools-addons"/>
+
+ <!-- only one of sync-setup, sync-enable, sync-unverifieditem, sync-syncnowitem or sync-reauthitem will be showing at once -->
+ <menuitem id="sync-setup"
+ class="sync-ui-item"
+ hidden="true"
+ oncommand="gSync.openPrefs('menubar')" data-l10n-id="menu-tools-fxa-sign-in"/>
+ <menuitem id="sync-enable"
+ class="sync-ui-item"
+ hidden="true"
+ oncommand="gSync.openPrefs('menubar')" data-l10n-id="menu-tools-turn-on-sync"/>
+ <menuitem id="sync-unverifieditem"
+ class="sync-ui-item"
+ hidden="true"
+ oncommand="gSync.openPrefs('menubar')" data-l10n-id="menu-tools-fxa-sign-in"/>
+ <menuitem id="sync-syncnowitem"
+ class="sync-ui-item"
+ hidden="true"
+ oncommand="gSync.doSync(event);" data-l10n-id="menu-tools-sync-now"/>
+ <menuitem id="sync-reauthitem"
+ class="sync-ui-item"
+ hidden="true"
+ oncommand="gSync.openSignInAgainPage('menubar');" data-l10n-id="menu-tools-fxa-re-auth"/>
+ <menuseparator id="devToolsSeparator"/>
+ <menu id="webDeveloperMenu" data-l10n-id="menu-tools-web-developer">
+ <menupopup id="menuWebDeveloperPopup">
+ <menuitem id="menu_pageSource"
+ key="key_viewSource"
+ command="View:PageSource" data-l10n-id="menu-tools-page-source"/>
+ </menupopup>
+ </menu>
+ <menuitem id="menu_pageInfo"
+ key="key_viewInfo"
+ command="View:PageInfo" data-l10n-id="menu-tools-page-info"/>
+#ifndef XP_UNIX
+ <menuseparator id="prefSep"/>
+ <menuitem id="menu_preferences"
+ oncommand="openPreferences(undefined);" data-l10n-id="menu-preferences"/>
+#endif
+#ifdef MOZ_DEBUG
+ <menuitem id="menu_layout_debugger"
+ data-l10n-id="menu-tools-layout-debugger"
+ oncommand="toOpenWindowByType('mozapp:layoutdebug',
+ 'chrome://layoutdebug/content/layoutdebug.xhtml');"/>
+#endif
+#ifdef XP_MACOSX
+<!-- nsMenuBarX hides these and uses them to build the Application menu. -->
+ <menuitem id="menu_preferences" data-l10n-id="menu-preferences" key="key_preferencesCmdMac" oncommand="openPreferences(undefined);"/>
+ <menuitem id="menu_mac_services" data-l10n-id="menu-application-services"/>
+ <menuitem id="menu_mac_hide_app" data-l10n-id="menu-application-hide-this" key="key_hideThisAppCmdMac"/>
+ <menuitem id="menu_mac_hide_others" data-l10n-id="menu-application-hide-other" key="key_hideOtherAppsCmdMac"/>
+ <menuitem id="menu_mac_show_all" data-l10n-id="menu-application-show-all"/>
+ <menuitem id="menu_mac_touch_bar" data-l10n-id="menu-application-touch-bar"/>
+#endif
+ </menupopup>
+ </menu>
+#ifdef XP_MACOSX
+ <menu id="windowMenu"
+ onpopupshowing="macWindowMenuDidShow();"
+ onpopuphidden="macWindowMenuDidHide();"
+ data-l10n-id="menu-window-menu">
+ <menupopup id="windowPopup">
+ <menuitem command="minimizeWindow" key="key_minimizeWindow"/>
+ <menuitem command="zoomWindow"/>
+ <!-- decomment when "BringAllToFront" is implemented
+ <menuseparator/>
+ <menuitem disabled="true" data-l10n-id="menu-window-bring-all-to-front"/> -->
+ <menuseparator id="sep-window-list"/>
+ </menupopup>
+ </menu>
+#endif
+ <menu id="helpMenu"
+#ifdef XP_WIN
+#else
+#endif
+ data-l10n-id="menu-help">
+ <menupopup id="menu_HelpPopup" onpopupshowing="buildHelpMenu();">
+<!-- Note: Items under here are cloned to the AppMenu Help submenu. The cloned items
+ have their strings defined by appmenu-data-l10n-id. -->
+ <menuitem id="menu_openHelp"
+ oncommand="openHelpLink('firefox-help')"
+ onclick="checkForMiddleClick(this, event);"
+ data-l10n-id="menu-help-product"
+ appmenu-data-l10n-id="appmenu-help-product"
+#ifdef XP_MACOSX
+ key="key_openHelpMac"/>
+#else
+ />
+#endif
+ <menuitem id="menu_openTour"
+ oncommand="openTourPage();"
+ data-l10n-id="menu-help-show-tour"
+ appmenu-data-l10n-id="appmenu-help-show-tour"/>
+ <menuitem id="help_importFromAnotherBrowser"
+ command="cmd_help_importFromAnotherBrowser"
+ data-l10n-id="menu-help-import-from-another-browser"
+ appmenu-data-l10n-id="appmenu-help-import-from-another-browser"/>
+ <menuitem id="menu_keyboardShortcuts"
+ oncommand="openHelpLink('keyboard-shortcuts')"
+ onclick="checkForMiddleClick(this, event);"
+ data-l10n-id="menu-help-keyboard-shortcuts"
+ appmenu-data-l10n-id="appmenu-help-keyboard-shortcuts"/>
+ <menuitem id="troubleShooting"
+ oncommand="openTroubleshootingPage()"
+ onclick="checkForMiddleClick(this, event);"
+ data-l10n-id="menu-help-troubleshooting-info"
+ appmenu-data-l10n-id="appmenu-help-troubleshooting-info"/>
+ <menuitem id="feedbackPage"
+ oncommand="openFeedbackPage()"
+ onclick="checkForMiddleClick(this, event);"
+ data-l10n-id="menu-help-feedback-page"
+ appmenu-data-l10n-id="appmenu-help-feedback-page"/>
+ <menuitem id="helpSafeMode"
+ oncommand="safeModeRestart();"
+ data-l10n-id="menu-help-safe-mode-without-addons"
+ appmenu-data-l10n-id="appmenu-help-safe-mode-without-addons"/>
+ <menuitem id="menu_HelpPopup_reportPhishingtoolmenu"
+ disabled="true"
+ oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event, {triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})});"
+ onclick="checkForMiddleClick(this, event);"
+ hidden="true"
+ data-l10n-id="menu-help-report-deceptive-site"
+ appmenu-data-l10n-id="appmenu-help-report-deceptive-site"/>
+ <menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu"
+ disabled="true"
+ oncommand="ReportFalseDeceptiveSite();"
+ onclick="checkForMiddleClick(this, event);"
+ data-l10n-id="menu-help-not-deceptive"
+ appmenu-data-l10n-id="appmenu-help-not-deceptive"
+ hidden="true"/>
+ <menuseparator id="helpPolicySeparator"
+ hidden="true"/>
+ <menuitem id="helpPolicySupport"
+ hidden="true"
+ oncommand="openUILinkIn(Services.policies.getSupportMenu().URL.href, 'tab', {triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})});"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuseparator id="aboutSeparator"/>
+ <menuitem id="aboutName"
+ oncommand="openAboutDialog();"
+ data-l10n-id="menu-about"
+ appmenu-data-l10n-id="appmenu-about"/>
+ </menupopup>
+ </menu>
+ </menubar>
diff --git a/browser/base/content/browser-pageActions.js b/browser/base/content/browser-pageActions.js
new file mode 100644
index 0000000000..8c896ba3b9
--- /dev/null
+++ b/browser/base/content/browser-pageActions.js
@@ -0,0 +1,1390 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.defineModuleGetter(
+ this,
+ "SearchUIUtils",
+ "resource:///modules/SearchUIUtils.jsm"
+);
+
+var BrowserPageActions = {
+ _panelNode: null,
+ /**
+ * The main page action button in the urlbar (DOM node)
+ */
+ get mainButtonNode() {
+ delete this.mainButtonNode;
+ return (this.mainButtonNode = document.getElementById("pageActionButton"));
+ },
+
+ /**
+ * The main page action panel DOM node (DOM node)
+ */
+ get panelNode() {
+ // Lazy load the page action panel the first time we need to display it
+ if (!this._panelNode) {
+ this.initializePanel();
+ }
+ delete this.panelNode;
+ return (this.panelNode = this._panelNode);
+ },
+
+ /**
+ * The panelmultiview node in the main page action panel (DOM node)
+ */
+ get multiViewNode() {
+ delete this.multiViewNode;
+ return (this.multiViewNode = document.getElementById(
+ "pageActionPanelMultiView"
+ ));
+ },
+
+ /**
+ * The main panelview node in the main page action panel (DOM node)
+ */
+ get mainViewNode() {
+ delete this.mainViewNode;
+ return (this.mainViewNode = document.getElementById(
+ "pageActionPanelMainView"
+ ));
+ },
+
+ /**
+ * The vbox body node in the main panelview node (DOM node)
+ */
+ get mainViewBodyNode() {
+ delete this.mainViewBodyNode;
+ return (this.mainViewBodyNode = this.mainViewNode.querySelector(
+ ".panel-subview-body"
+ ));
+ },
+
+ /**
+ * Inits. Call to init.
+ */
+ init() {
+ this.placeAllActionsInUrlbar();
+ this._onPanelShowing = this._onPanelShowing.bind(this);
+ },
+
+ _onPanelShowing() {
+ this.initializePanel();
+ for (let action of PageActions.actionsInPanel(window)) {
+ let buttonNode = this.panelButtonNodeForActionID(action.id);
+ action.onShowingInPanel(buttonNode);
+ }
+ },
+
+ placeLazyActionsInPanel() {
+ let actions = this._actionsToLazilyPlaceInPanel;
+ this._actionsToLazilyPlaceInPanel = [];
+ for (let action of actions) {
+ this._placeActionInPanelNow(action);
+ }
+ },
+
+ // Actions placed in the panel aren't actually placed until the panel is
+ // subsequently opened.
+ _actionsToLazilyPlaceInPanel: [],
+
+ /**
+ * Places all registered actions in the urlbar.
+ */
+ placeAllActionsInUrlbar() {
+ let urlbarActions = PageActions.actionsInUrlbar(window);
+ for (let action of urlbarActions) {
+ this.placeActionInUrlbar(action);
+ }
+ },
+
+ /**
+ * Initializes the panel if necessary.
+ */
+ initializePanel() {
+ // Lazy load the page action panel the first time we need to display it
+ if (!this._panelNode) {
+ let template = document.getElementById("pageActionPanelTemplate");
+ template.replaceWith(template.content);
+ this._panelNode = document.getElementById("pageActionPanel");
+ this._panelNode.addEventListener("popupshowing", this._onPanelShowing);
+ this._panelNode.addEventListener("popuphiding", () => {
+ this.mainButtonNode.removeAttribute("open");
+ });
+ }
+
+ for (let action of PageActions.actionsInPanel(window)) {
+ this.placeActionInPanel(action);
+ }
+ this.placeLazyActionsInPanel();
+ },
+
+ /**
+ * Adds or removes as necessary DOM nodes for the given action.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to place.
+ */
+ placeAction(action) {
+ this.placeActionInPanel(action);
+ this.placeActionInUrlbar(action);
+ },
+
+ /**
+ * Adds or removes as necessary DOM nodes for the action in the panel.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to place.
+ */
+ placeActionInPanel(action) {
+ if (this._panelNode && this.panelNode.state != "closed") {
+ this._placeActionInPanelNow(action);
+ } else {
+ // Lazily place the action in the panel the next time it opens.
+ this._actionsToLazilyPlaceInPanel.push(action);
+ }
+ },
+
+ _placeActionInPanelNow(action) {
+ if (action.shouldShowInPanel(window)) {
+ this._addActionToPanel(action);
+ } else {
+ this._removeActionFromPanel(action);
+ }
+ },
+
+ _addActionToPanel(action) {
+ let id = this.panelButtonNodeIDForActionID(action.id);
+ let node = document.getElementById(id);
+ if (node) {
+ return;
+ }
+ this._maybeNotifyBeforePlacedInWindow(action);
+ node = this._makePanelButtonNodeForAction(action);
+ node.id = id;
+ let insertBeforeNode = this._getNextNode(action, false);
+ this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
+ this.updateAction(action, null, {
+ panelNode: node,
+ });
+ this._updateActionDisabledInPanel(action, node);
+ action.onPlacedInPanel(node);
+ this._addOrRemoveSeparatorsInPanel();
+ },
+
+ _removeActionFromPanel(action) {
+ let lazyIndex = this._actionsToLazilyPlaceInPanel.findIndex(
+ a => a.id == action.id
+ );
+ if (lazyIndex >= 0) {
+ this._actionsToLazilyPlaceInPanel.splice(lazyIndex, 1);
+ }
+ let node = this.panelButtonNodeForActionID(action.id);
+ if (!node) {
+ return;
+ }
+ node.remove();
+ if (action.getWantsSubview(window)) {
+ let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
+ let panelViewNode = document.getElementById(panelViewNodeID);
+ if (panelViewNode) {
+ panelViewNode.remove();
+ }
+ }
+ this._addOrRemoveSeparatorsInPanel();
+ },
+
+ _addOrRemoveSeparatorsInPanel() {
+ let actions = PageActions.actionsInPanel(window);
+ let ids = [
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+ ];
+ for (let id of ids) {
+ let sep = actions.find(a => a.id == id);
+ if (sep) {
+ this._addActionToPanel(sep);
+ } else {
+ let node = this.panelButtonNodeForActionID(id);
+ if (node) {
+ node.remove();
+ }
+ }
+ }
+ },
+
+ /**
+ * Returns the node before which an action's node should be inserted.
+ *
+ * @param action (PageActions.Action, required)
+ * The action that will be inserted.
+ * @param forUrlbar (bool, required)
+ * True if you're inserting into the urlbar, false if you're inserting
+ * into the panel.
+ * @return (DOM node, maybe null) The DOM node before which to insert the
+ * given action. Null if the action should be inserted at the end.
+ */
+ _getNextNode(action, forUrlbar) {
+ let actions = forUrlbar
+ ? PageActions.actionsInUrlbar(window)
+ : PageActions.actionsInPanel(window);
+ let index = actions.findIndex(a => a.id == action.id);
+ if (index < 0) {
+ return null;
+ }
+ for (let i = index + 1; i < actions.length; i++) {
+ let node = forUrlbar
+ ? this.urlbarButtonNodeForActionID(actions[i].id)
+ : this.panelButtonNodeForActionID(actions[i].id);
+ if (node) {
+ return node;
+ }
+ }
+ return null;
+ },
+
+ _maybeNotifyBeforePlacedInWindow(action) {
+ if (!this._isActionPlacedInWindow(action)) {
+ action.onBeforePlacedInWindow(window);
+ }
+ },
+
+ _isActionPlacedInWindow(action) {
+ if (this.panelButtonNodeForActionID(action.id)) {
+ return true;
+ }
+ let urlbarNode = this.urlbarButtonNodeForActionID(action.id);
+ return urlbarNode && !urlbarNode.hidden;
+ },
+
+ _makePanelButtonNodeForAction(action) {
+ if (action.__isSeparator) {
+ let node = document.createXULElement("toolbarseparator");
+ return node;
+ }
+ let buttonNode = document.createXULElement("toolbarbutton");
+ buttonNode.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "pageAction-panel-button"
+ );
+ if (action.isBadged) {
+ buttonNode.setAttribute("badged", "true");
+ }
+ buttonNode.setAttribute("actionid", action.id);
+ buttonNode.addEventListener("command", event => {
+ this.doCommandForAction(action, event, buttonNode);
+ });
+ return buttonNode;
+ },
+
+ _makePanelViewNodeForAction(action, forUrlbar) {
+ let panelViewNode = document.createXULElement("panelview");
+ panelViewNode.id = this._panelViewNodeIDForActionID(action.id, forUrlbar);
+ panelViewNode.classList.add("PanelUI-subView");
+ let bodyNode = document.createXULElement("vbox");
+ bodyNode.id = panelViewNode.id + "-body";
+ bodyNode.classList.add("panel-subview-body");
+ panelViewNode.appendChild(bodyNode);
+ return panelViewNode;
+ },
+
+ /**
+ * Shows or hides a panel for an action. You can supply your own panel;
+ * otherwise one is created.
+ *
+ * @param action (PageActions.Action, required)
+ * The action for which to toggle the panel. If the action is in the
+ * urlbar, then the panel will be anchored to it. Otherwise, a
+ * suitable anchor will be used.
+ * @param panelNode (DOM node, optional)
+ * The panel to use. This method takes a hands-off approach with
+ * regard to your panel in terms of attributes, styling, etc.
+ * @param event (DOM event, optional)
+ * The event which triggered this panel.
+ */
+ togglePanelForAction(action, panelNode = null, event = null) {
+ let aaPanelNode = this.activatedActionPanelNode;
+ if (panelNode) {
+ // Note that this particular code path will not prevent the panel from
+ // opening later if PanelMultiView.showPopup was called but the panel has
+ // not been opened yet.
+ if (panelNode.state != "closed") {
+ PanelMultiView.hidePopup(panelNode);
+ return;
+ }
+ if (aaPanelNode) {
+ PanelMultiView.hidePopup(aaPanelNode);
+ }
+ } else if (aaPanelNode) {
+ PanelMultiView.hidePopup(aaPanelNode);
+ return;
+ } else {
+ panelNode = this._makeActivatedActionPanelForAction(action);
+ }
+
+ // Hide the main panel before showing the action's panel.
+ PanelMultiView.hidePopup(this.panelNode);
+
+ let anchorNode = this.panelAnchorNodeForAction(action);
+ anchorNode.setAttribute("open", "true");
+ panelNode.addEventListener(
+ "popuphiding",
+ () => {
+ anchorNode.removeAttribute("open");
+ },
+ { once: true }
+ );
+
+ PanelMultiView.openPopup(panelNode, anchorNode, {
+ position: "bottomcenter topright",
+ triggerEvent: event,
+ }).catch(Cu.reportError);
+ },
+
+ _makeActivatedActionPanelForAction(action) {
+ let panelNode = document.createXULElement("panel");
+ panelNode.id = this._activatedActionPanelID;
+ panelNode.classList.add("cui-widget-panel", "panel-no-padding");
+ panelNode.setAttribute("actionID", action.id);
+ panelNode.setAttribute("role", "group");
+ panelNode.setAttribute("type", "arrow");
+ panelNode.setAttribute("flip", "slide");
+ panelNode.setAttribute("noautofocus", "true");
+ panelNode.setAttribute("tabspecific", "true");
+
+ let panelViewNode = null;
+ let iframeNode = null;
+
+ if (action.getWantsSubview(window)) {
+ let multiViewNode = document.createXULElement("panelmultiview");
+ panelViewNode = this._makePanelViewNodeForAction(action, true);
+ multiViewNode.setAttribute("mainViewId", panelViewNode.id);
+ multiViewNode.appendChild(panelViewNode);
+ panelNode.appendChild(multiViewNode);
+ } else if (action.wantsIframe) {
+ iframeNode = document.createXULElement("iframe");
+ iframeNode.setAttribute("type", "content");
+ panelNode.appendChild(iframeNode);
+ }
+
+ let popupSet = document.getElementById("mainPopupSet");
+ popupSet.appendChild(panelNode);
+ panelNode.addEventListener(
+ "popuphidden",
+ () => {
+ PanelMultiView.removePopup(panelNode);
+ },
+ { once: true }
+ );
+
+ if (iframeNode) {
+ panelNode.addEventListener(
+ "popupshowing",
+ () => {
+ action.onIframeShowing(iframeNode, panelNode);
+ },
+ { once: true }
+ );
+ panelNode.addEventListener(
+ "popupshown",
+ () => {
+ iframeNode.focus();
+ },
+ { once: true }
+ );
+ panelNode.addEventListener(
+ "popuphiding",
+ () => {
+ action.onIframeHiding(iframeNode, panelNode);
+ },
+ { once: true }
+ );
+ panelNode.addEventListener(
+ "popuphidden",
+ () => {
+ action.onIframeHidden(iframeNode, panelNode);
+ },
+ { once: true }
+ );
+ }
+
+ if (panelViewNode) {
+ action.onSubviewPlaced(panelViewNode);
+ panelNode.addEventListener(
+ "popupshowing",
+ () => {
+ action.onSubviewShowing(panelViewNode);
+ },
+ { once: true }
+ );
+ }
+
+ return panelNode;
+ },
+
+ /**
+ * Returns the node in the urlbar to which popups for the given action should
+ * be anchored. If the action is null, a sensible anchor is returned.
+ *
+ * @param action (PageActions.Action, optional)
+ * The action you want to anchor.
+ * @param event (DOM event, optional)
+ * This is used to display the feedback panel on the right node when
+ * the command can be invoked from both the main panel and another
+ * location, such as an activated action panel or a button.
+ * @return (DOM node) The node to which the action should be anchored.
+ */
+ panelAnchorNodeForAction(action, event) {
+ if (event && event.target.closest("panel") == this.panelNode) {
+ return this.mainButtonNode;
+ }
+
+ // Try each of the following nodes in order, using the first that's visible.
+ let potentialAnchorNodeIDs = [
+ action && action.anchorIDOverride,
+ action && this.urlbarButtonNodeIDForActionID(action.id),
+ this.mainButtonNode.id,
+ "identity-icon",
+ "urlbar-search-button",
+ ];
+ for (let id of potentialAnchorNodeIDs) {
+ if (id) {
+ let node = document.getElementById(id);
+ if (node && !node.hidden) {
+ let bounds = window.windowUtils.getBoundsWithoutFlushing(node);
+ if (bounds.height > 0 && bounds.width > 0) {
+ return node;
+ }
+ }
+ }
+ }
+ let id = action ? action.id : "<no action>";
+ throw new Error(`PageActions: No anchor node for ${id}`);
+ },
+
+ get activatedActionPanelNode() {
+ return document.getElementById(this._activatedActionPanelID);
+ },
+
+ get _activatedActionPanelID() {
+ return "pageActionActivatedActionPanel";
+ },
+
+ /**
+ * Adds or removes as necessary a DOM node for the given action in the urlbar.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to place.
+ */
+ placeActionInUrlbar(action) {
+ let id = this.urlbarButtonNodeIDForActionID(action.id);
+ let node = document.getElementById(id);
+
+ if (!action.shouldShowInUrlbar(window)) {
+ if (node) {
+ if (action.__urlbarNodeInMarkup) {
+ node.hidden = true;
+ } else {
+ node.remove();
+ }
+ }
+ return;
+ }
+
+ let newlyPlaced = false;
+ if (action.__urlbarNodeInMarkup) {
+ this._maybeNotifyBeforePlacedInWindow(action);
+ // Allow the consumer to add the node in response to the
+ // onBeforePlacedInWindow notification.
+ node = document.getElementById(id);
+ if (!node) {
+ return;
+ }
+ newlyPlaced = node.hidden;
+ node.hidden = false;
+ } else if (!node) {
+ newlyPlaced = true;
+ this._maybeNotifyBeforePlacedInWindow(action);
+ node = this._makeUrlbarButtonNode(action);
+ node.id = id;
+ }
+
+ if (!newlyPlaced) {
+ return;
+ }
+
+ let insertBeforeNode = this._getNextNode(action, true);
+ this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
+ this.updateAction(action, null, {
+ urlbarNode: node,
+ });
+ action.onPlacedInUrlbar(node);
+ },
+
+ _makeUrlbarButtonNode(action) {
+ let buttonNode = document.createXULElement("image");
+ buttonNode.classList.add("urlbar-icon", "urlbar-page-action");
+ buttonNode.setAttribute("actionid", action.id);
+ buttonNode.setAttribute("role", "button");
+ let commandHandler = event => {
+ this.doCommandForAction(action, event, buttonNode);
+ };
+ buttonNode.addEventListener("click", commandHandler);
+ buttonNode.addEventListener("keypress", commandHandler);
+ return buttonNode;
+ },
+
+ /**
+ * Removes all the DOM nodes of the given action.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to remove.
+ */
+ removeAction(action) {
+ this._removeActionFromPanel(action);
+ this._removeActionFromUrlbar(action);
+ action.onRemovedFromWindow(window);
+ },
+
+ _removeActionFromUrlbar(action) {
+ let node = this.urlbarButtonNodeForActionID(action.id);
+ if (node) {
+ node.remove();
+ }
+ },
+
+ /**
+ * Updates the DOM nodes of an action to reflect either a changed property or
+ * all properties.
+ *
+ * @param action (PageActions.Action, required)
+ * The action to update.
+ * @param propertyName (string, optional)
+ * The name of the property to update. If not given, then DOM nodes
+ * will be updated to reflect the current values of all properties.
+ * @param opts (object, optional)
+ * - panelNode: The action's node in the panel to update.
+ * - urlbarNode: The action's node in the urlbar to update.
+ * - value: If a property name is passed, this argument may contain
+ * its current value, in order to prevent a further look-up.
+ */
+ updateAction(action, propertyName = null, opts = {}) {
+ let anyNodeGiven = "panelNode" in opts || "urlbarNode" in opts;
+ let panelNode = anyNodeGiven
+ ? opts.panelNode || null
+ : this.panelButtonNodeForActionID(action.id);
+ let urlbarNode = anyNodeGiven
+ ? opts.urlbarNode || null
+ : this.urlbarButtonNodeForActionID(action.id);
+ let value = opts.value || undefined;
+ if (propertyName) {
+ this[this._updateMethods[propertyName]](
+ action,
+ panelNode,
+ urlbarNode,
+ value
+ );
+ } else {
+ for (let name of ["iconURL", "title", "tooltip", "wantsSubview"]) {
+ this[this._updateMethods[name]](action, panelNode, urlbarNode, value);
+ }
+ }
+ },
+
+ _updateMethods: {
+ disabled: "_updateActionDisabled",
+ iconURL: "_updateActionIconURL",
+ title: "_updateActionLabeling",
+ tooltip: "_updateActionTooltip",
+ wantsSubview: "_updateActionWantsSubview",
+ },
+
+ _updateActionDisabled(
+ action,
+ panelNode,
+ urlbarNode,
+ disabled = action.getDisabled(window)
+ ) {
+ if (action.__transient) {
+ this.placeActionInPanel(action);
+ } else {
+ this._updateActionDisabledInPanel(action, panelNode, disabled);
+ }
+ this.placeActionInUrlbar(action);
+ },
+
+ _updateActionDisabledInPanel(
+ action,
+ panelNode,
+ disabled = action.getDisabled(window)
+ ) {
+ if (panelNode) {
+ if (disabled) {
+ panelNode.setAttribute("disabled", "true");
+ } else {
+ panelNode.removeAttribute("disabled");
+ }
+ }
+ },
+
+ _updateActionIconURL(
+ action,
+ panelNode,
+ urlbarNode,
+ properties = action.getIconProperties(window)
+ ) {
+ for (let [prop, value] of Object.entries(properties)) {
+ if (panelNode) {
+ panelNode.style.setProperty(prop, value);
+ }
+ if (urlbarNode) {
+ urlbarNode.style.setProperty(prop, value);
+ }
+ }
+ },
+
+ _updateActionLabeling(
+ action,
+ panelNode,
+ urlbarNode,
+ title = action.getTitle(window)
+ ) {
+ let tabCount = gBrowser.selectedTabs.length;
+ if (panelNode) {
+ if (action.panelFluentID) {
+ document.l10n.setAttributes(panelNode, action.panelFluentID, {
+ tabCount,
+ });
+ } else {
+ panelNode.setAttribute("label", title);
+ }
+ }
+ if (urlbarNode) {
+ // Some actions (e.g. Save Page to Pocket) have a wrapper node with the
+ // actual controls inside that wrapper. The wrapper is semantically
+ // meaningless, so it doesn't get reflected in the accessibility tree.
+ // In these cases, we don't want to set aria-label because that will
+ // force the element to be exposed to accessibility.
+ if (urlbarNode.nodeName != "hbox") {
+ urlbarNode.setAttribute("aria-label", title);
+ }
+ // tooltiptext falls back to the title, so update it too if necessary.
+ let tooltip = action.getTooltip(window);
+ if (!tooltip) {
+ if (action.urlbarFluentID) {
+ document.l10n.setAttributes(urlbarNode, action.urlbarFluentID, {
+ tabCount,
+ });
+ } else {
+ urlbarNode.setAttribute("tooltiptext", title);
+ }
+ }
+ }
+ },
+
+ _updateActionTooltip(
+ action,
+ panelNode,
+ urlbarNode,
+ tooltip = action.getTooltip(window)
+ ) {
+ if (urlbarNode) {
+ if (!tooltip) {
+ tooltip = action.getTitle(window);
+ }
+ if (tooltip) {
+ urlbarNode.setAttribute("tooltiptext", tooltip);
+ }
+ }
+ },
+
+ _updateActionWantsSubview(
+ action,
+ panelNode,
+ urlbarNode,
+ wantsSubview = action.getWantsSubview(window)
+ ) {
+ if (!panelNode) {
+ return;
+ }
+ let panelViewID = this._panelViewNodeIDForActionID(action.id, false);
+ let panelViewNode = document.getElementById(panelViewID);
+ panelNode.classList.toggle("subviewbutton-nav", wantsSubview);
+ if (!wantsSubview) {
+ if (panelViewNode) {
+ panelViewNode.remove();
+ }
+ return;
+ }
+ if (!panelViewNode) {
+ panelViewNode = this._makePanelViewNodeForAction(action, false);
+ this.multiViewNode.appendChild(panelViewNode);
+ action.onSubviewPlaced(panelViewNode);
+ }
+ },
+
+ doCommandForAction(action, event, buttonNode) {
+ // On mac, ctrl-click will send a context menu event from the widget, so we
+ // don't want to handle the click event when ctrl key is pressed.
+ if (
+ event &&
+ event.type == "click" &&
+ (event.button != 0 ||
+ (AppConstants.platform == "macosx" && event.ctrlKey))
+ ) {
+ return;
+ }
+ if (event && event.type == "keypress") {
+ if (event.key != " " && event.key != "Enter") {
+ return;
+ }
+ event.stopPropagation();
+ }
+ // If we're in the panel, open a subview inside the panel:
+ // Note that we can't use this.panelNode.contains(buttonNode) here
+ // because of XBL boundaries breaking Element.contains.
+ if (
+ action.getWantsSubview(window) &&
+ buttonNode &&
+ buttonNode.closest("panel") == this.panelNode
+ ) {
+ let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
+ let panelViewNode = document.getElementById(panelViewNodeID);
+ action.onSubviewShowing(panelViewNode);
+ this.multiViewNode.showSubView(panelViewNode, buttonNode);
+ return;
+ }
+ // Otherwise, hide the main popup in case it was open:
+ PanelMultiView.hidePopup(this.panelNode);
+
+ let aaPanelNode = this.activatedActionPanelNode;
+ if (!aaPanelNode || aaPanelNode.getAttribute("actionID") != action.id) {
+ action.onCommand(event, buttonNode);
+ }
+ if (action.getWantsSubview(window) || action.wantsIframe) {
+ this.togglePanelForAction(action, null, event);
+ }
+ },
+
+ /**
+ * Returns the action for a node.
+ *
+ * @param node (DOM node, required)
+ * A button DOM node, either one that's shown in the page action panel
+ * or the urlbar.
+ * @return (PageAction.Action) If the node has a related action and the action
+ * is not a separator, then the action is returned. Otherwise null is
+ * returned.
+ */
+ actionForNode(node) {
+ if (!node) {
+ return null;
+ }
+ let actionID = this._actionIDForNodeID(node.id);
+ let action = PageActions.actionForID(actionID);
+ if (!action) {
+ // The given node may be an ancestor of a node corresponding to an action,
+ // like how #star-button is contained in #star-button-box, the latter
+ // being the bookmark action's node. Look up the ancestor chain.
+ for (let n = node.parentNode; n && !action; n = n.parentNode) {
+ if (n.id == "page-action-buttons" || n.localName == "panelview") {
+ // We reached the page-action-buttons or panelview container.
+ // Stop looking; no acton was found.
+ break;
+ }
+ actionID = this._actionIDForNodeID(n.id);
+ action = PageActions.actionForID(actionID);
+ }
+ }
+ return action && !action.__isSeparator ? action : null;
+ },
+
+ /**
+ * The given action's top-level button in the main panel.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (DOM node) The action's button in the main panel.
+ */
+ panelButtonNodeForActionID(actionID) {
+ return document.getElementById(this.panelButtonNodeIDForActionID(actionID));
+ },
+
+ /**
+ * The ID of the given action's top-level button in the main panel.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (string) The ID of the action's button in the main panel.
+ */
+ panelButtonNodeIDForActionID(actionID) {
+ return `pageAction-panel-${actionID}`;
+ },
+
+ /**
+ * The given action's button in the urlbar.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (DOM node) The action's urlbar button node.
+ */
+ urlbarButtonNodeForActionID(actionID) {
+ return document.getElementById(
+ this.urlbarButtonNodeIDForActionID(actionID)
+ );
+ },
+
+ /**
+ * The ID of the given action's button in the urlbar.
+ *
+ * @param actionID (string, required)
+ * The action ID.
+ * @return (string) The ID of the action's urlbar button node.
+ */
+ urlbarButtonNodeIDForActionID(actionID) {
+ let action = PageActions.actionForID(actionID);
+ if (action && action.urlbarIDOverride) {
+ return action.urlbarIDOverride;
+ }
+ return `pageAction-urlbar-${actionID}`;
+ },
+
+ // The ID of the given action's panelview.
+ _panelViewNodeIDForActionID(actionID, forUrlbar) {
+ let placementID = forUrlbar ? "urlbar" : "panel";
+ return `pageAction-${placementID}-${actionID}-subview`;
+ },
+
+ // The ID of the action corresponding to the given top-level button in the
+ // panel or button in the urlbar.
+ _actionIDForNodeID(nodeID) {
+ if (!nodeID) {
+ return null;
+ }
+ let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
+ if (match) {
+ return match[1];
+ }
+ // Check all the urlbar ID overrides.
+ for (let action of PageActions.actions) {
+ if (action.urlbarIDOverride && action.urlbarIDOverride == nodeID) {
+ return action.id;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Call this when the main page action button in the urlbar is activated.
+ *
+ * @param event (DOM event, required)
+ * The click or whatever event.
+ */
+ mainButtonClicked(event) {
+ event.stopPropagation();
+ if (
+ // On mac, ctrl-click will send a context menu event from the widget, so
+ // we don't want to bring up the panel when ctrl key is pressed.
+ (event.type == "mousedown" &&
+ (event.button != 0 ||
+ (AppConstants.platform == "macosx" && event.ctrlKey))) ||
+ (event.type == "keypress" &&
+ event.charCode != KeyEvent.DOM_VK_SPACE &&
+ event.keyCode != KeyEvent.DOM_VK_RETURN)
+ ) {
+ return;
+ }
+
+ // If the activated-action panel is open and anchored to the main button,
+ // close it.
+ let panelNode = this.activatedActionPanelNode;
+ if (panelNode && panelNode.anchorNode.id == this.mainButtonNode.id) {
+ PanelMultiView.hidePopup(panelNode);
+ return;
+ }
+
+ if (this.panelNode.state == "open") {
+ PanelMultiView.hidePopup(this.panelNode);
+ } else if (this.panelNode.state == "closed") {
+ this.showPanel(event);
+ }
+ },
+
+ /**
+ * Show the page action panel
+ *
+ * @param event (DOM event, optional)
+ * The event that triggers showing the panel. (such as a mouse click,
+ * if the user clicked something to open the panel)
+ */
+ showPanel(event = null) {
+ this.panelNode.hidden = false;
+ this.mainButtonNode.setAttribute("open", "true");
+ PanelMultiView.openPopup(this.panelNode, this.mainButtonNode, {
+ position: "bottomcenter topright",
+ triggerEvent: event,
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * Call this on the context menu's popupshowing event.
+ *
+ * @param event (DOM event, required)
+ * The popupshowing event.
+ * @param popup (DOM node, required)
+ * The context menu popup DOM node.
+ */
+ async onContextMenuShowing(event, popup) {
+ if (event.target != popup) {
+ return;
+ }
+
+ this._contextAction = this.actionForNode(popup.triggerNode);
+ if (!this._contextAction) {
+ event.preventDefault();
+ return;
+ }
+
+ let state;
+ if (this._contextAction._isMozillaAction) {
+ state = this._contextAction.pinnedToUrlbar
+ ? "builtInPinned"
+ : "builtInUnpinned";
+ } else {
+ state = this._contextAction.pinnedToUrlbar
+ ? "extensionPinned"
+ : "extensionUnpinned";
+ }
+ popup.setAttribute("state", state);
+
+ let removeExtension = popup.querySelector(".removeExtensionItem");
+ let { extensionID } = this._contextAction;
+ let addon = extensionID && (await AddonManager.getAddonByID(extensionID));
+ removeExtension.hidden = !addon;
+ if (addon) {
+ removeExtension.disabled = !(
+ addon.permissions & AddonManager.PERM_CAN_UNINSTALL
+ );
+ }
+ },
+
+ /**
+ * Call this from the menu item in the context menu that toggles pinning.
+ */
+ togglePinningForContextAction() {
+ if (!this._contextAction) {
+ return;
+ }
+ let action = this._contextAction;
+ this._contextAction = null;
+
+ action.pinnedToUrlbar = !action.pinnedToUrlbar;
+ BrowserUsageTelemetry.recordWidgetChange(
+ action.id,
+ action.pinnedToUrlbar ? "page-action-buttons" : null,
+ "pageaction-context"
+ );
+ },
+
+ /**
+ * Call this from the menu item in the context menu that opens about:addons.
+ */
+ openAboutAddonsForContextAction() {
+ if (!this._contextAction) {
+ return;
+ }
+ let action = this._contextAction;
+ this._contextAction = null;
+
+ AMTelemetry.recordActionEvent({
+ object: "pageAction",
+ action: "manage",
+ extra: { addonId: action.extensionID },
+ });
+
+ let viewID = "addons://detail/" + encodeURIComponent(action.extensionID);
+ window.BrowserOpenAddonsMgr(viewID);
+ },
+
+ /**
+ * Call this from the menu item in the context menu that removes an add-on.
+ */
+ removeExtensionForContextAction() {
+ if (!this._contextAction) {
+ return;
+ }
+ let action = this._contextAction;
+ this._contextAction = null;
+
+ BrowserAddonUI.removeAddon(action.extensionID, "pageAction");
+ },
+
+ _contextAction: null,
+
+ /**
+ * We use this to set an attribute on the DOM node. If the attribute exists,
+ * then we get the panel node's attribute and set it on the DOM node. Otherwise,
+ * we get the title string and update the attribute with that value. The point is to map
+ * attributes on the node to strings on the main panel. Use this for DOM
+ * nodes that don't correspond to actions, like buttons in subviews.
+ *
+ * @param node (DOM node, required)
+ * The node you're setting up.
+ * @param attrName (string, required)
+ * The name of the attribute *on the node you're setting up*.
+ */
+ takeNodeAttributeFromPanel(node, attrName) {
+ let panelAttrName = node.getAttribute(attrName);
+ if (!panelAttrName && attrName == "title") {
+ attrName = "label";
+ panelAttrName = node.getAttribute(attrName);
+ }
+ if (panelAttrName) {
+ let attrValue = this.panelNode.getAttribute(panelAttrName);
+ if (attrValue) {
+ node.setAttribute(attrName, attrValue);
+ }
+ }
+ },
+
+ /**
+ * Call this on tab switch or when the current <browser>'s location changes.
+ */
+ onLocationChange() {
+ for (let action of PageActions.actions) {
+ action.onLocationChange(window);
+ }
+ },
+};
+
+/**
+ * Shows the feedback popup for an action.
+ *
+ * @param action (PageActions.Action, required)
+ * The action associated with the feedback.
+ * @param event (DOM event, optional)
+ * The event that triggered the feedback.
+ * @param messageId (string, optional)
+ * Can be used to set a message id that is different from the action id.
+ */
+function showBrowserPageActionFeedback(action, event = null, messageId = null) {
+ let anchor = BrowserPageActions.panelAnchorNodeForAction(action, event);
+
+ ConfirmationHint.show(anchor, messageId || action.id, {
+ event,
+ hideArrow: true,
+ });
+}
+
+// built-in actions below //////////////////////////////////////////////////////
+
+// bookmark
+BrowserPageActions.bookmark = {
+ onShowingInPanel(buttonNode) {
+ if (buttonNode.label == "null") {
+ BookmarkingUI.updateBookmarkPageMenuItem();
+ }
+ },
+
+ onCommand(event, buttonNode) {
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ BookmarkingUI.onStarCommand(event);
+ },
+};
+
+// pin tab
+BrowserPageActions.pinTab = {
+ updateState() {
+ let action = PageActions.actionForID("pinTab");
+ let { pinned } = gBrowser.selectedTab;
+ let fluentID;
+ if (pinned) {
+ fluentID = "page-action-unpin-tab";
+ } else {
+ fluentID = "page-action-pin-tab";
+ }
+
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
+ if (panelButton) {
+ document.l10n.setAttributes(panelButton, fluentID + "-panel");
+ panelButton.toggleAttribute("pinned", pinned);
+ }
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
+ action.id
+ );
+ if (urlbarButton) {
+ document.l10n.setAttributes(urlbarButton, fluentID + "-urlbar");
+ urlbarButton.toggleAttribute("pinned", pinned);
+ }
+ },
+
+ onCommand(event, buttonNode) {
+ if (gBrowser.selectedTab.pinned) {
+ gBrowser.unpinTab(gBrowser.selectedTab);
+ } else {
+ gBrowser.pinTab(gBrowser.selectedTab);
+ }
+ },
+};
+
+// copy URL
+BrowserPageActions.copyURL = {
+ onCommand(event, buttonNode) {
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(
+ gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec
+ );
+ let action = PageActions.actionForID("copyURL");
+ showBrowserPageActionFeedback(action, event);
+ },
+};
+
+// email link
+BrowserPageActions.emailLink = {
+ onCommand(event, buttonNode) {
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
+ },
+};
+
+// send to device
+BrowserPageActions.sendToDevice = {
+ onBeforePlacedInWindow(browserWindow) {
+ this._updateTitle();
+ gBrowser.addEventListener("TabMultiSelect", event => {
+ this._updateTitle();
+ });
+ },
+
+ // The action's title in this window depends on the number of tabs that are
+ // selected.
+ _updateTitle() {
+ let action = PageActions.actionForID("sendToDevice");
+ let tabCount = gBrowser.selectedTabs.length;
+
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
+ if (panelButton) {
+ document.l10n.setAttributes(panelButton, action.panelFluentID, {
+ tabCount,
+ });
+ }
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
+ action.id
+ );
+ if (urlbarButton) {
+ document.l10n.setAttributes(urlbarButton, action.urlbarFluentID, {
+ tabCount,
+ });
+ }
+ },
+
+ onSubviewPlaced(panelViewNode) {
+ let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+ let notReady = document.createXULElement("toolbarbutton");
+ notReady.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "pageAction-sendToDevice-notReady"
+ );
+ document.l10n.setAttributes(notReady, "page-action-send-tab-not-ready");
+ notReady.setAttribute("disabled", "true");
+ bodyNode.appendChild(notReady);
+ for (let node of bodyNode.children) {
+ BrowserPageActions.takeNodeAttributeFromPanel(node, "title");
+ BrowserPageActions.takeNodeAttributeFromPanel(node, "shortcut");
+ }
+ },
+
+ onLocationChange() {
+ let action = PageActions.actionForID("sendToDevice");
+ let browser = gBrowser.selectedBrowser;
+ let url = browser.currentURI.spec;
+ action.setDisabled(!gSync.isSendableURI(url), window);
+ },
+
+ onShowingSubview(panelViewNode) {
+ gSync.populateSendTabToDevicesView(panelViewNode);
+ },
+};
+
+// add search engine
+BrowserPageActions.addSearchEngine = {
+ get action() {
+ return PageActions.actionForID("addSearchEngine");
+ },
+
+ get engines() {
+ return gBrowser.selectedBrowser.engines || [];
+ },
+
+ get strings() {
+ delete this.strings;
+ let uri = "chrome://browser/locale/search.properties";
+ return (this.strings = Services.strings.createBundle(uri));
+ },
+
+ updateEngines() {
+ // As a slight optimization, if the action isn't in the urlbar, don't do
+ // anything here except disable it. The action's panel nodes are updated
+ // when the panel is shown.
+ this.action.setDisabled(!this.engines.length, window);
+ if (this.action.shouldShowInUrlbar(window)) {
+ this._updateTitleAndIcon();
+ }
+ },
+
+ _updateTitleAndIcon() {
+ if (!this.engines.length) {
+ return;
+ }
+ let title = this.strings.GetStringFromName("searchAddFoundEngine2");
+ this.action.setTitle(title, window);
+ this.action.setIconURL(this.engines[0].icon, window);
+ },
+
+ onShowingInPanel() {
+ this._updateTitleAndIcon();
+ this.action.setWantsSubview(this.engines.length > 1, window);
+ let button = BrowserPageActions.panelButtonNodeForActionID(this.action.id);
+ button.setAttribute("image", this.engines[0].icon);
+ button.setAttribute("uri", this.engines[0].uri);
+ button.setAttribute("crop", "center");
+ },
+
+ onSubviewShowing(panelViewNode) {
+ let body = panelViewNode.querySelector(".panel-subview-body");
+ while (body.firstChild) {
+ body.firstChild.remove();
+ }
+ for (let engine of this.engines) {
+ let button = document.createXULElement("toolbarbutton");
+ button.classList.add("subviewbutton", "subviewbutton-iconic");
+ button.setAttribute("label", engine.title);
+ button.setAttribute("image", engine.icon);
+ button.setAttribute("uri", engine.uri);
+ button.addEventListener("command", event => {
+ let panelNode = panelViewNode.closest("panel");
+ PanelMultiView.hidePopup(panelNode);
+ this._installEngine(
+ button.getAttribute("uri"),
+ button.getAttribute("image")
+ );
+ });
+ body.appendChild(button);
+ }
+ },
+
+ onCommand(event, buttonNode) {
+ if (!buttonNode.closest("panel")) {
+ // The urlbar button was clicked. It should have a subview if there are
+ // many engines.
+ let manyEngines = this.engines.length > 1;
+ this.action.setWantsSubview(manyEngines, window);
+ if (manyEngines) {
+ return;
+ }
+ }
+ // Either the panel button or urlbar button was clicked -- not a button in
+ // the subview -- but in either case, there's only one search engine.
+ // (Because this method isn't called when the panel button is clicked and it
+ // shows a subview, and the many-engines case for the urlbar returned early
+ // above.)
+ let engine = this.engines[0];
+ this._installEngine(engine.uri, engine.icon);
+ },
+
+ _installEngine(uri, image) {
+ SearchUIUtils.addOpenSearchEngine(
+ uri,
+ image,
+ gBrowser.selectedBrowser.browsingContext
+ )
+ .then(result => {
+ if (result) {
+ showBrowserPageActionFeedback(this.action);
+ }
+ })
+ .catch(console.error);
+ },
+};
+
+// share URL
+BrowserPageActions.shareURL = {
+ onCommand(event, buttonNode) {
+ let browser = gBrowser.selectedBrowser;
+ let currentURI = gURLBar.makeURIReadable(browser.currentURI).displaySpec;
+ this._windowsUIUtils.shareUrl(currentURI, browser.contentTitle);
+ },
+
+ onShowingInPanel(buttonNode) {
+ this._cached = false;
+ },
+
+ onShowingSubview(panelViewNode) {
+ let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+
+ // We cache the providers + the UI if the user selects the share
+ // panel multiple times while the panel is open.
+ if (this._cached && bodyNode.children.length) {
+ return;
+ }
+
+ let sharingService = this._sharingService;
+ let url = gBrowser.selectedBrowser.currentURI;
+ let currentURI = gURLBar.makeURIReadable(url).displaySpec;
+ let shareProviders = sharingService.getSharingProviders(currentURI);
+ let fragment = document.createDocumentFragment();
+
+ let onCommand = event => {
+ let shareName = event.target.getAttribute("share-name");
+ if (shareName) {
+ sharingService.shareUrl(
+ shareName,
+ currentURI,
+ gBrowser.selectedBrowser.contentTitle
+ );
+ } else if (event.target.classList.contains("share-more-button")) {
+ sharingService.openSharingPreferences();
+ }
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
+ };
+
+ shareProviders.forEach(function(share) {
+ let item = document.createXULElement("toolbarbutton");
+ item.setAttribute("label", share.menuItemTitle);
+ item.setAttribute("share-name", share.name);
+ item.setAttribute("image", share.image);
+ item.classList.add("subviewbutton", "subviewbutton-iconic");
+ item.addEventListener("command", onCommand);
+ fragment.appendChild(item);
+ });
+
+ let item = document.createXULElement("toolbarbutton");
+ document.l10n.setAttributes(item, "page-action-share-more-panel");
+ item.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "share-more-button"
+ );
+ item.addEventListener("command", onCommand);
+ fragment.appendChild(item);
+
+ while (bodyNode.firstChild) {
+ bodyNode.firstChild.remove();
+ }
+ bodyNode.appendChild(fragment);
+ this._cached = true;
+ },
+};
+
+// Attach sharingService here so tests can override the implementation
+XPCOMUtils.defineLazyServiceGetters(BrowserPageActions.shareURL, {
+ _sharingService: [
+ "@mozilla.org/widget/macsharingservice;1",
+ "nsIMacSharingService",
+ ],
+ _windowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
+});
diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js
new file mode 100644
index 0000000000..83ac8763c8
--- /dev/null
+++ b/browser/base/content/browser-places.js
@@ -0,0 +1,2534 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["PlacesToolbar", "PlacesMenu", "PlacesPanelview", "PlacesPanelMenuView"],
+ "chrome://browser/content/places/browserPlacesViews.js"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BookmarkPanelHub: "resource://activity-stream/lib/BookmarkPanelHub.jsm",
+});
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "NEWTAB_ENABLED",
+ "browser.newtabpage.enabled",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "SHOW_OTHER_BOOKMARKS",
+ "browser.toolbars.bookmarks.showOtherBookmarks",
+ true,
+ (aPref, aPrevVal, aNewVal) => {
+ BookmarkingUI.maybeShowOtherBookmarksFolder();
+ document
+ .getElementById("PlacesToolbar")
+ ?._placesView?.updateNodesVisibility();
+ }
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PanelMultiView",
+ "resource:///modules/PanelMultiView.jsm"
+);
+
+var StarUI = {
+ _itemGuids: null,
+ _batching: false,
+ _isNewBookmark: false,
+ _isComposing: false,
+ _autoCloseTimer: 0,
+ // The autoclose timer is diasbled if the user interacts with the
+ // popup, such as making a change through typing or clicking on
+ // the popup.
+ _autoCloseTimerEnabled: true,
+ // The autoclose timeout length. 3500ms matches the timeout that Pocket uses
+ // in browser/components/pocket/content/panels/js/saved.js.
+ _autoCloseTimeout: 3500,
+ _removeBookmarksOnPopupHidden: false,
+
+ _element(aID) {
+ return document.getElementById(aID);
+ },
+
+ get showForNewBookmarks() {
+ return Services.prefs.getBoolPref(
+ "browser.bookmarks.editDialog.showForNewBookmarks"
+ );
+ },
+
+ // Edit-bookmark panel
+ get panel() {
+ delete this.panel;
+ this._createPanelIfNeeded();
+ var element = this._element("editBookmarkPanel");
+ // initially the panel is hidden
+ // to avoid impacting startup / new window performance
+ element.hidden = false;
+ element.addEventListener("keypress", this, { mozSystemGroup: true });
+ element.addEventListener("mousedown", this);
+ element.addEventListener("mouseout", this);
+ element.addEventListener("mousemove", this);
+ element.addEventListener("compositionstart", this);
+ element.addEventListener("compositionend", this);
+ element.addEventListener("input", this);
+ element.addEventListener("popuphidden", this);
+ element.addEventListener("popupshown", this);
+ return (this.panel = element);
+ },
+
+ // nsIDOMEventListener
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mousemove":
+ clearTimeout(this._autoCloseTimer);
+ // The autoclose timer is not disabled on generic mouseout
+ // because the user may not have actually interacted with the popup.
+ break;
+ case "popuphidden": {
+ clearTimeout(this._autoCloseTimer);
+ if (aEvent.originalTarget == this.panel) {
+ let { selectedFolderGuid, didChangeFolder } = gEditItemOverlay;
+ gEditItemOverlay.uninitPanel(true);
+
+ this._anchorElement.removeAttribute("open");
+ this._anchorElement = null;
+
+ let removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
+ this._removeBookmarksOnPopupHidden = false;
+ let guidsForRemoval = this._itemGuids;
+ this._itemGuids = null;
+
+ if (this._batching) {
+ this.endBatch();
+ }
+
+ if (removeBookmarksOnPopupHidden && guidsForRemoval) {
+ if (this._isNewBookmark) {
+ PlacesTransactions.undo().catch(Cu.reportError);
+ break;
+ }
+ // Remove all bookmarks for the bookmark's url, this also removes
+ // the tags for the url.
+ PlacesTransactions.Remove(guidsForRemoval)
+ .transact()
+ .catch(Cu.reportError);
+ } else if (this._isNewBookmark) {
+ this.showConfirmation();
+ }
+
+ if (!removeBookmarksOnPopupHidden) {
+ this._storeRecentlyUsedFolder(
+ selectedFolderGuid,
+ didChangeFolder
+ ).catch(console.error);
+ }
+ }
+ break;
+ }
+ case "keypress":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+
+ if (aEvent.defaultPrevented) {
+ // The event has already been consumed inside of the panel.
+ break;
+ }
+
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_ESCAPE:
+ if (this._isNewBookmark) {
+ this._removeBookmarksOnPopupHidden = true;
+ }
+ this.panel.hidePopup();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ if (
+ aEvent.target.classList.contains("expander-up") ||
+ aEvent.target.classList.contains("expander-down") ||
+ aEvent.target.id == "editBMPanel_newFolderButton" ||
+ aEvent.target.id == "editBookmarkPanelRemoveButton"
+ ) {
+ // XXX Why is this necessary? The defaultPrevented check should
+ // be enough.
+ break;
+ }
+ this.panel.hidePopup();
+ break;
+ // This case is for catching character-generating keypresses
+ case 0:
+ let accessKey = document.getElementById("key_close");
+ if (eventMatchesKey(aEvent, accessKey)) {
+ this.panel.hidePopup();
+ }
+ break;
+ }
+ break;
+ case "compositionend":
+ // After composition is committed, "mouseout" or something can set
+ // auto close timer.
+ this._isComposing = false;
+ break;
+ case "compositionstart":
+ if (aEvent.defaultPrevented) {
+ // If the composition was canceled, nothing to do here.
+ break;
+ }
+ this._isComposing = true;
+ // Explicit fall-through, during composition, panel shouldn't be hidden automatically.
+ case "input":
+ // Might have edited some text without keyboard events nor composition
+ // events. Fall-through to cancel auto close in such case.
+ case "mousedown":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+ break;
+ case "mouseout":
+ if (!this._autoCloseTimerEnabled) {
+ // Don't autoclose the popup if the user has made a selection
+ // or keypress and then subsequently mouseout.
+ break;
+ }
+ // Explicit fall-through
+ case "popupshown":
+ // Don't handle events for descendent elements.
+ if (aEvent.target != aEvent.currentTarget) {
+ break;
+ }
+ // auto-close if new and not interacted with
+ if (this._isNewBookmark && !this._isComposing) {
+ let delay = this._autoCloseTimeout;
+ if (this._closePanelQuickForTesting) {
+ delay /= 10;
+ }
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimer = setTimeout(() => {
+ if (!this.panel.matches(":hover")) {
+ this.panel.hidePopup(true);
+ }
+ }, delay);
+ this._autoCloseTimerEnabled = true;
+ }
+ break;
+ }
+ },
+
+ getRecommendation(data) {
+ return BookmarkPanelHub.messageRequest(data, window);
+ },
+
+ toggleRecommendation() {
+ BookmarkPanelHub.toggleRecommendation();
+ },
+
+ async showEditBookmarkPopup(aNode, aIsNewBookmark, aUrl) {
+ // Slow double-clicks (not true double-clicks) shouldn't
+ // cause the panel to flicker.
+ if (this.panel.state != "closed") {
+ return;
+ }
+
+ this._isNewBookmark = aIsNewBookmark;
+ this._itemGuids = null;
+
+ this._element("editBookmarkPanelTitle").value = this._isNewBookmark
+ ? gNavigatorBundle.getString("editBookmarkPanel.newBookmarkTitle")
+ : gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
+
+ this._element(
+ "editBookmarkPanel_showForNewBookmarks"
+ ).checked = this.showForNewBookmarks;
+
+ this._itemGuids = [];
+ await PlacesUtils.bookmarks.fetch({ url: aUrl }, bookmark =>
+ this._itemGuids.push(bookmark.guid)
+ );
+
+ let removeButton = this._element("editBookmarkPanelRemoveButton");
+ if (this._isNewBookmark) {
+ removeButton.label = gNavigatorBundle.getString(
+ "editBookmarkPanel.cancel.label"
+ );
+ removeButton.setAttribute(
+ "accesskey",
+ gNavigatorBundle.getString("editBookmarkPanel.cancel.accesskey")
+ );
+ } else {
+ // The label of the remove button differs if the URI is bookmarked
+ // multiple times.
+ let bookmarksCount = this._itemGuids.length;
+ let forms = gNavigatorBundle.getString(
+ "editBookmark.removeBookmarks.label"
+ );
+ let label = PluralForm.get(bookmarksCount, forms).replace(
+ "#1",
+ bookmarksCount
+ );
+ removeButton.label = label;
+ removeButton.setAttribute(
+ "accesskey",
+ gNavigatorBundle.getString("editBookmark.removeBookmarks.accesskey")
+ );
+ }
+
+ this._setIconAndPreviewImage();
+
+ await this.getRecommendation({
+ container: this._element("editBookmarkPanelRecommendation"),
+ infoButton: this._element("editBookmarkPanelInfoButton"),
+ recommendationContainer: this._element("editBookmarkPanelRecommendation"),
+ document,
+ url: aUrl.href,
+ close: e => {
+ e.stopPropagation();
+ BookmarkPanelHub.toggleRecommendation(false);
+ },
+ hidePopup: () => {
+ this.panel.hidePopup();
+ },
+ });
+
+ this.beginBatch();
+
+ this._anchorElement = BookmarkingUI.anchor;
+ this._anchorElement.setAttribute("open", "true");
+
+ let onPanelReady = fn => {
+ let target = this.panel;
+ if (target.parentNode) {
+ // By targeting the panel's parent and using a capturing listener, we
+ // can have our listener called before others waiting for the panel to
+ // be shown (which probably expect the panel to be fully initialized)
+ target = target.parentNode;
+ }
+ target.addEventListener(
+ "popupshown",
+ function(event) {
+ fn();
+ },
+ { capture: true, once: true }
+ );
+ };
+ gEditItemOverlay.initPanel({
+ node: aNode,
+ onPanelReady,
+ hiddenRows: ["location", "keyword"],
+ focusedElement: "preferred",
+ isNewBookmark: this._isNewBookmark,
+ });
+
+ this.panel.openPopup(this._anchorElement, "bottomcenter topright");
+ },
+
+ _createPanelIfNeeded() {
+ // Lazy load the editBookmarkPanel the first time we need to display it.
+ if (!this._element("editBookmarkPanel")) {
+ MozXULElement.insertFTLIfNeeded("browser/editBookmarkOverlay.ftl");
+ let template = this._element("editBookmarkPanelTemplate");
+ let clone = template.content.cloneNode(true);
+ template.replaceWith(clone);
+ }
+ },
+
+ _setIconAndPreviewImage() {
+ let faviconImage = this._element("editBookmarkPanelFavicon");
+ faviconImage.removeAttribute("iconloadingprincipal");
+ faviconImage.removeAttribute("src");
+
+ let tab = gBrowser.selectedTab;
+ if (tab.hasAttribute("image") && !tab.hasAttribute("busy")) {
+ faviconImage.setAttribute(
+ "iconloadingprincipal",
+ tab.getAttribute("iconloadingprincipal")
+ );
+ faviconImage.setAttribute("src", tab.getAttribute("image"));
+ }
+
+ let canvas = PageThumbs.createCanvas(window);
+ PageThumbs.captureToCanvas(gBrowser.selectedBrowser, canvas).catch(e =>
+ Cu.reportError(e)
+ );
+ document.mozSetImageElement("editBookmarkPanelImageCanvas", canvas);
+ },
+
+ removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
+ this._removeBookmarksOnPopupHidden = true;
+ this.panel.hidePopup();
+ },
+
+ // Matching the way it is used in the Library, editBookmarkOverlay implements
+ // an instant-apply UI, having no batched-Undo/Redo support.
+ // However, in this context (the Star UI) we have a Cancel button whose
+ // expected behavior is to undo all the operations done in the panel.
+ // Sometime in the future this needs to be reimplemented using a
+ // non-instant apply code path, but for the time being, we patch-around
+ // editBookmarkOverlay so that all of the actions done in the panel
+ // are treated by PlacesTransactions as a single batch. To do so,
+ // we start a PlacesTransactions batch when the star UI panel is shown, and
+ // we keep the batch ongoing until the panel is hidden.
+ _batchBlockingDeferred: null,
+ beginBatch() {
+ if (this._batching) {
+ return;
+ }
+ this._batchBlockingDeferred = PromiseUtils.defer();
+ PlacesTransactions.batch(async () => {
+ // First await for the batch to be concluded.
+ await this._batchBlockingDeferred.promise;
+ // And then for any pending promises added in the meanwhile.
+ await Promise.all(gEditItemOverlay.transactionPromises);
+ });
+ this._batching = true;
+ },
+
+ endBatch() {
+ if (!this._batching) {
+ return;
+ }
+
+ this._batchBlockingDeferred.resolve();
+ this._batchBlockingDeferred = null;
+ this._batching = false;
+ },
+
+ async _storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder) {
+ if (!selectedFolderGuid) {
+ return;
+ }
+
+ // If we're changing where a bookmark gets saved, persist that location.
+ if (didChangeFolder && gBookmarksToolbar2h2020) {
+ Services.prefs.setCharPref(
+ "browser.bookmarks.defaultLocation",
+ selectedFolderGuid
+ );
+ }
+
+ // Don't store folders that are always displayed in "Recent Folders".
+ if (PlacesUtils.bookmarks.userContentRoots.includes(selectedFolderGuid)) {
+ return;
+ }
+
+ // List of recently used folders:
+ let lastUsedFolderGuids = await PlacesUtils.metadata.get(
+ PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
+ []
+ );
+
+ let index = lastUsedFolderGuids.indexOf(selectedFolderGuid);
+ if (index > 1) {
+ // The guid is in the array but not the most recent.
+ lastUsedFolderGuids.splice(index, 1);
+ lastUsedFolderGuids.unshift(selectedFolderGuid);
+ } else if (index == -1) {
+ lastUsedFolderGuids.unshift(selectedFolderGuid);
+ }
+ while (lastUsedFolderGuids.length > PlacesUIUtils.maxRecentFolders) {
+ lastUsedFolderGuids.pop();
+ }
+
+ await PlacesUtils.metadata.set(
+ PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
+ lastUsedFolderGuids
+ );
+ },
+
+ onShowForNewBookmarksCheckboxCommand() {
+ Services.prefs.setBoolPref(
+ "browser.bookmarks.editDialog.showForNewBookmarks",
+ this._element("editBookmarkPanel_showForNewBookmarks").checked
+ );
+ },
+
+ showConfirmation() {
+ let animationTriggered = LibraryUI.triggerLibraryAnimation("bookmark");
+
+ // Show the "Saved to Library!" hint in addition to the library button
+ // animation for the first three times, or when the animation was skipped
+ // e.g. because the library button has been customized away.
+ const HINT_COUNT_PREF =
+ "browser.bookmarks.editDialog.confirmationHintShowCount";
+ const HINT_COUNT = Services.prefs.getIntPref(HINT_COUNT_PREF, 0);
+ if (animationTriggered && HINT_COUNT >= 3) {
+ return;
+ }
+ Services.prefs.setIntPref(HINT_COUNT_PREF, HINT_COUNT + 1);
+
+ let anchor;
+ if (window.toolbar.visible) {
+ for (let id of ["library-button", "bookmarks-menu-button"]) {
+ let element = document.getElementById(id);
+ if (
+ element &&
+ element.getAttribute("cui-areatype") != "menu-panel" &&
+ element.getAttribute("overflowedItem") != "true"
+ ) {
+ anchor = element;
+ break;
+ }
+ }
+ }
+ if (!anchor) {
+ anchor = document.getElementById("PanelUI-menu-button");
+ }
+ ConfirmationHint.show(anchor, "pageBookmarked");
+ },
+};
+
+var PlacesCommandHook = {
+ /**
+ * Adds a bookmark to the page loaded in the current browser.
+ */
+ async bookmarkPage() {
+ let browser = gBrowser.selectedBrowser;
+ let url = new URL(browser.currentURI.spec);
+ let info = await PlacesUtils.bookmarks.fetch({ url });
+ let isNewBookmark = !info;
+ let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks;
+ if (isNewBookmark) {
+ // This is async because we have to validate the guid
+ // coming from prefs.
+ let parentGuid = await PlacesUIUtils.defaultParentGuid;
+ info = { url, parentGuid };
+ // Bug 1148838 - Make this code work for full page plugins.
+ let charset = null;
+
+ let isErrorPage = false;
+ if (browser.documentURI) {
+ isErrorPage = /^about:(neterror|certerror|blocked)/.test(
+ browser.documentURI.spec
+ );
+ }
+
+ try {
+ if (isErrorPage) {
+ let entry = await PlacesUtils.history.fetch(browser.currentURI);
+ if (entry) {
+ info.title = entry.title;
+ }
+ } else {
+ info.title = browser.contentTitle;
+ }
+ info.title = info.title || url.href;
+ charset = browser.characterSet;
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ if (showEditUI) {
+ // If we bookmark the page here but open right into a cancelable
+ // state (i.e. new bookmark in Library), start batching here so
+ // all of the actions can be undone in a single undo step.
+ StarUI.beginBatch();
+ }
+
+ info.guid = await PlacesTransactions.NewBookmark(info).transact();
+
+ if (charset) {
+ PlacesUIUtils.setCharsetForPage(url, charset, window).catch(
+ Cu.reportError
+ );
+ }
+ }
+
+ // Revert the contents of the location bar
+ gURLBar.handleRevert();
+
+ // If it was not requested to open directly in "edit" mode, we are done.
+ if (!showEditUI) {
+ StarUI.showConfirmation();
+ return;
+ }
+
+ let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
+
+ await StarUI.showEditBookmarkPopup(node, isNewBookmark, url);
+ },
+
+ /**
+ * Adds a bookmark to the page targeted by a link.
+ * @param url (string)
+ * the address of the link target
+ * @param title
+ * The link text
+ */
+ async bookmarkLink(url, title) {
+ let bm = await PlacesUtils.bookmarks.fetch({ url });
+ if (bm) {
+ let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ PlacesUIUtils.showBookmarkDialog({ action: "edit", node }, window.top);
+ return;
+ }
+
+ let parentGuid = await PlacesUIUtils.defaultParentGuid;
+ let parentId = await PlacesUtils.promiseItemId(parentGuid);
+ let defaultInsertionPoint = new PlacesInsertionPoint({
+ parentId,
+ parentGuid,
+ });
+ PlacesUIUtils.showBookmarkDialog(
+ {
+ action: "add",
+ type: "bookmark",
+ uri: Services.io.newURI(url),
+ title,
+ defaultInsertionPoint,
+ hiddenRows: ["location", "keyword"],
+ },
+ window.top
+ );
+ },
+
+ /**
+ * List of nsIURI objects characterizing tabs given in param.
+ * Duplicates are discarded.
+ */
+ getUniquePages(tabs) {
+ let uniquePages = {};
+ let URIs = [];
+
+ tabs.forEach(tab => {
+ let browser = tab.linkedBrowser;
+ let uri = browser.currentURI;
+ let title = browser.contentTitle || tab.label;
+ let spec = uri.spec;
+ if (!(spec in uniquePages)) {
+ uniquePages[spec] = null;
+ URIs.push({ uri, title });
+ }
+ });
+ return URIs;
+ },
+
+ /**
+ * List of nsIURI objects characterizing the tabs currently open in the
+ * browser, modulo pinned tabs. The URIs will be in the order in which their
+ * corresponding tabs appeared and duplicates are discarded.
+ */
+ get uniqueCurrentPages() {
+ let visibleUnpinnedTabs = gBrowser.visibleTabs.filter(tab => !tab.pinned);
+ return this.getUniquePages(visibleUnpinnedTabs);
+ },
+
+ /**
+ * List of nsIURI objects characterizing the tabs currently
+ * selected in the window. Duplicates are discarded.
+ */
+ get uniqueSelectedPages() {
+ return this.getUniquePages(gBrowser.selectedTabs);
+ },
+
+ /**
+ * Opens the Places Organizer.
+ * @param {String} item The item to select in the organizer window,
+ * options are (case sensitive):
+ * BookmarksMenu, BookmarksToolbar, UnfiledBookmarks,
+ * AllBookmarks, History, Downloads.
+ */
+ showPlacesOrganizer(item) {
+ var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
+ // Due to bug 528706, getMostRecentWindow can return closed windows.
+ if (!organizer || organizer.closed) {
+ // No currently open places window, so open one with the specified mode.
+ openDialog(
+ "chrome://browser/content/places/places.xhtml",
+ "",
+ "chrome,toolbar=yes,dialog=no,resizable",
+ item
+ );
+ } else {
+ organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(item);
+ organizer.focus();
+ }
+ },
+
+ searchBookmarks() {
+ gURLBar.search(UrlbarTokenizer.RESTRICT.BOOKMARK, {
+ searchModeEntry: "bookmarkmenu",
+ });
+ },
+};
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "RecentlyClosedTabsAndWindowsMenuUtils",
+ "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm"
+);
+
+// View for the history menu.
+function HistoryMenu(aPopupShowingEvent) {
+ // Workaround for Bug 610187. The sidebar does not include all the Places
+ // views definitions, and we don't need them there.
+ // Defining the prototype inheritance in the prototype itself would cause
+ // browser.js to halt on "PlacesMenu is not defined" error.
+ this.__proto__.__proto__ = PlacesMenu.prototype;
+ Object.keys(this._elements).forEach(name => {
+ this[name] = document.getElementById(this._elements[name]);
+ });
+ PlacesMenu.call(this, aPopupShowingEvent, "place:sort=4&maxResults=15");
+}
+
+HistoryMenu.prototype = {
+ _elements: {
+ undoTabMenu: "historyUndoMenu",
+ hiddenTabsMenu: "hiddenTabsMenu",
+ undoWindowMenu: "historyUndoWindowMenu",
+ syncTabsMenuitem: "sync-tabs-menuitem",
+ },
+
+ _getClosedTabCount() {
+ try {
+ return SessionStore.getClosedTabCount(window);
+ } catch (ex) {
+ // SessionStore doesn't track the hidden window, so just return zero then.
+ return 0;
+ }
+ },
+
+ toggleHiddenTabs() {
+ if (window.gBrowser && gBrowser.visibleTabs.length < gBrowser.tabs.length) {
+ this.hiddenTabsMenu.removeAttribute("hidden");
+ } else {
+ this.hiddenTabsMenu.setAttribute("hidden", "true");
+ }
+ },
+
+ toggleRecentlyClosedTabs: function HM_toggleRecentlyClosedTabs() {
+ // enable/disable the Recently Closed Tabs sub menu
+ // no restorable tabs, so disable menu
+ if (this._getClosedTabCount() == 0) {
+ this.undoTabMenu.setAttribute("disabled", true);
+ } else {
+ this.undoTabMenu.removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoSubmenu: function PHM_populateUndoSubmenu() {
+ var undoPopup = this.undoTabMenu.menupopup;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes()) {
+ undoPopup.firstChild.remove();
+ }
+
+ // no restorable tabs, so make sure menu is disabled, and return
+ if (this._getClosedTabCount() == 0) {
+ this.undoTabMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ this.undoTabMenu.removeAttribute("disabled");
+
+ // populate menu
+ let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(
+ window,
+ "menuitem"
+ );
+ undoPopup.appendChild(tabsFragment);
+ },
+
+ toggleRecentlyClosedWindows: function PHM_toggleRecentlyClosedWindows() {
+ // enable/disable the Recently Closed Windows sub menu
+ // no restorable windows, so disable menu
+ if (SessionStore.getClosedWindowCount() == 0) {
+ this.undoWindowMenu.setAttribute("disabled", true);
+ } else {
+ this.undoWindowMenu.removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoWindowSubmenu: function PHM_populateUndoWindowSubmenu() {
+ let undoPopup = this.undoWindowMenu.menupopup;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes()) {
+ undoPopup.firstChild.remove();
+ }
+
+ // no restorable windows, so make sure menu is disabled, and return
+ if (SessionStore.getClosedWindowCount() == 0) {
+ this.undoWindowMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ this.undoWindowMenu.removeAttribute("disabled");
+
+ // populate menu
+ let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(
+ window,
+ "menuitem"
+ );
+ undoPopup.appendChild(windowsFragment);
+ },
+
+ toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() {
+ // Enable/disable the Tabs From Other Computers menu. Some of the menus handled
+ // by HistoryMenu do not have this menuitem.
+ if (!this.syncTabsMenuitem) {
+ return;
+ }
+
+ if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) {
+ this.syncTabsMenuitem.setAttribute("hidden", true);
+ return;
+ }
+
+ this.syncTabsMenuitem.setAttribute("hidden", false);
+ },
+
+ _onPopupShowing: function HM__onPopupShowing(aEvent) {
+ PlacesMenu.prototype._onPopupShowing.apply(this, arguments);
+
+ // Don't handle events for submenus.
+ if (aEvent.target != aEvent.currentTarget) {
+ return;
+ }
+
+ this.toggleHiddenTabs();
+ this.toggleRecentlyClosedTabs();
+ this.toggleRecentlyClosedWindows();
+ this.toggleTabsFromOtherComputers();
+ },
+
+ _onCommand: function HM__onCommand(aEvent) {
+ aEvent = getRootEvent(aEvent);
+ let placesNode = aEvent.target._placesNode;
+ if (placesNode) {
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ PlacesUIUtils.markPageAsTyped(placesNode.uri);
+ }
+ openUILink(placesNode.uri, aEvent, {
+ ignoreAlt: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ }
+ },
+};
+
+/**
+ * Functions for handling events in the Bookmarks Toolbar and menu.
+ */
+var BookmarksEventHandler = {
+ /**
+ * Handler for click event for an item in the bookmarks toolbar or menu.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Left-click is handled in the onCommand function.
+ * When items are middle-clicked (or clicked with modifier), open in tabs.
+ * If the click came through a menu, close the menu.
+ * @param aEvent
+ * DOMEvent for the click
+ * @param aView
+ * The places view which aEvent should be associated with.
+ */
+
+ onMouseUp(aEvent) {
+ // Handles left-click with modifier if not browser.bookmarks.openInTabClosesMenu.
+ if (aEvent.button != 0 || PlacesUIUtils.openInTabClosesMenu) {
+ return;
+ }
+ let target = aEvent.originalTarget;
+ if (target.tagName != "menuitem") {
+ return;
+ }
+ let modifKey =
+ AppConstants.platform === "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
+ if (modifKey) {
+ target.setAttribute("closemenu", "none");
+ var menupopup = target.parentNode;
+ menupopup.addEventListener(
+ "popuphidden",
+ () => {
+ target.removeAttribute("closemenu");
+ },
+ { once: true }
+ );
+ } else {
+ // Handles edge case where same menuitem was opened previously
+ // while menu was kept open, but now menu should close.
+ target.removeAttribute("closemenu");
+ }
+ },
+
+ onClick: function BEH_onClick(aEvent, aView) {
+ // Only handle middle-click or left-click with modifiers.
+ let modifKey;
+ if (AppConstants.platform == "macosx") {
+ modifKey = aEvent.metaKey || aEvent.shiftKey;
+ } else {
+ modifKey = aEvent.ctrlKey || aEvent.shiftKey;
+ }
+
+ if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey)) {
+ return;
+ }
+
+ var target = aEvent.originalTarget;
+ // If this event bubbled up from a menu or menuitem,
+ // close the menus if browser.bookmarks.openInTabClosesMenu.
+ var tag = target.tagName;
+ if (
+ PlacesUIUtils.openInTabClosesMenu &&
+ (tag == "menuitem" || tag == "menu")
+ ) {
+ closeMenus(aEvent.target);
+ }
+
+ if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) {
+ // Don't open the root folder in tabs when the empty area on the toolbar
+ // is middle-clicked or when a non-bookmark item (except for Open in Tabs)
+ // in a bookmarks menupopup is middle-clicked.
+ if (target.localName == "menu" || target.localName == "toolbarbutton") {
+ PlacesUIUtils.openMultipleLinksInTabs(
+ target._placesNode,
+ aEvent,
+ aView
+ );
+ }
+ } else if (aEvent.button == 1) {
+ // left-clicks with modifier are already served by onCommand
+ this.onCommand(aEvent);
+ }
+ },
+
+ /**
+ * Handler for command event for an item in the bookmarks toolbar.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Opens the item.
+ * @param aEvent
+ * DOMEvent for the command
+ */
+ onCommand: function BEH_onCommand(aEvent) {
+ var target = aEvent.originalTarget;
+ if (target._placesNode) {
+ PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent);
+ // Only record interactions through the Bookmarks Toolbar
+ if (target.closest("#PersonalToolbar")) {
+ Services.telemetry.scalarAdd(
+ "browser.engagement.bookmarks_toolbar_bookmark_opened",
+ 1
+ );
+ }
+ }
+ },
+
+ fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) {
+ var node;
+ var cropped = false;
+ var targetURI;
+
+ if (aDocument.tooltipNode.localName == "treechildren") {
+ var tree = aDocument.tooltipNode.parentNode;
+ var cell = tree.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.row == -1) {
+ return false;
+ }
+ node = tree.view.nodeForTreeIndex(cell.row);
+ cropped = tree.isCellCropped(cell.row, cell.col);
+ } else {
+ // Check whether the tooltipNode is a Places node.
+ // In such a case use it, otherwise check for targetURI attribute.
+ var tooltipNode = aDocument.tooltipNode;
+ if (tooltipNode._placesNode) {
+ node = tooltipNode._placesNode;
+ } else {
+ // This is a static non-Places node.
+ targetURI = tooltipNode.getAttribute("targetURI");
+ }
+ }
+
+ if (!node && !targetURI) {
+ return false;
+ }
+
+ // Show node.label as tooltip's title for non-Places nodes.
+ var title = node ? node.title : tooltipNode.label;
+
+ // Show URL only for Places URI-nodes or nodes with a targetURI attribute.
+ var url;
+ if (targetURI || PlacesUtils.nodeIsURI(node)) {
+ url = targetURI || node.uri;
+ }
+
+ // Show tooltip for containers only if their title is cropped.
+ if (!cropped && !url) {
+ return false;
+ }
+
+ var tooltipTitle = aDocument.getElementById("bhtTitleText");
+ tooltipTitle.hidden = !title || title == url;
+ if (!tooltipTitle.hidden) {
+ tooltipTitle.textContent = title;
+ }
+
+ var tooltipUrl = aDocument.getElementById("bhtUrlText");
+ tooltipUrl.hidden = !url;
+ if (!tooltipUrl.hidden) {
+ tooltipUrl.value = url;
+ }
+
+ // Show tooltip.
+ return true;
+ },
+};
+
+// Handles special drag and drop functionality for Places menus that are not
+// part of a Places view (e.g. the bookmarks menu in the menubar).
+var PlacesMenuDNDHandler = {
+ _springLoadDelayMs: 350,
+ _closeDelayMs: 500,
+ _loadTimer: null,
+ _closeTimer: null,
+ _closingTimerNode: null,
+
+ /**
+ * Called when the user enters the <menu> element during a drag.
+ * @param event
+ * The DragEnter event that spawned the opening.
+ */
+ onDragEnter: function PMDH_onDragEnter(event) {
+ // Opening menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target)) {
+ return;
+ }
+
+ // If we re-enter the same menu or anchor before the close timer runs out,
+ // we should ensure that we do not close:
+ if (this._closeTimer && this._closingTimerNode === event.currentTarget) {
+ this._closeTimer.cancel();
+ this._closingTimerNode = null;
+ this._closeTimer = null;
+ }
+
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+ let popup = event.target.menupopup;
+ if (
+ this._loadTimer ||
+ popup.state === "showing" ||
+ popup.state === "open"
+ ) {
+ return;
+ }
+
+ this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._loadTimer.initWithCallback(
+ () => {
+ this._loadTimer = null;
+ popup.setAttribute("autoopened", "true");
+ popup.openPopup();
+ },
+ this._springLoadDelayMs,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles dragleave on the <menu> element.
+ */
+ onDragLeave: function PMDH_onDragLeave(event) {
+ // Handle menu-button separate targets.
+ if (
+ event.relatedTarget === event.currentTarget ||
+ (event.relatedTarget &&
+ event.relatedTarget.parentNode === event.currentTarget)
+ ) {
+ return;
+ }
+
+ // Closing menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target)) {
+ return;
+ }
+
+ PlacesControllerDragHelper.currentDropTarget = null;
+ let popup = event.target.menupopup;
+
+ if (this._loadTimer) {
+ this._loadTimer.cancel();
+ this._loadTimer = null;
+ }
+ this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._closingTimerNode = event.currentTarget;
+ this._closeTimer.initWithCallback(
+ function() {
+ this._closeTimer = null;
+ this._closingTimerNode = null;
+ let node = PlacesControllerDragHelper.currentDropTarget;
+ let inHierarchy = false;
+ while (node && !inHierarchy) {
+ inHierarchy = node == event.target;
+ node = node.parentNode;
+ }
+ if (!inHierarchy && popup && popup.hasAttribute("autoopened")) {
+ popup.removeAttribute("autoopened");
+ popup.hidePopup();
+ }
+ },
+ this._closeDelayMs,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ /**
+ * Determines if a XUL element represents a static container.
+ * @returns true if the element is a container element (menu or
+ *` menu-toolbarbutton), false otherwise.
+ */
+ _isStaticContainer: function PMDH__isContainer(node) {
+ let isMenu =
+ node.localName == "menu" ||
+ (node.localName == "toolbarbutton" &&
+ node.getAttribute("type") == "menu");
+ let isStatic =
+ !("_placesNode" in node) &&
+ node.menupopup &&
+ node.menupopup.hasAttribute("placespopup") &&
+ !node.parentNode.hasAttribute("placespopup");
+ return isMenu && isStatic;
+ },
+
+ /**
+ * Called when the user drags over the <menu> element.
+ * @param event
+ * The DragOver event.
+ */
+ onDragOver: function PMDH_onDragOver(event) {
+ let ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.bookmarksMenuFolderId,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+ if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) {
+ event.preventDefault();
+ }
+
+ event.stopPropagation();
+ },
+
+ /**
+ * Called when the user drops on the <menu> element.
+ * @param event
+ * The Drop event.
+ */
+ onDrop: function PMDH_onDrop(event) {
+ // Put the item at the end of bookmark menu.
+ let ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.bookmarksMenuFolderId,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+ PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
+ PlacesControllerDragHelper.currentDropTarget = null;
+ event.stopPropagation();
+ },
+};
+
+/**
+ * This object handles the initialization and uninitialization of the bookmarks
+ * toolbar. It also has helper functions for the managed bookmarks button.
+ */
+var PlacesToolbarHelper = {
+ get _viewElt() {
+ return document.getElementById("PlacesToolbar");
+ },
+
+ /**
+ * Initialize. This will check whether we've finished startup and can
+ * show toolbars.
+ */
+ async init() {
+ let telemetryKey = await PlacesUIUtils.canLoadToolbarContentPromise;
+ let didCreate = this._realInit();
+ this._measureToolbarPaintDelay(telemetryKey, didCreate);
+ },
+
+ /**
+ * @return whether we actually initialized the places view (and
+ * aren't collapsed).
+ */
+ _realInit() {
+ let viewElt = this._viewElt;
+ if (!viewElt || viewElt._placesView || window.closed) {
+ return false;
+ }
+
+ // CustomizableUI.addListener is idempotent, so we can safely
+ // call this multiple times.
+ CustomizableUI.addListener(this);
+
+ if (!this._isObservingToolbars) {
+ this._isObservingToolbars = true;
+ window.addEventListener("toolbarvisibilitychange", this);
+ }
+
+ // If the bookmarks toolbar item is:
+ // - not in a toolbar, or;
+ // - the toolbar is collapsed, or;
+ // - the toolbar is hidden some other way:
+ // don't initialize. Also, there is no need to initialize the toolbar if
+ // customizing, because that will happen when the customization is done.
+ let toolbar = this._getParentToolbar(viewElt);
+ if (
+ !toolbar ||
+ toolbar.collapsed ||
+ this._isCustomizing ||
+ getComputedStyle(toolbar, "").display == "none"
+ ) {
+ return false;
+ }
+
+ if (
+ toolbar.id == "PersonalToolbar" &&
+ !toolbar.hasAttribute("initialized")
+ ) {
+ toolbar.setAttribute("initialized", "true");
+ BookmarkingUI.updateEmptyToolbarMessage();
+ }
+
+ new PlacesToolbar(`place:parent=${PlacesUtils.bookmarks.toolbarGuid}`);
+ return true;
+ },
+
+ // Only measure once per window:
+ _shouldMeasure: true,
+ _measureToolbarPaintDelay(telemetryKey, didCreate) {
+ if (!this._shouldMeasure) {
+ return;
+ }
+ this._shouldMeasure = false;
+ // If we create and show the toolbar later, we don't want to measure how
+ // long it took, so it's important this check happens after setting
+ // _shouldMeasure.
+ if (!didCreate) {
+ return;
+ }
+
+ let recordDelay = time => {
+ let entries = window.performance.getEntriesByType("paint");
+ let timeEntry = entries.find(e => e.name == "first-contentful-paint");
+ let histogram = Services.telemetry.getKeyedHistogramById(
+ "PLACES_BOOKMARKS_TOOLBAR_RENDER_DELAY_MS"
+ );
+ if (timeEntry) {
+ let delay = time - timeEntry.startTime - timeEntry.duration;
+ histogram.add(telemetryKey, Math.round(delay));
+ } else {
+ // If there is no base time, we haven't painted yet, so we rendered
+ // before paint:
+ histogram.add(telemetryKey, 0);
+ }
+ };
+ if (!window.windowUtils.isMozAfterPaintPending) {
+ recordDelay(performance.now());
+ return;
+ }
+ let removeListeners = () => {
+ window.removeEventListener("unload", removeListeners);
+ window.removeEventListener("MozAfterPaint", paintHandler);
+ };
+ let paintHandler = ev => {
+ removeListeners();
+ recordDelay(ev.paintTimeStamp);
+ };
+ window.addEventListener("MozAfterPaint", paintHandler);
+ window.addEventListener("unload", removeListeners);
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "toolbarvisibilitychange":
+ if (event.target == this._getParentToolbar(this._viewElt)) {
+ this._resetView();
+ }
+ break;
+ }
+ },
+
+ /**
+ * This is a no-op if we haven't been initialized.
+ */
+ uninit: function PTH_uninit() {
+ if (this._isObservingToolbars) {
+ delete this._isObservingToolbars;
+ window.removeEventListener("toolbarvisibilitychange", this);
+ }
+ CustomizableUI.removeListener(this);
+ },
+
+ customizeStart: function PTH_customizeStart() {
+ try {
+ let viewElt = this._viewElt;
+ if (viewElt && viewElt._placesView) {
+ viewElt._placesView.uninit();
+ }
+ } finally {
+ this._isCustomizing = true;
+ }
+ },
+
+ customizeDone: function PTH_customizeDone() {
+ this._isCustomizing = false;
+ this.init();
+ },
+
+ onPlaceholderCommand() {
+ let widgetGroup = CustomizableUI.getWidget("personal-bookmarks");
+ let widget = widgetGroup.forWindow(window);
+ if (
+ widget.overflowed ||
+ widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL
+ ) {
+ PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
+ }
+ },
+
+ _getParentToolbar(element) {
+ while (element) {
+ if (element.localName == "toolbar") {
+ return element;
+ }
+ element = element.parentNode;
+ }
+ return null;
+ },
+
+ onWidgetUnderflow(aNode, aContainer) {
+ // The view gets broken by being removed and reinserted by the overflowable
+ // toolbar, so we have to force an uninit and reinit.
+ let win = aNode.ownerGlobal;
+ if (aNode.id == "personal-bookmarks" && win == window) {
+ this._resetView();
+ }
+ },
+
+ onWidgetAdded(aWidgetId, aArea, aPosition) {
+ if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) {
+ // It's possible (with the "Add to Menu", "Add to Toolbar" context
+ // options) that the Places Toolbar Items have been moved without
+ // letting us prepare and handle it with with customizeStart and
+ // customizeDone. If that's the case, we need to reset the views
+ // since they're probably broken from the DOM reparenting.
+ this._resetView();
+ }
+ },
+
+ _resetView() {
+ if (this._viewElt) {
+ // It's possible that the placesView might not exist, and we need to
+ // do a full init. This could happen if the Bookmarks Toolbar Items are
+ // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar"
+ // context menu option, outside of customize mode.
+ if (this._viewElt._placesView) {
+ this._viewElt._placesView.uninit();
+ }
+ this.init();
+ }
+ },
+
+ async populateManagedBookmarks(popup) {
+ if (popup.hasChildNodes()) {
+ return;
+ }
+ // Show item's uri in the status bar when hovering, and clear on exit
+ popup.addEventListener("DOMMenuItemActive", function(event) {
+ XULBrowserWindow.setOverLink(event.target.link);
+ });
+ popup.addEventListener("DOMMenuItemInactive", function() {
+ XULBrowserWindow.setOverLink("");
+ });
+ let fragment = document.createDocumentFragment();
+ await this.addManagedBookmarks(
+ fragment,
+ Services.policies.getActivePolicies().ManagedBookmarks
+ );
+ popup.appendChild(fragment);
+ },
+
+ async addManagedBookmarks(menu, children) {
+ for (let i = 0; i < children.length; i++) {
+ let entry = children[i];
+ if (entry.children) {
+ // It's a folder.
+ let submenu = document.createXULElement("menu");
+ if (entry.name) {
+ submenu.setAttribute("label", entry.name);
+ } else {
+ submenu.setAttribute("data-l10n-id", "managed-bookmarks-subfolder");
+ }
+ submenu.setAttribute("container", "true");
+ submenu.setAttribute("class", "menu-iconic bookmark-item");
+ let submenupopup = document.createXULElement("menupopup");
+ submenu.appendChild(submenupopup);
+ menu.appendChild(submenu);
+ this.addManagedBookmarks(submenupopup, entry.children);
+ } else if (entry.name && entry.url) {
+ // It's bookmark.
+ let { preferredURI } = Services.uriFixup.getFixupURIInfo(entry.url);
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", entry.name);
+ menuitem.setAttribute("image", "page-icon:" + preferredURI.spec);
+ menuitem.setAttribute(
+ "class",
+ "menuitem-iconic bookmark-item menuitem-with-favicon"
+ );
+ menuitem.link = preferredURI.spec;
+ menu.appendChild(menuitem);
+ }
+ }
+ },
+
+ openManagedBookmark(event) {
+ openUILink(event.target.link, event, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ },
+
+ onDragStartManaged(event) {
+ if (!event.target.link) {
+ return;
+ }
+
+ let dt = event.dataTransfer;
+
+ let node = {};
+ node.type = 0;
+ node.title = event.target.label;
+ node.uri = event.target.link;
+
+ function addData(type, index) {
+ let wrapNode = PlacesUtils.wrapNode(node, type);
+ dt.mozSetDataAt(type, wrapNode, index);
+ }
+
+ addData(PlacesUtils.TYPE_X_MOZ_URL, 0);
+ addData(PlacesUtils.TYPE_UNICODE, 0);
+ addData(PlacesUtils.TYPE_HTML, 0);
+ },
+};
+
+/**
+ * Handles the Library button in the toolbar.
+ */
+var LibraryUI = {
+ /**
+ * @returns true if the animation could be triggered, false otherwise.
+ */
+ triggerLibraryAnimation(animation) {
+ let libraryButton = document.getElementById("library-button");
+ if (
+ !libraryButton ||
+ libraryButton.getAttribute("cui-areatype") == "menu-panel" ||
+ libraryButton.getAttribute("overflowedItem") == "true" ||
+ !libraryButton.closest("#nav-bar") ||
+ !window.toolbar.visible ||
+ gReduceMotion
+ ) {
+ return false;
+ }
+
+ let animatableBox = document.getElementById("library-animatable-box");
+ let navBar = document.getElementById("nav-bar");
+ let iconBounds = window.windowUtils.getBoundsWithoutFlushing(
+ libraryButton.icon
+ );
+ let libraryBounds = window.windowUtils.getBoundsWithoutFlushing(
+ libraryButton
+ );
+
+ animatableBox.style.setProperty(
+ "--library-button-height",
+ libraryBounds.height + "px"
+ );
+ animatableBox.style.setProperty("--library-icon-x", iconBounds.x + "px");
+ if (navBar.hasAttribute("brighttext")) {
+ animatableBox.setAttribute("brighttext", "true");
+ } else {
+ animatableBox.removeAttribute("brighttext");
+ }
+ animatableBox.removeAttribute("fade");
+ libraryButton.setAttribute("animate", animation);
+ animatableBox.setAttribute("animate", animation);
+ if (!this._libraryButtonAnimationEndListeners[animation]) {
+ this._libraryButtonAnimationEndListeners[animation] = event => {
+ this._libraryButtonAnimationEndListener(event, animation);
+ };
+ }
+ animatableBox.addEventListener(
+ "animationend",
+ this._libraryButtonAnimationEndListeners[animation]
+ );
+
+ window.addEventListener("resize", this._onWindowResize);
+
+ return true;
+ },
+
+ _libraryButtonAnimationEndListeners: {},
+ _libraryButtonAnimationEndListener(aEvent, animation) {
+ let animatableBox = document.getElementById("library-animatable-box");
+ if (aEvent.animationName.startsWith(`library-${animation}-animation`)) {
+ animatableBox.setAttribute("fade", "true");
+ } else if (aEvent.animationName == `library-${animation}-fade`) {
+ animatableBox.removeEventListener(
+ "animationend",
+ LibraryUI._libraryButtonAnimationEndListeners[animation]
+ );
+ animatableBox.removeAttribute("animate");
+ animatableBox.removeAttribute("fade");
+ window.removeEventListener("resize", this._onWindowResize);
+ let libraryButton = document.getElementById("library-button");
+ // Put the 'fill' back in the normal icon.
+ libraryButton.removeAttribute("animate");
+ }
+ },
+
+ _windowResizeRunning: false,
+ _onWindowResize(aEvent) {
+ if (LibraryUI._windowResizeRunning) {
+ return;
+ }
+ LibraryUI._windowResizeRunning = true;
+
+ requestAnimationFrame(() => {
+ let libraryButton = document.getElementById("library-button");
+ // Only update the position if the library button remains in the
+ // navbar (not moved to the palette or elsewhere).
+ if (
+ !libraryButton ||
+ libraryButton.getAttribute("cui-areatype") == "menu-panel" ||
+ libraryButton.getAttribute("overflowedItem") == "true" ||
+ !libraryButton.closest("#nav-bar")
+ ) {
+ return;
+ }
+
+ let animatableBox = document.getElementById("library-animatable-box");
+ let iconBounds = window.windowUtils.getBoundsWithoutFlushing(
+ libraryButton.icon
+ );
+
+ // Resizing the window will only have the ability to change the X offset of the
+ // library button.
+ animatableBox.style.setProperty("--library-icon-x", iconBounds.x + "px");
+
+ LibraryUI._windowResizeRunning = false;
+ });
+ },
+};
+
+/**
+ * Handles the bookmarks menu-button in the toolbar.
+ */
+
+var BookmarkingUI = {
+ STAR_ID: "star-button",
+ STAR_BOX_ID: "star-button-box",
+ BOOKMARK_BUTTON_ID: "bookmarks-menu-button",
+ BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb",
+ get button() {
+ delete this.button;
+ let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID);
+ return (this.button = widgetGroup.forWindow(window).node);
+ },
+
+ get star() {
+ delete this.star;
+ return (this.star = document.getElementById(this.STAR_ID));
+ },
+
+ get starBox() {
+ delete this.starBox;
+ return (this.starBox = document.getElementById(this.STAR_BOX_ID));
+ },
+
+ get anchor() {
+ let action = PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK);
+ return BrowserPageActions.panelAnchorNodeForAction(action);
+ },
+
+ get stringbundleset() {
+ delete this.stringbundleset;
+ return (this.stringbundleset = document.getElementById("stringbundleset"));
+ },
+
+ get toolbar() {
+ delete this.toolbar;
+ return (this.toolbar = document.getElementById("PersonalToolbar"));
+ },
+
+ STATUS_UPDATING: -1,
+ STATUS_UNSTARRED: 0,
+ STATUS_STARRED: 1,
+ get status() {
+ if (this._pendingUpdate) {
+ return this.STATUS_UPDATING;
+ }
+ return this.star.hasAttribute("starred")
+ ? this.STATUS_STARRED
+ : this.STATUS_UNSTARRED;
+ },
+
+ onPopupShowing: function BUI_onPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget) {
+ return;
+ }
+
+ // On non-photon, this code should never be reached. However, if you click
+ // the outer button's border, some cpp code for the menu button's XBL
+ // binding decides to open the popup even though the dropmarker is invisible.
+ //
+ // Separately, in Photon, if the button is in the dynamic portion of the
+ // overflow panel, we want to show a subview instead.
+ if (
+ this.button.getAttribute("cui-areatype") ==
+ CustomizableUI.TYPE_MENU_PANEL ||
+ this.button.hasAttribute("overflowedItem")
+ ) {
+ this._showSubView();
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID).forWindow(
+ window
+ );
+ if (widget.overflowed) {
+ // Don't open a popup in the overflow popup, rather just open the Library.
+ event.preventDefault();
+ widget.node.removeAttribute("closemenu");
+ PlacesCommandHook.showPlacesOrganizer("BookmarksMenu");
+ return;
+ }
+
+ this._initMobileBookmarks(document.getElementById("BMB_mobileBookmarks"));
+
+ this.updateLabel(
+ "BMB_viewBookmarksSidebar",
+ SidebarUI.currentID == "viewBookmarksSidebar"
+ );
+ this.updateLabel("BMB_viewBookmarksToolbar", !this.toolbar.collapsed);
+ },
+
+ updateLabel(elementId, visible) {
+ let element = PanelMultiView.getViewNode(document, elementId);
+ let l10nID = element.getAttribute("data-l10n-id");
+ document.l10n.setAttributes(element, l10nID, { isVisible: !!visible });
+ },
+
+ toggleBookmarksToolbar(reason) {
+ let newState = this.toolbar.collapsed ? "always" : "never";
+ Services.prefs.setCharPref(
+ "browser.toolbars.bookmarks.visibility",
+ // See firefox.js for possible values
+ newState
+ );
+
+ CustomizableUI.setToolbarVisibility(this.toolbar.id, newState, false);
+ BrowserUsageTelemetry.recordToolbarVisibility(
+ this.toolbar.id,
+ newState,
+ reason
+ );
+ },
+
+ isOnNewTabPage({ currentURI }) {
+ // Prevent loading AboutNewTab.jsm during startup path if it
+ // is only the newTabURL getter we are interested in.
+ let newTabURL = Cu.isModuleLoaded("resource:///modules/AboutNewTab.jsm")
+ ? AboutNewTab.newTabURL
+ : "about:newtab";
+ // Don't treat a custom "about:blank" new tab URL as the "New Tab Page"
+ // due to about:blank being used in different contexts and the
+ // difficulty in determining if the eventual page load is
+ // about:blank or if the about:blank load is just temporary.
+ if (newTabURL == "about:blank") {
+ newTabURL = "about:newtab";
+ }
+ let newTabURLs = [newTabURL, "about:home"];
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ newTabURLs.push("about:privatebrowsing");
+ }
+ return newTabURLs.some(uri => currentURI?.spec.startsWith(uri));
+ },
+
+ buildBookmarksToolbarSubmenu(toolbar) {
+ let alwaysShowMenuItem = document.createXULElement("menuitem");
+ let alwaysHideMenuItem = document.createXULElement("menuitem");
+ let showOnNewTabMenuItem = document.createXULElement("menuitem");
+ let menuPopup = document.createXULElement("menupopup");
+ menuPopup.append(
+ alwaysShowMenuItem,
+ alwaysHideMenuItem,
+ showOnNewTabMenuItem
+ );
+ let menu = document.createXULElement("menu");
+ menu.appendChild(menuPopup);
+
+ menu.setAttribute("label", toolbar.getAttribute("toolbarname"));
+ menu.setAttribute("id", "toggle_" + toolbar.id);
+ menu.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
+ menu.setAttribute("toolbarId", toolbar.id);
+
+ // Used by the Places context menu in the Bookmarks Toolbar
+ // when nothing is selected
+ menu.setAttribute("selectiontype", "none");
+
+ MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl");
+ let menuItems = [
+ [
+ showOnNewTabMenuItem,
+ "toolbar-context-menu-bookmarks-toolbar-on-new-tab-2",
+ "newtab",
+ ],
+ [
+ alwaysShowMenuItem,
+ "toolbar-context-menu-bookmarks-toolbar-always-show-2",
+ "always",
+ ],
+ [
+ alwaysHideMenuItem,
+ "toolbar-context-menu-bookmarks-toolbar-never-show-2",
+ "never",
+ ],
+ ];
+ menuItems.map(([menuItem, l10nId, visibilityEnum]) => {
+ document.l10n.setAttributes(menuItem, l10nId);
+ menuItem.setAttribute("type", "radio");
+ // The persisted state of the PersonalToolbar is stored in
+ // "browser.toolbars.bookmarks.visibility".
+ menuItem.setAttribute(
+ "checked",
+ gBookmarksToolbarVisibility == visibilityEnum
+ );
+ // Identify these items for "onViewToolbarCommand" so
+ // we know to check the visibilityEnum value.
+ menuItem.dataset.bookmarksToolbarVisibility = true;
+ menuItem.dataset.visibilityEnum = visibilityEnum;
+ menuItem.addEventListener("command", onViewToolbarCommand);
+ });
+ let menuItemForNextStateFromKbShortcut =
+ gBookmarksToolbarVisibility == "never"
+ ? alwaysShowMenuItem
+ : alwaysHideMenuItem;
+ menuItemForNextStateFromKbShortcut.setAttribute(
+ "key",
+ "viewBookmarksToolbarKb"
+ );
+
+ return menu;
+ },
+
+ /**
+ * Check if we need to make the empty toolbar message `hidden`.
+ * We'll have it unhidden during startup, to make sure the toolbar
+ * has height, and we'll unhide it if there is nothing else on the toolbar.
+ * We hide it in customize mode, unless there's nothing on the toolbar.
+ */
+ updateEmptyToolbarMessage() {
+ let emptyMsg = document.getElementById("personal-toolbar-empty");
+
+ // If the bookmarks are here but it's early in startup, show the message.
+ // It'll get made visibility: hidden early in startup anyway - it's just
+ // to ensure the toolbar has height.
+ if (!this.toolbar.hasAttribute("initialized")) {
+ emptyMsg.hidden = false;
+ emptyMsg.setAttribute("nowidth", "");
+ return;
+ }
+
+ // Do we have visible kids?
+ let hasVisibleChildren = !!this.toolbar.querySelector(
+ `:scope > toolbarpaletteitem > toolbarbutton:not([hidden]),
+ :scope > toolbarpaletteitem > toolbaritem:not([hidden], #personal-bookmarks),
+ :scope > toolbarbutton:not([hidden]),
+ :scope > toolbaritem:not([hidden], #personal-bookmarks)`
+ );
+
+ if (!hasVisibleChildren) {
+ // Hmm, apparently not. Check for bookmarks or customize mode:
+ let bookmarksToolbarItemsPlacement = CustomizableUI.getPlacementOfWidget(
+ "personal-bookmarks"
+ );
+ let bookmarksItemInToolbar =
+ bookmarksToolbarItemsPlacement?.area == CustomizableUI.AREA_BOOKMARKS;
+
+ hasVisibleChildren =
+ bookmarksItemInToolbar &&
+ (this._isCustomizing ||
+ !!PlacesUtils.getChildCountForFolder(
+ PlacesUtils.bookmarks.toolbarGuid
+ ));
+ }
+ emptyMsg.hidden = hasVisibleChildren;
+ emptyMsg.toggleAttribute("nowidth", !hasVisibleChildren);
+ },
+
+ openLibraryIfLinkClicked(event) {
+ if (
+ ((event.type == "click" && event.button == 0) ||
+ (event.type == "keydown" && event.keyCode == KeyEvent.DOM_VK_RETURN)) &&
+ event.target.localName == "a"
+ ) {
+ PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
+ }
+ },
+
+ attachPlacesView(event, node) {
+ // If the view is already there, bail out early.
+ if (node.parentNode._placesView) {
+ return;
+ }
+
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.menuGuid}`, {
+ extraClasses: {
+ entry: "subviewbutton",
+ footer: "panel-subview-footer",
+ },
+ insertionPoint: ".panel-subview-footer",
+ });
+ },
+
+ // Set by sync after syncing bookmarks successfully once.
+ MOBILE_BOOKMARKS_PREF: "browser.bookmarks.showMobileBookmarks",
+
+ _shouldShowMobileBookmarks() {
+ return Services.prefs.getBoolPref(this.MOBILE_BOOKMARKS_PREF, false);
+ },
+
+ _initMobileBookmarks(mobileMenuItem) {
+ mobileMenuItem.hidden = !this._shouldShowMobileBookmarks();
+ },
+
+ _uninitView: function BUI__uninitView() {
+ // When an element with a placesView attached is removed and re-inserted,
+ // XBL reapplies the binding causing any kind of issues and possible leaks,
+ // so kill current view and let popupshowing generate a new one.
+ if (this.button._placesView) {
+ this.button._placesView.uninit();
+ }
+ // Also uninit the main menubar placesView, since it would have the same
+ // issues.
+ let menubar = document.getElementById("bookmarksMenu");
+ if (menubar && menubar._placesView) {
+ menubar._placesView.uninit();
+ }
+
+ // We have to do the same thing for the "special" views underneath the
+ // the bookmarks menu.
+ const kSpecialViewNodeIDs = [
+ "BMB_bookmarksToolbar",
+ "BMB_unsortedBookmarks",
+ ];
+ for (let viewNodeID of kSpecialViewNodeIDs) {
+ let elem = document.getElementById(viewNodeID);
+ if (elem && elem._placesView) {
+ elem._placesView.uninit();
+ }
+ }
+ },
+
+ onCustomizeStart: function BUI_customizeStart(aWindow) {
+ if (aWindow == window) {
+ this._uninitView();
+ this._isCustomizing = true;
+
+ this.updateEmptyToolbarMessage();
+
+ if (!gBookmarksToolbar2h2020) {
+ return;
+ }
+
+ let isVisible =
+ Services.prefs.getCharPref(
+ "browser.toolbars.bookmarks.visibility",
+ "newtab"
+ ) != "never";
+ // Temporarily show the bookmarks toolbar in Customize mode if
+ // the toolbar isn't set to Never. We don't have to worry about
+ // hiding when leaving customize mode since the toolbar will
+ // hide itself on location change.
+ setToolbarVisibility(this.toolbar, isVisible, false);
+ }
+ },
+
+ onWidgetAdded: function BUI_widgetAdded(aWidgetId, aArea) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ if (aArea == CustomizableUI.AREA_BOOKMARKS) {
+ this.updateEmptyToolbarMessage();
+ }
+ },
+
+ onWidgetRemoved: function BUI_widgetRemoved(aWidgetId, aOldArea) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ if (aOldArea == CustomizableUI.AREA_BOOKMARKS) {
+ this.updateEmptyToolbarMessage();
+ }
+ },
+
+ onWidgetReset: function BUI_widgetReset(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ _onWidgetWasMoved: function BUI_widgetWasMoved() {
+ // If we're moved outside of customize mode, we need to uninit
+ // our view so it gets reconstructed.
+ if (!this._isCustomizing) {
+ this._uninitView();
+ }
+ },
+
+ onCustomizeEnd: function BUI_customizeEnd(aWindow) {
+ if (aWindow == window) {
+ this._isCustomizing = false;
+ this.updateEmptyToolbarMessage();
+ }
+ },
+
+ init() {
+ CustomizableUI.addListener(this);
+ this.updateEmptyToolbarMessage();
+ this.star.addEventListener("mouseover", this, { once: true });
+ },
+
+ _hasBookmarksObserver: false,
+ _itemGuids: new Set(),
+ uninit: function BUI_uninit() {
+ this.updateBookmarkPageMenuItem(true);
+ CustomizableUI.removeListener(this);
+
+ this.star.removeEventListener("mouseover", this);
+
+ this._uninitView();
+
+ if (this._hasBookmarksObserver) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ PlacesUtils.observers.removeListener(
+ ["bookmark-added", "bookmark-removed"],
+ this.handlePlacesEvents
+ );
+ }
+
+ if (this._pendingUpdate) {
+ delete this._pendingUpdate;
+ }
+ },
+
+ onLocationChange: function BUI_onLocationChange() {
+ if (this._uri && gBrowser.currentURI.equals(this._uri)) {
+ return;
+ }
+ this.updateStarState();
+ },
+
+ updateStarState: function BUI_updateStarState() {
+ this._uri = gBrowser.currentURI;
+ this._itemGuids.clear();
+ let guids = new Set();
+
+ // those objects are use to check if we are in the current iteration before
+ // returning any result.
+ let pendingUpdate = (this._pendingUpdate = {});
+
+ PlacesUtils.bookmarks
+ .fetch({ url: this._uri }, b => guids.add(b.guid), { concurrent: true })
+ .catch(Cu.reportError)
+ .then(() => {
+ if (pendingUpdate != this._pendingUpdate) {
+ return;
+ }
+
+ // It's possible that "bookmark-added" gets called before the async statement
+ // calls back. For such an edge case, retain all unique entries from the
+ // array.
+ if (this._itemGuids.size > 0) {
+ this._itemGuids = new Set(...this._itemGuids, ...guids);
+ } else {
+ this._itemGuids = guids;
+ }
+
+ this._updateStar();
+
+ // Start observing bookmarks if needed.
+ if (!this._hasBookmarksObserver) {
+ try {
+ PlacesUtils.bookmarks.addObserver(this);
+ this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
+ PlacesUtils.observers.addListener(
+ ["bookmark-added", "bookmark-removed"],
+ this.handlePlacesEvents
+ );
+ this._hasBookmarksObserver = true;
+ } catch (ex) {
+ Cu.reportError(
+ "BookmarkingUI failed adding a bookmarks observer: " + ex
+ );
+ }
+ }
+
+ delete this._pendingUpdate;
+ });
+ },
+
+ _updateStar: function BUI__updateStar() {
+ let starred = this._itemGuids.size > 0;
+ if (!starred) {
+ this.star.removeAttribute("animate");
+ }
+
+ // Update the image for all elements.
+ for (let element of [
+ this.star,
+ document.getElementById("context-bookmarkpage"),
+ PanelMultiView.getViewNode(document, "panelMenuBookmarkThisPage"),
+ document.getElementById("pageAction-panel-bookmark"),
+ ]) {
+ if (!element) {
+ // The page action panel element may not have been created yet.
+ continue;
+ }
+ if (starred) {
+ element.setAttribute("starred", "true");
+ } else {
+ element.removeAttribute("starred");
+ }
+ }
+
+ if (!this.star) {
+ // The BOOKMARK_BUTTON_SHORTCUT exists only in browser.xhtml.
+ // Return early if we're not in this context, but still reset the
+ // Bookmark This Page items.
+ this.updateBookmarkPageMenuItem(true);
+ return;
+ }
+
+ // Update the tooltip for elements that require it.
+ let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT);
+ let l10nArgs = {
+ shortcut: ShortcutUtils.prettifyShortcut(shortcut),
+ };
+ document.l10n.setAttributes(
+ this.star,
+ starred ? "urlbar-star-edit-bookmark" : "urlbar-star-add-bookmark",
+ l10nArgs
+ );
+
+ // Update the Bookmark This Page menuitem when bookmarked state changes.
+ this.updateBookmarkPageMenuItem();
+
+ Services.obs.notifyObservers(
+ null,
+ "bookmark-icon-updated",
+ starred ? "starred" : "unstarred"
+ );
+ },
+
+ /**
+ * Update the "bookmark this page" menuitems on the menubar, panels, context
+ * menu and page actions.
+ * @param {boolean} [forceReset] passed when we're destroyed and the label
+ * should go back to the default (Bookmark This Page), for MacOS.
+ */
+ updateBookmarkPageMenuItem(forceReset = false) {
+ let isStarred = !forceReset && this._itemGuids.size > 0;
+ // Define the l10n id which will be used to localize elements
+ // that only require a label using the menubar.ftl messages.
+ let menuItemL10nId = isStarred
+ ? "menu-bookmark-edit"
+ : "menu-bookmark-this-page";
+
+ let menuItem = document.getElementById("menu_bookmarkThisPage");
+ if (menuItem) {
+ // Localize the menubar item.
+ document.l10n.setAttributes(menuItem, menuItemL10nId);
+ }
+
+ let panelMenuItemL10nId = isStarred
+ ? "library-bookmarks-bookmark-edit"
+ : "library-bookmarks-bookmark-this-page";
+ let panelMenuToolbarButton = PanelMultiView.getViewNode(
+ document,
+ "panelMenuBookmarkThisPage"
+ );
+ if (panelMenuToolbarButton) {
+ document.l10n.setAttributes(panelMenuToolbarButton, panelMenuItemL10nId);
+ }
+
+ // Localize the context menu item element.
+ let contextItem = document.getElementById("context-bookmarkpage");
+ if (contextItem) {
+ let shortcutElem = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT);
+ if (shortcutElem) {
+ let shortcut = ShortcutUtils.prettifyShortcut(shortcutElem);
+ let contextItemL10nId = isStarred
+ ? "main-context-menu-bookmark-change-with-shortcut"
+ : "main-context-menu-bookmark-add-with-shortcut";
+ let l10nArgs = { shortcut };
+ document.l10n.setAttributes(contextItem, contextItemL10nId, l10nArgs);
+ } else {
+ let contextItemL10nId = isStarred
+ ? "main-context-menu-bookmark-change"
+ : "main-context-menu-bookmark-add";
+ document.l10n.setAttributes(contextItem, contextItemL10nId);
+ }
+ }
+
+ // Update Page Actions.
+ if (document.getElementById("page-action-buttons")) {
+ // Fetch the label attribute value of the message and
+ // apply it on the star title.
+ //
+ // Note: This should be updated once bug 1608198 is fixed.
+ this._latestMenuItemL10nId = menuItemL10nId;
+ document.l10n.formatMessages([{ id: menuItemL10nId }]).then(l10n => {
+ // It's possible for this promise to be scheduled multiple times.
+ // In such a case, we'd like to avoid setting the title if there's
+ // a newer l10n id pending to be set.
+ if (this._latestMenuItemL10nId != menuItemL10nId) {
+ return;
+ }
+
+ // We assume that menuItemL10nId has a single attribute.
+ let label = l10n[0].attributes[0].value;
+
+ // Update the label, tooltip, and the starred state for the
+ // page action panel.
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(
+ PageActions.ACTION_ID_BOOKMARK
+ );
+ if (panelButton) {
+ panelButton.setAttribute("label", label);
+ }
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
+ PageActions.ACTION_ID_BOOKMARK
+ );
+ if (urlbarButton) {
+ urlbarButton.setAttribute("tooltiptext", label);
+ }
+ });
+ }
+ },
+
+ onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget) {
+ return;
+ }
+
+ this._initMobileBookmarks(document.getElementById("menu_mobileBookmarks"));
+ },
+
+ showSubView(anchor) {
+ this._showSubView(null, anchor);
+ },
+
+ _showSubView(
+ event,
+ anchor = document.getElementById(this.BOOKMARK_BUTTON_ID)
+ ) {
+ let view = PanelMultiView.getViewNode(document, "PanelUI-bookmarks");
+ view.addEventListener("ViewShowing", this);
+ view.addEventListener("ViewHiding", this);
+ anchor.setAttribute("closemenu", "none");
+ PanelUI.showSubView("PanelUI-bookmarks", anchor, event);
+ },
+
+ onCommand: function BUI_onCommand(aEvent) {
+ if (aEvent.target != aEvent.currentTarget) {
+ return;
+ }
+
+ // Handle special case when the button is in the panel.
+ if (
+ this.button.getAttribute("cui-areatype") == CustomizableUI.TYPE_MENU_PANEL
+ ) {
+ this._showSubView(aEvent);
+ return;
+ }
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID).forWindow(
+ window
+ );
+ if (widget.overflowed) {
+ // Close the overflow panel because the Edit Bookmark panel will appear.
+ widget.node.removeAttribute("closemenu");
+ }
+ this.onStarCommand(aEvent);
+ },
+
+ onStarCommand(aEvent) {
+ // Ignore non-left clicks on the star, or if we are updating its state.
+ if (
+ !this._pendingUpdate &&
+ (aEvent.type != "click" || aEvent.button == 0)
+ ) {
+ let isBookmarked = this._itemGuids.size > 0;
+ if (!isBookmarked) {
+ BrowserUtils.setToolbarButtonHeightProperty(this.star);
+ // there are no other animations on this element, so we can simply
+ // listen for animationend with the "once" option to clean up
+ let animatableBox = document.getElementById(
+ "star-button-animatable-box"
+ );
+ animatableBox.addEventListener(
+ "animationend",
+ event => {
+ this.star.removeAttribute("animate");
+ },
+ { once: true }
+ );
+ this.star.setAttribute("animate", "true");
+ }
+ PlacesCommandHook.bookmarkPage();
+ }
+ },
+
+ handleEvent: function BUI_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mouseover":
+ this.star.setAttribute("preloadanimations", "true");
+ break;
+ case "ViewShowing":
+ this.onPanelMenuViewShowing(aEvent);
+ break;
+ case "ViewHiding":
+ this.onPanelMenuViewHiding(aEvent);
+ break;
+ }
+ },
+
+ onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) {
+ let panelview = aEvent.target;
+
+ // Get all statically placed buttons to supply them with keyboard shortcuts.
+ let staticButtons = panelview.getElementsByTagName("toolbarbutton");
+ for (let i = 0, l = staticButtons.length; i < l; ++i) {
+ CustomizableUI.addShortcut(staticButtons[i]);
+ }
+
+ // Setup the Places view.
+ // We restrict the amount of results to 42. Not 50, but 42. Why? Because 42.
+ let query =
+ "place:queryType=" +
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
+ "&sort=" +
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
+ "&maxResults=42&excludeQueries=1";
+
+ this._panelMenuView = new PlacesPanelview(
+ document.getElementById("panelMenu_bookmarksMenu"),
+ panelview,
+ query
+ );
+ panelview.removeEventListener("ViewShowing", this);
+ },
+
+ onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) {
+ this._panelMenuView.uninit();
+ delete this._panelMenuView;
+ aEvent.target.removeEventListener("ViewHiding", this);
+ },
+
+ showBookmarkingTools(triggerNode) {
+ let placement = CustomizableUI.getPlacementOfWidget(
+ this.BOOKMARK_BUTTON_ID
+ );
+ this.updateLabel(
+ "panelMenu_toggleBookmarksMenu",
+ placement && placement.area == CustomizableUI.AREA_NAVBAR
+ );
+ this.updateLabel(
+ "panelMenu_viewBookmarksSidebar",
+ SidebarUI.currentID == "viewBookmarksSidebar"
+ );
+ this.updateLabel("panelMenu_viewBookmarksToolbar", !this.toolbar.collapsed);
+ PanelUI.showSubView("PanelUI-bookmarkingTools", triggerNode);
+ },
+
+ toggleMenuButtonInToolbar(triggerNode) {
+ let placement = CustomizableUI.getPlacementOfWidget(
+ this.BOOKMARK_BUTTON_ID
+ );
+ const area = CustomizableUI.AREA_NAVBAR;
+ if (!placement) {
+ // Button is in the palette, so we can move it to the navbar.
+ let pos;
+ let widgetIDs = CustomizableUI.getWidgetIdsInArea(
+ CustomizableUI.AREA_NAVBAR
+ );
+ // If there's a spring inside the navbar, find it and use that as the
+ // placement marker.
+ let lastSpringID = null;
+ for (let i = widgetIDs.length - 1; i >= 0; --i) {
+ let id = widgetIDs[i];
+ if (CustomizableUI.isSpecialWidget(id) && /spring/.test(id)) {
+ lastSpringID = id;
+ break;
+ }
+ }
+ if (lastSpringID) {
+ pos = CustomizableUI.getPlacementOfWidget(lastSpringID).position + 1;
+ } else {
+ // Next alternative is to use the searchbar as the placement marker.
+ const searchWidgetID = "search-container";
+ if (widgetIDs.includes(searchWidgetID)) {
+ pos =
+ CustomizableUI.getPlacementOfWidget(searchWidgetID).position + 1;
+ } else {
+ // Last alternative is to use the navbar as the placement marker.
+ pos =
+ CustomizableUI.getPlacementOfWidget("urlbar-container").position +
+ 1;
+ }
+ }
+
+ CustomizableUI.addWidgetToArea(this.BOOKMARK_BUTTON_ID, area, pos);
+ BrowserUsageTelemetry.recordWidgetChange(
+ this.BOOKMARK_BUTTON_ID,
+ area,
+ "bookmark-tools"
+ );
+ } else {
+ // Move it back to the palette.
+ CustomizableUI.removeWidgetFromArea(this.BOOKMARK_BUTTON_ID);
+ BrowserUsageTelemetry.recordWidgetChange(
+ this.BOOKMARK_BUTTON_ID,
+ null,
+ "bookmark-tools"
+ );
+ }
+ triggerNode.setAttribute("checked", !placement);
+ updateToggleControlLabel(triggerNode);
+ },
+
+ handlePlacesEvents(aEvents) {
+ for (let ev of aEvents) {
+ switch (ev.type) {
+ case "bookmark-added":
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemGuids.size == 0) {
+ if (ev.url && ev.url == this._uri.spec) {
+ // If a new bookmark has been added to the tracked uri, register it.
+ if (!this._itemGuids.has(ev.guid)) {
+ this._itemGuids.add(ev.guid);
+ this._updateStar();
+ }
+ }
+ }
+ break;
+ case "bookmark-removed":
+ // If one of the tracked bookmarks has been removed, unregister it.
+ if (this._itemGuids.has(ev.guid)) {
+ this._itemGuids.delete(ev.guid);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemGuids.size == 0) {
+ this._updateStar();
+ }
+ }
+ break;
+ }
+
+ if (ev.parentGuid === PlacesUtils.bookmarks.unfiledGuid) {
+ this.maybeShowOtherBookmarksFolder();
+ }
+ this.updateEmptyToolbarMessage();
+ }
+ },
+
+ onItemChanged(
+ aItemId,
+ aProperty,
+ aIsAnnotationProperty,
+ aNewValue,
+ aLastModified,
+ aItemType,
+ aParentId,
+ aGuid
+ ) {
+ if (aProperty == "uri") {
+ // If the changed bookmark was tracked, check if it is now pointing to
+ // a different uri and unregister it.
+ if (this._itemGuids.has(aGuid) && aNewValue != this._uri.spec) {
+ this._itemGuids.delete(aGuid);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemGuids.size == 0) {
+ this._updateStar();
+ }
+ } else if (!this._itemGuids.has(aGuid) && aNewValue == this._uri.spec) {
+ // If another bookmark is now pointing to the tracked uri, register it.
+ this._itemGuids.add(aGuid);
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemGuids.size == 1) {
+ this._updateStar();
+ }
+ }
+ }
+ },
+
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onBeforeItemRemoved() {},
+ onItemMoved(
+ aItemId,
+ aProperty,
+ aIsAnnotationProperty,
+ aNewValue,
+ aLastModified,
+ aItemType,
+ aGuid,
+ oldParentGuid,
+ newParentGuid
+ ) {
+ let hasMovedToOrOutOfOtherBookmarks =
+ newParentGuid === PlacesUtils.bookmarks.unfiledGuid ||
+ oldParentGuid === PlacesUtils.bookmarks.unfiledGuid;
+ if (hasMovedToOrOutOfOtherBookmarks) {
+ this.maybeShowOtherBookmarksFolder();
+ }
+
+ let hasMovedToToolbar = newParentGuid === PlacesUtils.bookmarks.toolbarGuid;
+ let hasMovedOutOfToolbar =
+ oldParentGuid === PlacesUtils.bookmarks.toolbarGuid;
+ if (hasMovedToToolbar || hasMovedOutOfToolbar) {
+ this.updateEmptyToolbarMessage();
+ }
+ },
+
+ onWidgetUnderflow(aNode, aContainer) {
+ let win = aNode.ownerGlobal;
+ if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window) {
+ return;
+ }
+
+ // The view gets broken by being removed and reinserted. Uninit
+ // here so popupshowing will generate a new one:
+ this._uninitView();
+ },
+
+ async maybeShowOtherBookmarksFolder() {
+ // PlacesToolbar._placesView can be undefined if the toolbar isn't initialized,
+ // collapsed, or hidden in some other way.
+ let toolbar = document.getElementById("PlacesToolbar");
+
+ // Only show the "Other Bookmarks" folder in the toolbar if pref is enabled.
+ if (!gBookmarksToolbar2h2020 || !toolbar?._placesView) {
+ return;
+ }
+
+ let unfiledGuid = PlacesUtils.bookmarks.unfiledGuid;
+ let numberOfBookmarks = PlacesUtils.getChildCountForFolder(unfiledGuid);
+ let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+ let otherBookmarks = document.getElementById("OtherBookmarks");
+
+ if (
+ numberOfBookmarks > 0 &&
+ SHOW_OTHER_BOOKMARKS &&
+ placement?.area == CustomizableUI.AREA_BOOKMARKS
+ ) {
+ let result = PlacesUtils.getFolderContents(unfiledGuid);
+ let node = result.root;
+
+ // Build the "Other Bookmarks" button if it doesn't exist.
+ if (!otherBookmarks) {
+ this.buildOtherBookmarksFolder(node);
+ }
+
+ otherBookmarks = document.getElementById("OtherBookmarks");
+ otherBookmarks.hidden = false;
+ } else if (otherBookmarks) {
+ otherBookmarks.hidden = true;
+ }
+ },
+
+ buildShowOtherBookmarksMenuItem() {
+ let unfiledGuid = PlacesUtils.bookmarks.unfiledGuid;
+ let numberOfBookmarks = PlacesUtils.getChildCountForFolder(unfiledGuid);
+
+ if (!gBookmarksToolbar2h2020 || numberOfBookmarks < 1) {
+ return null;
+ }
+
+ let menuItem = document.createXULElement("menuitem");
+
+ menuItem.setAttribute("id", "show-other-bookmarks_PersonalToolbar");
+ menuItem.setAttribute("toolbarId", "PersonalToolbar");
+ menuItem.setAttribute("type", "checkbox");
+ menuItem.setAttribute("checked", SHOW_OTHER_BOOKMARKS);
+ menuItem.setAttribute("selectiontype", "none|single");
+
+ MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl");
+ document.l10n.setAttributes(
+ menuItem,
+ "toolbar-context-menu-bookmarks-show-other-bookmarks"
+ );
+ menuItem.addEventListener("command", () => {
+ Services.prefs.setBoolPref(
+ "browser.toolbars.bookmarks.showOtherBookmarks",
+ !SHOW_OTHER_BOOKMARKS
+ );
+ });
+
+ return menuItem;
+ },
+
+ buildOtherBookmarksFolder(node) {
+ let otherBookmarksButton = document.createXULElement("toolbarbutton");
+ otherBookmarksButton.setAttribute("type", "menu");
+ otherBookmarksButton.setAttribute("container", "true");
+ otherBookmarksButton.setAttribute(
+ "onpopupshowing",
+ "document.getElementById('PlacesToolbar')._placesView._onOtherBookmarksPopupShowing(event);"
+ );
+ otherBookmarksButton.id = "OtherBookmarks";
+ otherBookmarksButton.className = "bookmark-item";
+ otherBookmarksButton.hidden = "true";
+
+ MozXULElement.insertFTLIfNeeded("browser/places.ftl");
+ document.l10n.setAttributes(otherBookmarksButton, "other-bookmarks-folder");
+
+ let otherBookmarksPopup = document.createXULElement("menupopup", {
+ is: "places-popup",
+ });
+ otherBookmarksPopup.setAttribute("placespopup", "true");
+ otherBookmarksPopup.setAttribute("context", "placesContext");
+ otherBookmarksPopup.id = "OtherBookmarksPopup";
+
+ otherBookmarksPopup._placesNode = PlacesUtils.asContainer(node);
+ otherBookmarksButton._placesNode = PlacesUtils.asContainer(node);
+
+ otherBookmarksButton.appendChild(otherBookmarksPopup);
+
+ let chevronButton = document.getElementById("PlacesChevron");
+ chevronButton.parentNode.append(otherBookmarksButton);
+
+ let placesToolbar = document.getElementById("PlacesToolbar");
+ placesToolbar._placesView._otherBookmarks = otherBookmarksButton;
+ placesToolbar._placesView._otherBookmarksPopup = otherBookmarksPopup;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]),
+};
diff --git a/browser/base/content/browser-safebrowsing.js b/browser/base/content/browser-safebrowsing.js
new file mode 100644
index 0000000000..01d8063ffb
--- /dev/null
+++ b/browser/base/content/browser-safebrowsing.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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+var gSafeBrowsing = {
+ setReportPhishingMenu() {
+ // In order to detect whether or not we're at the phishing warning
+ // page, we have to check the documentURI instead of the currentURI.
+ // This is because when the DocShell loads an error page, the
+ // currentURI stays at the original target, while the documentURI
+ // will point to the internal error page we loaded instead.
+ var docURI = gBrowser.selectedBrowser.documentURI;
+ var isPhishingPage =
+ docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked");
+
+ // Show/hide the appropriate menu item.
+ const reportMenu = document.getElementById(
+ "menu_HelpPopup_reportPhishingtoolmenu"
+ );
+ reportMenu.hidden = isPhishingPage;
+ const reportErrorMenu = document.getElementById(
+ "menu_HelpPopup_reportPhishingErrortoolmenu"
+ );
+ reportErrorMenu.hidden = !isPhishingPage;
+
+ // Now look at the currentURI to learn which page we were trying
+ // to browse to.
+ const uri = gBrowser.currentURI;
+ const isReportablePage =
+ uri && (uri.schemeIs("http") || uri.schemeIs("https"));
+
+ const disabledByPolicy = !Services.policies.isAllowed("feedbackCommands");
+
+ if (disabledByPolicy || isPhishingPage || !isReportablePage) {
+ reportMenu.setAttribute("disabled", "true");
+ } else {
+ reportMenu.removeAttribute("disabled");
+ }
+
+ if (disabledByPolicy || !isPhishingPage || !isReportablePage) {
+ reportErrorMenu.setAttribute("disabled", "true");
+ } else {
+ reportErrorMenu.removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Used to report a phishing page or a false positive
+ *
+ * @param name
+ * String One of "PhishMistake", "MalwareMistake", or "Phish"
+ * @param info
+ * Information about the reasons for blocking the resource.
+ * In the case false positive, it may contain SafeBrowsing
+ * matching list and provider of the list
+ * @return String the report phishing URL.
+ */
+ getReportURL(name, info) {
+ let reportInfo = info;
+ if (!reportInfo) {
+ let pageUri = gBrowser.currentURI;
+
+ // Remove the query to avoid including potentially sensitive data
+ if (pageUri instanceof Ci.nsIURL) {
+ pageUri = pageUri
+ .mutate()
+ .setQuery("")
+ .finalize();
+ }
+
+ reportInfo = { uri: pageUri.asciiSpec };
+ }
+ return SafeBrowsing.getReportURL(name, reportInfo);
+ },
+};
diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc
new file mode 100644
index 0000000000..54d988951e
--- /dev/null
+++ b/browser/base/content/browser-sets.inc
@@ -0,0 +1,388 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+#define XP_GNOME 1
+#endif
+#endif
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundle_shell" src="chrome://browser/locale/shellservice.properties"/>
+ </stringbundleset>
+
+ <commandset id="mainCommandSet">
+ <command id="cmd_newNavigator" oncommand="OpenBrowserWindow()"/>
+ <command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" />
+ <command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" />
+
+ <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab(event);"/>
+ <command id="cmd_newNavigatorTabNoEvent" oncommand="BrowserOpenTab();"/>
+ <command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/>
+ <command id="Browser:SavePage" oncommand="saveBrowser(gBrowser.selectedBrowser);"/>
+
+ <command id="Browser:SendLink"
+ oncommand="MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);"/>
+
+ <command id="cmd_pageSetup" oncommand="PrintUtils.showPageSetup();"/>
+ <command id="cmd_print" oncommand="PrintUtils.startPrintWindow('cmd_print', gBrowser.selectedBrowser.browsingContext);"/>
+ <!-- cmd_print_kb only exists so that we can differentiate menu prints from keyboard shortcut prints -->
+ <command id="cmd_print_kb" oncommand="PrintUtils.startPrintWindow('cmd_print_kb', gBrowser.selectedBrowser.browsingContext);"/>
+ <command id="cmd_printPreview" oncommand="PrintUtils.printPreview('cmd_printPreview', PrintPreviewListener).catch(() => {});"/>
+ <command id="cmd_file_importFromAnotherBrowser" oncommand="MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_FILE_MENU]);"/>
+ <command id="cmd_help_importFromAnotherBrowser" oncommand="MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_HELP_MENU]);"/>
+ <command id="cmd_close" oncommand="BrowserCloseTabOrWindow(event);"/>
+ <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()"/>
+ <command id="cmd_toggleMute" oncommand="gBrowser.toggleMuteAudioOnMultiSelectedTabs(gBrowser.selectedTab)"/>
+ <command id="cmd_CustomizeToolbars" oncommand="gCustomizeMode.enter()"/>
+ <command id="cmd_toggleOfflineStatus" oncommand="BrowserOffline.toggleOfflineStatus();"/>
+ <command id="cmd_quitApplication" oncommand="goQuitApplication()"/>
+
+ <command id="View:PageSource" oncommand="BrowserViewSource(window.gBrowser.selectedBrowser);"/>
+ <command id="View:PageInfo" oncommand="BrowserPageInfo();"/>
+ <command id="View:FullScreen" oncommand="BrowserFullScreen();"/>
+ <command id="View:ReaderView" oncommand="AboutReaderParent.toggleReaderMode(event);"/>
+ <command id="View:PictureInPicture" oncommand="PictureInPicture.onCommand(event);"/>
+ <command id="cmd_find" oncommand="gLazyFindCommand('onFindCommand')"/>
+ <command id="cmd_findAgain" oncommand="gLazyFindCommand('onFindAgainCommand', false)"/>
+ <command id="cmd_findPrevious" oncommand="gLazyFindCommand('onFindAgainCommand', true)"/>
+#ifdef XP_MACOSX
+ <command id="cmd_findSelection" oncommand="gLazyFindCommand('onFindSelectionCommand')"/>
+#endif
+ <!-- work-around bug 392512 -->
+ <command id="Browser:AddBookmarkAs"
+ oncommand="PlacesCommandHook.bookmarkPage();"/>
+ <command id="Browser:BookmarkAllTabs"
+ oncommand="PlacesUIUtils.showBookmarkPagesDialog(PlacesCommandHook.uniqueCurrentPages);"/>
+ <command id="Browser:Back" oncommand="BrowserBack();" disabled="true"/>
+ <command id="Browser:BackOrBackDuplicate" oncommand="BrowserBack(event);" disabled="true">
+ <observes element="Browser:Back" attribute="disabled"/>
+ </command>
+ <command id="Browser:Forward" oncommand="BrowserForward();" disabled="true"/>
+ <command id="Browser:ForwardOrForwardDuplicate" oncommand="BrowserForward(event);" disabled="true">
+ <observes element="Browser:Forward" attribute="disabled"/>
+ </command>
+ <command id="Browser:Stop" oncommand="BrowserStop();" disabled="true"/>
+ <command id="Browser:Reload" oncommand="if (event.shiftKey) BrowserReloadSkipCache(); else BrowserReload()" disabled="true"/>
+ <command id="Browser:ReloadOrDuplicate" oncommand="BrowserReloadOrDuplicate(event)" disabled="true">
+ <observes element="Browser:Reload" attribute="disabled"/>
+ </command>
+ <command id="Browser:ReloadSkipCache" oncommand="BrowserReloadSkipCache()" disabled="true">
+ <observes element="Browser:Reload" attribute="disabled"/>
+ </command>
+ <command id="Browser:NextTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(1, true);"/>
+ <command id="Browser:PrevTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(-1, true);"/>
+ <command id="Browser:ShowAllTabs" oncommand="gTabsPanel.showAllTabsPanel();"/>
+ <command id="cmd_fullZoomReduce" oncommand="FullZoom.reduce()"/>
+ <command id="cmd_fullZoomEnlarge" oncommand="FullZoom.enlarge()"/>
+ <command id="cmd_fullZoomReset" oncommand="FullZoom.reset(); FullZoom.resetScalingZoom();"/>
+ <command id="cmd_fullZoomToggle" oncommand="ZoomManager.toggleZoom();"/>
+ <command id="cmd_gestureRotateLeft" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
+ <command id="cmd_gestureRotateRight" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
+ <command id="cmd_gestureRotateEnd" oncommand="gGestureSupport.rotateEnd()"/>
+ <command id="Browser:OpenLocation" oncommand="openLocation(event);"/>
+ <command id="Browser:RestoreLastSession" oncommand="SessionStore.restoreLastSession();" disabled="true"/>
+ <command id="Browser:NewUserContextTab" oncommand="openNewUserContextTab(event.sourceEvent);"/>
+ <command id="Browser:OpenAboutContainers" oncommand="openPreferences('paneContainers');"/>
+ <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
+ <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
+ <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
+ <command id="Tools:Sanitize" oncommand="Sanitizer.showUI(window);"/>
+ <command id="Tools:PrivateBrowsing"
+ oncommand="OpenBrowserWindow({private: true});"/>
+#ifdef NIGHTLY_BUILD
+ <command id="Tools:FissionWindow"
+ oncommand="OpenBrowserWindow({fission: true, private: !!window?.browsingContext?.usePrivateBrowsing});"
+ hidden="true"/>
+ <command id="Tools:NonFissionWindow"
+ oncommand="OpenBrowserWindow({fission: false, private: !!window?.browsingContext?.usePrivateBrowsing});"
+ hidden="true"/>
+#endif
+ <command id="History:UndoCloseTab" oncommand="undoCloseTab();" data-l10n-args='{"tabCount": 1}'/>
+ <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
+
+ <command id="wrCaptureCmd" oncommand="gGfxUtils.webrenderCapture();" disabled="true"/>
+ <command id="wrToggleCaptureSequenceCmd" oncommand="gGfxUtils.toggleWebrenderCaptureSequence();" disabled="true"/>
+#ifdef NIGHTLY_BUILD
+ <command id="windowRecordingCmd" oncommand="gGfxUtils.toggleWindowRecording();"/>
+#endif
+#ifdef XP_MACOSX
+ <command id="minimizeWindow"
+ data-l10n-id="window-minimize-command"
+ oncommand="window.minimize();" />
+ <command id="zoomWindow"
+ data-l10n-id="window-zoom-command"
+ oncommand="zoomWindow();" />
+#endif
+ </commandset>
+
+#include ../../components/places/content/placesCommands.inc.xhtml
+
+ <keyset id="mainKeyset">
+ <key id="key_newNavigator"
+ data-l10n-id="window-new-shortcut"
+ command="cmd_newNavigator"
+ modifiers="accel" reserved="true"/>
+ <key id="key_newNavigatorTab" data-l10n-id="tab-new-shortcut" modifiers="accel"
+ command="cmd_newNavigatorTabNoEvent" reserved="true"/>
+ <key id="focusURLBar" data-l10n-id="location-open-shortcut" command="Browser:OpenLocation"
+ modifiers="accel"/>
+#ifndef XP_MACOSX
+ <key id="focusURLBar2" data-l10n-id="location-open-shortcut-alt" command="Browser:OpenLocation"
+ modifiers="alt"/>
+#endif
+
+#
+# Search Command Key Logic works like this:
+#
+# Unix: Ctrl+K (cross platform binding)
+# Ctrl+J (in case of emacs Ctrl-K conflict)
+# Mac: Cmd+K (cross platform binding)
+# Cmd+Opt+F (platform convention)
+# Win: Ctrl+K (cross platform binding)
+# Ctrl+E (IE compat)
+#
+# We support Ctrl+K on all platforms now and advertise it in the menu since it is
+# our standard - it is a "safe" choice since it is near no harmful keys like "W" as
+# "E" is. People mourning the loss of Ctrl+K for emacs compat can switch their GTK
+# system setting to use emacs emulation, and we should respect it. Focus-Search-Box
+# is a fundamental keybinding and we are maintaining a XP binding so that it is easy
+# for people to switch to Linux.
+#
+ <key id="key_search" data-l10n-id="search-focus-shortcut" command="Tools:Search" modifiers="accel"/>
+ <key id="key_search2"
+#ifdef XP_MACOSX
+ data-l10n-id="find-shortcut"
+ modifiers="accel,alt"
+#else
+ data-l10n-id="search-focus-shortcut-alt"
+ modifiers="accel"
+#endif
+ command="Tools:Search"/>
+ <key id="key_openDownloads"
+ data-l10n-id="downloads-shortcut"
+#ifdef XP_GNOME
+ modifiers="accel,shift"
+#else
+ modifiers="accel"
+#endif
+ command="Tools:Downloads"/>
+ <key id="key_openAddons" data-l10n-id="addons-shortcut" command="Tools:Addons" modifiers="accel,shift"/>
+ <key id="openFileKb" data-l10n-id="file-open-shortcut" command="Browser:OpenFile" modifiers="accel"/>
+ <key id="key_savePage" data-l10n-id="save-page-shortcut" command="Browser:SavePage" modifiers="accel"/>
+ <key id="printKb" data-l10n-id="print-shortcut" command="cmd_print_kb" modifiers="accel"/>
+ <key id="key_close" data-l10n-id="close-shortcut" command="cmd_close" modifiers="accel" reserved="true"/>
+ <key id="key_closeWindow" data-l10n-id="close-shortcut" command="cmd_closeWindow" modifiers="accel,shift" reserved="true"/>
+ <key id="key_toggleMute" data-l10n-id="mute-toggle-shortcut" command="cmd_toggleMute" modifiers="control"/>
+ <key id="key_undo"
+ data-l10n-id="text-action-undo-shortcut"
+ modifiers="accel"/>
+ <key id="key_redo"
+#ifdef XP_UNIX
+ data-l10n-id="text-action-undo-shortcut"
+ modifiers="accel,shift"
+#else
+ data-l10n-id="text-action-redo-shortcut"
+ modifiers="accel"
+#endif
+ />
+ <key id="key_cut"
+ data-l10n-id="text-action-cut-shortcut"
+ modifiers="accel"/>
+ <key id="key_copy"
+ data-l10n-id="text-action-copy-shortcut"
+ modifiers="accel"/>
+ <key id="key_paste"
+ data-l10n-id="text-action-paste-shortcut"
+ modifiers="accel"/>
+ <key id="key_delete" keycode="VK_DELETE" command="cmd_delete" reserved="false"/>
+ <key id="key_selectAll" data-l10n-id="text-action-select-all-shortcut" modifiers="accel"/>
+
+ <key keycode="VK_BACK" command="cmd_handleBackspace" reserved="false"/>
+ <key keycode="VK_BACK" command="cmd_handleShiftBackspace" modifiers="shift" reserved="false"/>
+#ifndef XP_MACOSX
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="alt"/>
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="alt"/>
+#else
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="accel" />
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="accel" />
+#endif
+#ifdef XP_UNIX
+ <key id="goBackKb2" data-l10n-id="nav-back-shortcut-alt" command="Browser:Back" modifiers="accel"/>
+ <key id="goForwardKb2" data-l10n-id="nav-fwd-shortcut-alt" command="Browser:Forward" modifiers="accel"/>
+#endif
+ <key id="goHome" keycode="VK_HOME" oncommand="BrowserHome();" modifiers="alt"/>
+ <key keycode="VK_F5" command="Browser:Reload"/>
+#ifndef XP_MACOSX
+ <key id="showAllHistoryKb" data-l10n-id="history-show-all-shortcut" command="Browser:ShowAllHistory" modifiers="accel,shift"/>
+ <key keycode="VK_F5" command="Browser:ReloadSkipCache" modifiers="accel"/>
+ <key id="key_fullScreen" keycode="VK_F11" command="View:FullScreen"/>
+#else
+ <key id="showAllHistoryKb" data-l10n-id="history-show-all-shortcut-mac" command="Browser:ShowAllHistory" modifiers="accel"/>
+ <key id="key_fullScreen" data-l10n-id="full-screen-shortcut" command="View:FullScreen" modifiers="accel,control"/>
+ <key id="key_fullScreen_old" data-l10n-id="full-screen-shortcut" command="View:FullScreen" modifiers="accel,shift"/>
+ <key keycode="VK_F11" command="View:FullScreen"/>
+#endif
+ <key id="key_toggleReaderMode"
+ command="View:ReaderView"
+#ifdef XP_WIN
+ data-l10n-id="reader-mode-toggle-shortcut-windows"
+#else
+ data-l10n-id="reader-mode-toggle-shortcut-other"
+ modifiers="accel,alt"
+#endif
+ disabled="true"/>
+
+#ifndef XP_MACOSX
+ <key id="key_togglePictureInPicture" data-l10n-id="picture-in-picture-toggle-shortcut" command="View:PictureInPicture" modifiers="accel,shift"/>
+ <key data-l10n-id="picture-in-picture-toggle-shortcut-alt" command="View:PictureInPicture" modifiers="accel,shift"/>
+#else
+ <key data-l10n-id="picture-in-picture-toggle-shortcut-mac" command="View:PictureInPicture" modifiers="accel,alt,shift"/>
+ <key data-l10n-id="picture-in-picture-toggle-shortcut-mac-alt" command="View:PictureInPicture" modifiers="accel,alt,shift"/>
+#endif
+
+ <key data-l10n-id="nav-reload-shortcut" command="Browser:Reload" modifiers="accel" id="key_reload"/>
+ <key data-l10n-id="nav-reload-shortcut" command="Browser:ReloadSkipCache" modifiers="accel,shift" id="key_reload_skip_cache"/>
+ <key id="key_viewSource" data-l10n-id="page-source-shortcut" command="View:PageSource" modifiers="accel"/>
+#ifdef XP_MACOSX
+ <key id="key_viewSourceSafari" data-l10n-id="page-source-shortcut-safari" command="View:PageSource" modifiers="accel,alt"/>
+#endif
+ <key id="key_viewInfo" data-l10n-id="page-info-shortcut" command="View:PageInfo" modifiers="accel"/>
+ <key id="key_find" data-l10n-id="find-shortcut" command="cmd_find" modifiers="accel"/>
+ <key id="key_findAgain" data-l10n-id="search-find-again-shortcut" command="cmd_findAgain" modifiers="accel"/>
+ <key id="key_findPrevious" data-l10n-id="search-find-again-shortcut" command="cmd_findPrevious" modifiers="accel,shift"/>
+#ifdef XP_MACOSX
+ <key id="key_findSelection" data-l10n-id="search-find-selection-shortcut" command="cmd_findSelection" modifiers="accel"/>
+#endif
+ <key data-l10n-id="search-find-again-shortcut-alt" command="cmd_findAgain"/>
+ <key data-l10n-id="search-find-again-shortcut-alt" command="cmd_findPrevious" modifiers="shift"/>
+
+ <key id="addBookmarkAsKb" data-l10n-id="bookmark-this-page-shortcut" command="Browser:AddBookmarkAs" modifiers="accel"/>
+ <key id="bookmarkAllTabsKb"
+ data-l10n-id="bookmark-this-page-shortcut"
+ oncommand="PlacesUIUtils.showBookmarkPagesDialog(PlacesCommandHook.uniqueCurrentPages);"
+ modifiers="accel,shift"/>
+ <key id="manBookmarkKb" data-l10n-id="bookmark-show-library-shortcut" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/>
+ <key id="viewBookmarksSidebarKb"
+ data-l10n-id="bookmark-show-sidebar-shortcut"
+ modifiers="accel"
+ oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
+ <key id="viewBookmarksToolbarKb"
+ data-l10n-id="bookmark-show-toolbar-shortcut"
+ oncommand="BookmarkingUI.toggleBookmarksToolbar('shortcut');"
+ modifiers="accel,shift"/>
+
+ <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/>
+
+#ifdef XP_MACOSX
+ <key id="key_stop_mac" modifiers="accel" data-l10n-id="nav-stop-shortcut" command="Browser:Stop"/>
+#endif
+
+ <key id="key_gotoHistory"
+ data-l10n-id="history-sidebar-shortcut"
+#ifdef XP_MACOSX
+ modifiers="accel,shift"
+#else
+ modifiers="accel"
+#endif
+ oncommand="SidebarUI.toggle('viewHistorySidebar');"/>
+
+ <key id="key_fullZoomReduce" data-l10n-id="full-zoom-reduce-shortcut" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key data-l10n-id="full-zoom-reduce-shortcut-alt-a" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key data-l10n-id="full-zoom-reduce-shortcut-alt-b" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key id="key_fullZoomEnlarge" data-l10n-id="full-zoom-enlarge-shortcut" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key data-l10n-id="full-zoom-enlarge-shortcut-alt" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key data-l10n-id="full-zoom-enlarge-shortcut-alt2" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomReset" data-l10n-id="full-zoom-reset-shortcut" command="cmd_fullZoomReset" modifiers="accel"/>
+ <key data-l10n-id="full-zoom-reset-shortcut-alt" command="cmd_fullZoomReset" modifiers="accel"/>
+
+ <key id="key_showAllTabs" keycode="VK_TAB" modifiers="control,shift"/>
+
+ <key id="key_switchTextDirection" data-l10n-id="bidi-switch-direction-shortcut" command="cmd_switchTextDirection" modifiers="accel,shift" />
+
+ <key id="key_privatebrowsing" command="Tools:PrivateBrowsing" data-l10n-id="private-browsing-shortcut"
+ modifiers="accel,shift" reserved="true"/>
+ <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/>
+#ifdef XP_MACOSX
+ <key id="key_sanitize_mac" command="Tools:Sanitize" keycode="VK_BACK" modifiers="accel,shift"/>
+#endif
+ <key id="key_quitApplication" data-l10n-id="quit-app-shortcut"
+#ifdef XP_WIN
+ modifiers="accel,shift"
+#else
+ modifiers="accel"
+#endif
+# On OS X, dark voodoo magic invokes the quit code for this key.
+# So we're not adding the attribute on OSX because of backwards/add-on compat.
+# See bug 1369909 for background on this.
+#ifndef XP_MACOSX
+ command="cmd_quitApplication"
+#endif
+ reserved="true"/>
+
+ <key id="key_undoCloseTab" command="History:UndoCloseTab" data-l10n-id="tab-new-shortcut" modifiers="accel,shift"/>
+ <key id="key_undoCloseWindow" command="History:UndoCloseWindow" data-l10n-id="window-new-shortcut" modifiers="accel,shift"/>
+
+#ifdef XP_GNOME
+#define NUM_SELECT_TAB_MODIFIER alt
+#else
+#define NUM_SELECT_TAB_MODIFIER accel
+#endif
+
+#expand <key id="key_selectTab1" oncommand="gBrowser.selectTabAtIndex(0, event);" key="1" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab2" oncommand="gBrowser.selectTabAtIndex(1, event);" key="2" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab3" oncommand="gBrowser.selectTabAtIndex(2, event);" key="3" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab4" oncommand="gBrowser.selectTabAtIndex(3, event);" key="4" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab5" oncommand="gBrowser.selectTabAtIndex(4, event);" key="5" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab6" oncommand="gBrowser.selectTabAtIndex(5, event);" key="6" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab7" oncommand="gBrowser.selectTabAtIndex(6, event);" key="7" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab8" oncommand="gBrowser.selectTabAtIndex(7, event);" key="8" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectLastTab" oncommand="gBrowser.selectTabAtIndex(-1, event);" key="9" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+
+ <key id="key_wrCaptureCmd"
+#ifdef XP_MACOSX
+ key="3" modifiers="control,shift"
+#else
+ key="#" modifiers="control"
+#endif
+ command="wrCaptureCmd"/>
+ <key id="key_wrToggleCaptureSequenceCmd"
+#ifdef XP_MACOSX
+ key="6" modifiers="control,shift"
+#else
+ key="^" modifiers="control"
+#endif
+ command="wrToggleCaptureSequenceCmd"/>
+#ifdef NIGHTLY_BUILD
+ <key id="key_windowRecordingCmd"
+#ifdef XP_MACOSX
+ key="4" modifiers="control,shift"
+#else
+ key="$" modifiers="control"
+#endif
+ command="windowRecordingCmd"/>
+#endif
+#ifdef XP_MACOSX
+ <key id="key_minimizeWindow"
+ command="minimizeWindow"
+ data-l10n-id="window-minimize-shortcut"
+ modifiers="accel"/>
+ <key id="key_openHelpMac"
+ oncommand="openHelpLink('firefox-osxkey');"
+ data-l10n-id="help-shortcut"
+ modifiers="accel"/>
+ <!-- These are used to build the Application menu -->
+ <key id="key_preferencesCmdMac"
+ data-l10n-id="preferences-shortcut"
+ modifiers="accel"/>
+ <key id="key_hideThisAppCmdMac"
+ data-l10n-id="hide-app-shortcut"
+ modifiers="accel"/>
+ <key id="key_hideOtherAppsCmdMac"
+ data-l10n-id="hide-other-apps-shortcut"
+ modifiers="accel,alt"/>
+#endif
+ </keyset>
diff --git a/browser/base/content/browser-sidebar.js b/browser/base/content/browser-sidebar.js
new file mode 100644
index 0000000000..59185832cf
--- /dev/null
+++ b/browser/base/content/browser-sidebar.js
@@ -0,0 +1,608 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * SidebarUI controls showing and hiding the browser sidebar.
+ */
+var SidebarUI = {
+ get sidebars() {
+ if (this._sidebars) {
+ return this._sidebars;
+ }
+ return (this._sidebars = new Map([
+ [
+ "viewBookmarksSidebar",
+ {
+ title: document
+ .getElementById("sidebar-switcher-bookmarks")
+ .getAttribute("label"),
+ url: "chrome://browser/content/places/bookmarksSidebar.xhtml",
+ menuId: "menu_bookmarksSidebar",
+ buttonId: "sidebar-switcher-bookmarks",
+ },
+ ],
+ [
+ "viewHistorySidebar",
+ {
+ title: document
+ .getElementById("sidebar-switcher-history")
+ .getAttribute("label"),
+ url: "chrome://browser/content/places/historySidebar.xhtml",
+ menuId: "menu_historySidebar",
+ buttonId: "sidebar-switcher-history",
+ triggerButtonId: "appMenuViewHistorySidebar",
+ },
+ ],
+ [
+ "viewTabsSidebar",
+ {
+ title: document
+ .getElementById("sidebar-switcher-tabs")
+ .getAttribute("label"),
+ url: "chrome://browser/content/syncedtabs/sidebar.xhtml",
+ menuId: "menu_tabsSidebar",
+ buttonId: "sidebar-switcher-tabs",
+ triggerButtonId: "PanelUI-remotetabs-view-sidebar",
+ },
+ ],
+ ]));
+ },
+
+ // Avoid getting the browser element from init() to avoid triggering the
+ // <browser> constructor during startup if the sidebar is hidden.
+ get browser() {
+ if (this._browser) {
+ return this._browser;
+ }
+ return (this._browser = document.getElementById("sidebar"));
+ },
+ POSITION_START_PREF: "sidebar.position_start",
+ DEFAULT_SIDEBAR_ID: "viewBookmarksSidebar",
+
+ // lastOpenedId is set in show() but unlike currentID it's not cleared out on hide
+ // and isn't persisted across windows
+ lastOpenedId: null,
+
+ _box: null,
+ // The constructor of this label accesses the browser element due to the
+ // control="sidebar" attribute, so avoid getting this label during startup.
+ get _title() {
+ if (this.__title) {
+ return this.__title;
+ }
+ return (this.__title = document.getElementById("sidebar-title"));
+ },
+ _splitter: null,
+ _icon: null,
+ _reversePositionButton: null,
+ _switcherPanel: null,
+ _switcherTarget: null,
+ _switcherArrow: null,
+ _inited: false,
+
+ _initDeferred: PromiseUtils.defer(),
+
+ get promiseInitialized() {
+ return this._initDeferred.promise;
+ },
+
+ get initialized() {
+ return this._inited;
+ },
+
+ init() {
+ this._box = document.getElementById("sidebar-box");
+ this._splitter = document.getElementById("sidebar-splitter");
+ this._icon = document.getElementById("sidebar-icon");
+ this._reversePositionButton = document.getElementById(
+ "sidebar-reverse-position"
+ );
+ this._switcherPanel = document.getElementById("sidebarMenu-popup");
+ this._switcherTarget = document.getElementById("sidebar-switcher-target");
+ this._switcherArrow = document.getElementById("sidebar-switcher-arrow");
+
+ this._switcherTarget.addEventListener("command", () => {
+ this.toggleSwitcherPanel();
+ });
+
+ this._inited = true;
+
+ this._initDeferred.resolve();
+ },
+
+ uninit() {
+ // If this is the last browser window, persist various values that should be
+ // remembered for after a restart / reopening a browser window.
+ let enumerator = Services.wm.getEnumerator("navigator:browser");
+ if (!enumerator.hasMoreElements()) {
+ let xulStore = Services.xulStore;
+ xulStore.persist(this._box, "sidebarcommand");
+
+ if (this._box.hasAttribute("positionend")) {
+ xulStore.persist(this._box, "positionend");
+ } else {
+ xulStore.removeValue(
+ document.documentURI,
+ "sidebar-box",
+ "positionend"
+ );
+ }
+ if (this._box.hasAttribute("checked")) {
+ xulStore.persist(this._box, "checked");
+ } else {
+ xulStore.removeValue(document.documentURI, "sidebar-box", "checked");
+ }
+
+ xulStore.persist(this._box, "width");
+ xulStore.persist(this._title, "value");
+ }
+ },
+
+ /**
+ * Opens the switcher panel if it's closed, or closes it if it's open.
+ */
+ toggleSwitcherPanel() {
+ if (
+ this._switcherPanel.state == "open" ||
+ this._switcherPanel.state == "showing"
+ ) {
+ this.hideSwitcherPanel();
+ } else if (this._switcherPanel.state == "closed") {
+ this.showSwitcherPanel();
+ }
+ },
+
+ hideSwitcherPanel() {
+ this._switcherPanel.hidePopup();
+ },
+
+ showSwitcherPanel() {
+ this._ensureShortcutsShown();
+ this._switcherPanel.addEventListener(
+ "popuphiding",
+ () => {
+ this._switcherTarget.classList.remove("active");
+ },
+ { once: true }
+ );
+
+ // Combine start/end position with ltr/rtl to set the label in the popup appropriately.
+ let label =
+ this._positionStart == RTL_UI
+ ? gNavigatorBundle.getString("sidebar.moveToLeft")
+ : gNavigatorBundle.getString("sidebar.moveToRight");
+ this._reversePositionButton.setAttribute("label", label);
+
+ this._switcherPanel.hidden = false;
+ this._switcherPanel.openPopup(this._icon);
+ this._switcherTarget.classList.add("active");
+ },
+
+ updateShortcut({ button, key }) {
+ // If the shortcuts haven't been rendered yet then it will be set correctly
+ // on the first render so there's nothing to do now.
+ if (!this._addedShortcuts) {
+ return;
+ }
+ if (key) {
+ let keyId = key.getAttribute("id");
+ button = this._switcherPanel.querySelector(`[key="${keyId}"]`);
+ } else if (button) {
+ let keyId = button.getAttribute("key");
+ key = document.getElementById(keyId);
+ }
+ if (!button || !key) {
+ return;
+ }
+ button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
+ },
+
+ _addedShortcuts: false,
+ _ensureShortcutsShown() {
+ if (this._addedShortcuts) {
+ return;
+ }
+ this._addedShortcuts = true;
+ for (let button of this._switcherPanel.querySelectorAll(
+ "toolbarbutton[key]"
+ )) {
+ this.updateShortcut({ button });
+ }
+ },
+
+ /**
+ * Change the pref that will trigger a call to setPosition
+ */
+ reversePosition() {
+ Services.prefs.setBoolPref(this.POSITION_START_PREF, !this._positionStart);
+ },
+
+ /**
+ * Read the positioning pref and position the sidebar and the splitter
+ * appropriately within the browser container.
+ */
+ setPosition() {
+ // First reset all ordinals to match DOM ordering.
+ let browser = document.getElementById("browser");
+ [...browser.children].forEach((node, i) => {
+ node.style.MozBoxOrdinalGroup = i + 1;
+ });
+
+ if (!this._positionStart) {
+ // DOM ordering is: | sidebar-box | splitter | appcontent |
+ // Want to display as: | appcontent | splitter | sidebar-box |
+ // So we just swap box and appcontent ordering
+ let appcontent = document.getElementById("appcontent");
+ let boxOrdinal = this._box.style.MozBoxOrdinalGroup;
+ this._box.style.MozBoxOrdinalGroup = appcontent.style.MozBoxOrdinalGroup;
+ appcontent.style.MozBoxOrdinalGroup = boxOrdinal;
+ // Indicate we've switched ordering to the box
+ this._box.setAttribute("positionend", true);
+ } else {
+ this._box.removeAttribute("positionend");
+ }
+
+ this.hideSwitcherPanel();
+
+ let content = SidebarUI.browser.contentWindow;
+ if (content && content.updatePosition) {
+ content.updatePosition();
+ }
+ },
+
+ /**
+ * Try and adopt the status of the sidebar from another window.
+ * @param {Window} sourceWindow - Window to use as a source for sidebar status.
+ * @return true if we adopted the state, or false if the caller should
+ * initialize the state itself.
+ */
+ adoptFromWindow(sourceWindow) {
+ // If the opener had a sidebar, open the same sidebar in our window.
+ // The opener can be the hidden window too, if we're coming from the state
+ // where no windows are open, and the hidden window has no sidebar box.
+ let sourceUI = sourceWindow.SidebarUI;
+ if (!sourceUI || !sourceUI._box) {
+ // no source UI or no _box means we also can't adopt the state.
+ return false;
+ }
+
+ // Set sidebar command even if hidden, so that we keep the same sidebar
+ // even if it's currently closed.
+ let commandID = sourceUI._box.getAttribute("sidebarcommand");
+ if (commandID) {
+ this._box.setAttribute("sidebarcommand", commandID);
+ }
+
+ if (sourceUI._box.hidden) {
+ // just hidden means we have adopted the hidden state.
+ return true;
+ }
+
+ // dynamically generated sidebars will fail this check, but we still
+ // consider it adopted.
+ if (!this.sidebars.has(commandID)) {
+ return true;
+ }
+
+ this._box.setAttribute(
+ "width",
+ sourceUI._box.getBoundingClientRect().width
+ );
+ this.showInitially(commandID);
+
+ return true;
+ },
+
+ windowPrivacyMatches(w1, w2) {
+ return (
+ PrivateBrowsingUtils.isWindowPrivate(w1) ===
+ PrivateBrowsingUtils.isWindowPrivate(w2)
+ );
+ },
+
+ /**
+ * If loading a sidebar was delayed on startup, start the load now.
+ */
+ startDelayedLoad() {
+ let sourceWindow = window.opener;
+ // No source window means this is the initial window. If we're being
+ // opened from another window, check that it is one we might open a sidebar
+ // for.
+ if (sourceWindow) {
+ if (
+ sourceWindow.closed ||
+ sourceWindow.location.protocol != "chrome:" ||
+ !this.windowPrivacyMatches(sourceWindow, window)
+ ) {
+ return;
+ }
+ // Try to adopt the sidebar state from the source window
+ if (this.adoptFromWindow(sourceWindow)) {
+ return;
+ }
+ }
+
+ // If we're not adopting settings from a parent window, set them now.
+ let wasOpen = this._box.getAttribute("checked");
+ if (!wasOpen) {
+ return;
+ }
+
+ let commandID = this._box.getAttribute("sidebarcommand");
+ if (commandID && this.sidebars.has(commandID)) {
+ this.showInitially(commandID);
+ } else {
+ this._box.removeAttribute("checked");
+ // Remove the |sidebarcommand| attribute, because the element it
+ // refers to no longer exists, so we should assume this sidebar
+ // panel has been uninstalled. (249883)
+ // We use setAttribute rather than removeAttribute so it persists
+ // correctly.
+ this._box.setAttribute("sidebarcommand", "");
+ // On a startup in which the startup cache was invalidated (e.g. app update)
+ // extensions will not be started prior to delayedLoad, thus the
+ // sidebarcommand element will not exist yet. Store the commandID so
+ // extensions may reopen if necessary. A startup cache invalidation
+ // can be forced (for testing) by deleting compatibility.ini from the
+ // profile.
+ this.lastOpenedId = commandID;
+ }
+ },
+
+ /**
+ * Fire a "SidebarShown" event on the sidebar to give any interested parties
+ * a chance to update the button or whatever.
+ */
+ _fireShowEvent() {
+ let event = new CustomEvent("SidebarShown", { bubbles: true });
+ this._switcherTarget.dispatchEvent(event);
+ },
+
+ /**
+ * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
+ * a chance to adjust focus as needed. An additional event is needed, because
+ * we don't want to focus the sidebar when it's opened on startup or in a new
+ * window, only when the user opens the sidebar.
+ */
+ _fireFocusedEvent() {
+ let event = new CustomEvent("SidebarFocused", { bubbles: true });
+ this.browser.contentWindow.dispatchEvent(event);
+ },
+
+ /**
+ * True if the sidebar is currently open.
+ */
+ get isOpen() {
+ return !this._box.hidden;
+ },
+
+ /**
+ * The ID of the current sidebar.
+ */
+ get currentID() {
+ return this.isOpen ? this._box.getAttribute("sidebarcommand") : "";
+ },
+
+ get title() {
+ return this._title.value;
+ },
+
+ set title(value) {
+ this._title.value = value;
+ },
+
+ /**
+ * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
+ * with a different commandID, then the sidebar will be opened using the
+ * specified commandID. Otherwise the sidebar will be hidden.
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * visibility toggling of the sidebar.
+ * @return {Promise}
+ */
+ toggle(commandID = this.lastOpenedId, triggerNode) {
+ // First priority for a default value is this.lastOpenedId which is set during show()
+ // and not reset in hide(), unlike currentID. If show() hasn't been called and we don't
+ // have a persisted command either, or the command doesn't exist anymore, then
+ // fallback to a default sidebar.
+ if (!commandID) {
+ commandID = this._box.getAttribute("sidebarcommand");
+ }
+ if (!commandID || !this.sidebars.has(commandID)) {
+ commandID = this.DEFAULT_SIDEBAR_ID;
+ }
+
+ if (this.isOpen && commandID == this.currentID) {
+ this.hide(triggerNode);
+ return Promise.resolve();
+ }
+ return this.show(commandID, triggerNode);
+ },
+
+ _loadSidebarExtension(commandID) {
+ let sidebar = this.sidebars.get(commandID);
+ let { extensionId } = sidebar;
+ if (extensionId) {
+ SidebarUI.browser.contentWindow.loadPanel(
+ extensionId,
+ sidebar.panel,
+ sidebar.browserStyle
+ );
+ }
+ },
+
+ /**
+ * Show the sidebar.
+ *
+ * This wraps the internal method, including a ping to telemetry.
+ *
+ * @param {string} commandID ID of the sidebar to use.
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * showing of the sidebar.
+ * @return {Promise<boolean>}
+ */
+ async show(commandID, triggerNode) {
+ let panelType = commandID.substring(4, commandID.length - 7);
+ Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
+
+ // Extensions without private window access wont be in the
+ // sidebars map.
+ if (!this.sidebars.has(commandID)) {
+ return false;
+ }
+ return this._show(commandID).then(() => {
+ this._loadSidebarExtension(commandID);
+
+ if (triggerNode) {
+ updateToggleControlLabel(triggerNode);
+ }
+
+ this._fireFocusedEvent();
+ return true;
+ });
+ },
+
+ /**
+ * Show the sidebar, without firing the focused event or logging telemetry.
+ * This is intended to be used when the sidebar is opened automatically
+ * when a window opens (not triggered by user interaction).
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @return {Promise<boolean>}
+ */
+ async showInitially(commandID) {
+ let panelType = commandID.substring(4, commandID.length - 7);
+ Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
+
+ // Extensions without private window access wont be in the
+ // sidebars map.
+ if (!this.sidebars.has(commandID)) {
+ return false;
+ }
+ return this._show(commandID).then(() => {
+ this._loadSidebarExtension(commandID);
+ return true;
+ });
+ },
+
+ /**
+ * Implementation for show. Also used internally for sidebars that are shown
+ * when a window is opened and we don't want to ping telemetry.
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @return {Promise<void>}
+ */
+ _show(commandID) {
+ return new Promise(resolve => {
+ this.selectMenuItem(commandID);
+
+ this._box.hidden = this._splitter.hidden = false;
+ this.setPosition();
+
+ this.hideSwitcherPanel();
+
+ this._box.setAttribute("checked", "true");
+ this._box.setAttribute("sidebarcommand", commandID);
+ this.lastOpenedId = commandID;
+
+ let { url, title } = this.sidebars.get(commandID);
+ this.title = title;
+ this.browser.setAttribute("src", url); // kick off async load
+
+ if (this.browser.contentDocument.location.href != url) {
+ this.browser.addEventListener(
+ "load",
+ event => {
+ // We're handling the 'load' event before it bubbles up to the usual
+ // (non-capturing) event handlers. Let it bubble up before resolving.
+ setTimeout(() => {
+ resolve();
+
+ // Now that the currentId is updated, fire a show event.
+ this._fireShowEvent();
+ }, 0);
+ },
+ { capture: true, once: true }
+ );
+ } else {
+ resolve();
+
+ // Now that the currentId is updated, fire a show event.
+ this._fireShowEvent();
+ }
+ });
+ },
+
+ /**
+ * Hide the sidebar.
+ *
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * hiding of the sidebar.
+ */
+ hide(triggerNode) {
+ if (!this.isOpen) {
+ return;
+ }
+
+ this.hideSwitcherPanel();
+
+ this.selectMenuItem("");
+
+ // Replace the document currently displayed in the sidebar with about:blank
+ // so that we can free memory by unloading the page. We need to explicitly
+ // create a new content viewer because the old one doesn't get destroyed
+ // until about:blank has loaded (which does not happen as long as the
+ // element is hidden).
+ this.browser.setAttribute("src", "about:blank");
+ this.browser.docShell.createAboutBlankContentViewer(null, null);
+
+ this._box.removeAttribute("checked");
+ this._box.hidden = this._splitter.hidden = true;
+
+ let selBrowser = gBrowser.selectedBrowser;
+ selBrowser.focus();
+ if (triggerNode) {
+ updateToggleControlLabel(triggerNode);
+ }
+ },
+
+ /**
+ * Sets the checked state only on the menu items of the specified sidebar, or
+ * none if the argument is an empty string.
+ */
+ selectMenuItem(commandID) {
+ for (let [id, { menuId, buttonId, triggerButtonId }] of this.sidebars) {
+ let menu = document.getElementById(menuId);
+ let button = document.getElementById(buttonId);
+ let triggerbutton =
+ triggerButtonId && document.getElementById(triggerButtonId);
+ if (id == commandID) {
+ menu.setAttribute("checked", "true");
+ button.setAttribute("checked", "true");
+ if (triggerbutton) {
+ triggerbutton.setAttribute("checked", "true");
+ updateToggleControlLabel(triggerbutton);
+ }
+ } else {
+ menu.removeAttribute("checked");
+ button.removeAttribute("checked");
+ if (triggerbutton) {
+ triggerbutton.removeAttribute("checked");
+ updateToggleControlLabel(triggerbutton);
+ }
+ }
+ }
+ },
+};
+
+// Add getters related to the position here, since we will want them
+// available for both startDelayedLoad and init.
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarUI,
+ "_positionStart",
+ SidebarUI.POSITION_START_PREF,
+ true,
+ SidebarUI.setPosition.bind(SidebarUI)
+);
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
new file mode 100644
index 0000000000..5850d7ceb4
--- /dev/null
+++ b/browser/base/content/browser-siteIdentity.js
@@ -0,0 +1,2114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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-env mozilla/browser-window */
+
+/**
+ * Utility object to handle manipulations of the identity indicators in the UI
+ */
+var gIdentityHandler = {
+ /**
+ * nsIURI for which the identity UI is displayed. This has been already
+ * processed by createExposableURI.
+ */
+ _uri: null,
+
+ /**
+ * We only know the connection type if this._uri has a defined "host" part.
+ *
+ * These URIs, like "about:", "file:" and "data:" URIs, will usually be treated as a
+ * an unknown connection.
+ */
+ _uriHasHost: false,
+
+ /**
+ * If this tab belongs to a WebExtension, contains its WebExtensionPolicy.
+ */
+ _pageExtensionPolicy: null,
+
+ /**
+ * Whether this._uri refers to an internally implemented browser page.
+ *
+ * Note that this is set for some "about:" pages, but general "chrome:" URIs
+ * are not included in this category by default.
+ */
+ _isSecureInternalUI: false,
+
+ /**
+ * Whether the content window is considered a "secure context". This
+ * includes "potentially trustworthy" origins such as file:// URLs or localhost.
+ * https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
+ */
+ _isSecureContext: false,
+
+ /**
+ * nsITransportSecurityInfo metadata provided by gBrowser.securityUI the last
+ * time the identity UI was updated, or null if the connection is not secure.
+ */
+ _secInfo: null,
+
+ /**
+ * Bitmask provided by nsIWebProgressListener.onSecurityChange.
+ */
+ _state: 0,
+
+ /**
+ * RegExp used to decide if an about url should be shown as being part of
+ * the browser UI.
+ */
+ _secureInternalPages: /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion)(?:[?#]|$)/i,
+
+ /**
+ * Whether the established HTTPS connection is considered "broken".
+ * This could have several reasons, such as mixed content or weak
+ * cryptography. If this is true, _isSecureConnection is false.
+ */
+ get _isBrokenConnection() {
+ return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
+ },
+
+ /**
+ * Whether the connection to the current site was done via secure
+ * transport. Note that this attribute is not true in all cases that
+ * the site was accessed via HTTPS, i.e. _isSecureConnection will
+ * be false when _isBrokenConnection is true, even though the page
+ * was loaded over HTTPS.
+ */
+ get _isSecureConnection() {
+ // If a <browser> is included within a chrome document, then this._state
+ // will refer to the security state for the <browser> and not the top level
+ // document. In this case, don't upgrade the security state in the UI
+ // with the secure state of the embedded <browser>.
+ return (
+ !this._isURILoadedFromFile &&
+ this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE
+ );
+ },
+
+ get _isEV() {
+ // If a <browser> is included within a chrome document, then this._state
+ // will refer to the security state for the <browser> and not the top level
+ // document. In this case, don't upgrade the security state in the UI
+ // with the EV state of the embedded <browser>.
+ return (
+ !this._isURILoadedFromFile &&
+ this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL
+ );
+ },
+
+ get _isMixedActiveContentLoaded() {
+ return (
+ this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT
+ );
+ },
+
+ get _isMixedActiveContentBlocked() {
+ return (
+ this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT
+ );
+ },
+
+ get _isMixedPassiveContentLoaded() {
+ return (
+ this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT
+ );
+ },
+
+ get _isContentHttpsOnlyModeUpgraded() {
+ return (
+ this._state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED
+ );
+ },
+
+ get _isContentHttpsOnlyModeUpgradeFailed() {
+ return (
+ this._state &
+ Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED
+ );
+ },
+
+ get _isCertUserOverridden() {
+ return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN;
+ },
+
+ get _isCertDistrustImminent() {
+ return this._state & Ci.nsIWebProgressListener.STATE_CERT_DISTRUST_IMMINENT;
+ },
+
+ get _isAboutCertErrorPage() {
+ return (
+ gBrowser.selectedBrowser.documentURI &&
+ gBrowser.selectedBrowser.documentURI.scheme == "about" &&
+ gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith("certerror")
+ );
+ },
+
+ get _isAboutNetErrorPage() {
+ return (
+ gBrowser.selectedBrowser.documentURI &&
+ gBrowser.selectedBrowser.documentURI.scheme == "about" &&
+ gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith("neterror")
+ );
+ },
+
+ get _isAboutHttpsOnlyErrorPage() {
+ return (
+ gBrowser.selectedBrowser.documentURI &&
+ gBrowser.selectedBrowser.documentURI.scheme == "about" &&
+ gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith(
+ "httpsonlyerror"
+ )
+ );
+ },
+
+ get _isPotentiallyTrustworthy() {
+ return (
+ !this._isBrokenConnection &&
+ (this._isSecureContext ||
+ (gBrowser.selectedBrowser.documentURI &&
+ gBrowser.selectedBrowser.documentURI.scheme == "chrome"))
+ );
+ },
+
+ get _isAboutBlockedPage() {
+ return (
+ gBrowser.selectedBrowser.documentURI &&
+ gBrowser.selectedBrowser.documentURI.scheme == "about" &&
+ gBrowser.selectedBrowser.documentURI.pathQueryRef.startsWith("blocked")
+ );
+ },
+
+ _popupInitialized: false,
+ _initializePopup() {
+ if (!this._popupInitialized) {
+ let wrapper = document.getElementById("template-identity-popup");
+ wrapper.replaceWith(wrapper.content);
+ this._popupInitialized = true;
+ }
+ },
+
+ hidePopup() {
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ },
+
+ // smart getters
+ get _identityPopup() {
+ if (!this._popupInitialized) {
+ return null;
+ }
+ delete this._identityPopup;
+ return (this._identityPopup = document.getElementById("identity-popup"));
+ },
+ get _identityBox() {
+ delete this._identityBox;
+ return (this._identityBox = document.getElementById("identity-box"));
+ },
+ get _identityPopupMultiView() {
+ delete this._identityPopupMultiView;
+ return (this._identityPopupMultiView = document.getElementById(
+ "identity-popup-multiView"
+ ));
+ },
+ get _identityPopupMainView() {
+ delete this._identityPopupMainView;
+ return (this._identityPopupMainView = document.getElementById(
+ "identity-popup-mainView"
+ ));
+ },
+ get _identityPopupMainViewHeaderLabel() {
+ delete this._identityPopupMainViewHeaderLabel;
+ return (this._identityPopupMainViewHeaderLabel = document.getElementById(
+ "identity-popup-mainView-panel-header-span"
+ ));
+ },
+ get _identityPopupSecurityView() {
+ delete this._identityPopupSecurityView;
+ return (this._identityPopupSecurityView = document.getElementById(
+ "identity-popup-securityView"
+ ));
+ },
+ get _identityPopupHttpsOnlyModeMenuList() {
+ delete this._identityPopupHttpsOnlyModeMenuList;
+ return (this._identityPopupHttpsOnlyModeMenuList = document.getElementById(
+ "identity-popup-security-httpsonlymode-menulist"
+ ));
+ },
+ get _identityPopupHttpsOnlyModeMenuListTempItem() {
+ delete this._identityPopupHttpsOnlyModeMenuListTempItem;
+ return (this._identityPopupHttpsOnlyModeMenuListTempItem = document.getElementById(
+ "identity-popup-security-menulist-tempitem"
+ ));
+ },
+ get _identityPopupSecurityEVContentOwner() {
+ delete this._identityPopupSecurityEVContentOwner;
+ return (this._identityPopupSecurityEVContentOwner = document.getElementById(
+ "identity-popup-security-ev-content-owner"
+ ));
+ },
+ get _identityPopupContentOwner() {
+ delete this._identityPopupContentOwner;
+ return (this._identityPopupContentOwner = document.getElementById(
+ "identity-popup-content-owner"
+ ));
+ },
+ get _identityPopupContentSupp() {
+ delete this._identityPopupContentSupp;
+ return (this._identityPopupContentSupp = document.getElementById(
+ "identity-popup-content-supplemental"
+ ));
+ },
+ get _identityPopupContentVerif() {
+ delete this._identityPopupContentVerif;
+ return (this._identityPopupContentVerif = document.getElementById(
+ "identity-popup-content-verifier"
+ ));
+ },
+ get _identityPopupCustomRootLearnMore() {
+ delete this._identityPopupCustomRootLearnMore;
+ return (this._identityPopupCustomRootLearnMore = document.getElementById(
+ "identity-popup-custom-root-learn-more"
+ ));
+ },
+ get _identityPopupMixedContentLearnMore() {
+ delete this._identityPopupMixedContentLearnMore;
+ return (this._identityPopupMixedContentLearnMore = [
+ ...document.querySelectorAll(".identity-popup-mcb-learn-more"),
+ ]);
+ },
+
+ get _identityIconLabel() {
+ delete this._identityIconLabel;
+ return (this._identityIconLabel = document.getElementById(
+ "identity-icon-label"
+ ));
+ },
+ get _overrideService() {
+ delete this._overrideService;
+ return (this._overrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService));
+ },
+ get _identityIcon() {
+ delete this._identityIcon;
+ return (this._identityIcon = document.getElementById("identity-icon"));
+ },
+ get _permissionList() {
+ delete this._permissionList;
+ return (this._permissionList = document.getElementById(
+ "identity-popup-permission-list"
+ ));
+ },
+ get _defaultPermissionAnchor() {
+ delete this._defaultPermissionAnchor;
+ return (this._defaultPermissionAnchor = document.getElementById(
+ "identity-popup-permission-list-default-anchor"
+ ));
+ },
+ get _permissionEmptyHint() {
+ delete this._permissionEmptyHint;
+ return (this._permissionEmptyHint = document.getElementById(
+ "identity-popup-permission-empty-hint"
+ ));
+ },
+ get _permissionReloadHint() {
+ delete this._permissionReloadHint;
+ return (this._permissionReloadHint = document.getElementById(
+ "identity-popup-permission-reload-hint"
+ ));
+ },
+ get _popupExpander() {
+ delete this._popupExpander;
+ return (this._popupExpander = document.getElementById(
+ "identity-popup-security-expander"
+ ));
+ },
+ get _clearSiteDataFooter() {
+ delete this._clearSiteDataFooter;
+ return (this._clearSiteDataFooter = document.getElementById(
+ "identity-popup-clear-sitedata-footer"
+ ));
+ },
+ get _permissionAnchors() {
+ delete this._permissionAnchors;
+ let permissionAnchors = {};
+ for (let anchor of document.getElementById("blocked-permissions-container")
+ .children) {
+ permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor;
+ }
+ return (this._permissionAnchors = permissionAnchors);
+ },
+
+ get _geoSharingIcon() {
+ delete this._geoSharingIcon;
+ return (this._geoSharingIcon = document.getElementById("geo-sharing-icon"));
+ },
+
+ get _xrSharingIcon() {
+ delete this._xrSharingIcon;
+ return (this._xrSharingIcon = document.getElementById("xr-sharing-icon"));
+ },
+
+ get _webRTCSharingIcon() {
+ delete this._webRTCSharingIcon;
+ return (this._webRTCSharingIcon = document.getElementById(
+ "webrtc-sharing-icon"
+ ));
+ },
+
+ get _insecureConnectionIconEnabled() {
+ delete this._insecureConnectionIconEnabled;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_insecureConnectionIconEnabled",
+ "security.insecure_connection_icon.enabled"
+ );
+ return this._insecureConnectionIconEnabled;
+ },
+ get _insecureConnectionIconPBModeEnabled() {
+ delete this._insecureConnectionIconPBModeEnabled;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_insecureConnectionIconPBModeEnabled",
+ "security.insecure_connection_icon.pbmode.enabled"
+ );
+ return this._insecureConnectionIconPBModeEnabled;
+ },
+ get _insecureConnectionTextEnabled() {
+ delete this._insecureConnectionTextEnabled;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_insecureConnectionTextEnabled",
+ "security.insecure_connection_text.enabled"
+ );
+ return this._insecureConnectionTextEnabled;
+ },
+ get _insecureConnectionTextPBModeEnabled() {
+ delete this._insecureConnectionTextPBModeEnabled;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_insecureConnectionTextPBModeEnabled",
+ "security.insecure_connection_text.pbmode.enabled"
+ );
+ return this._insecureConnectionTextPBModeEnabled;
+ },
+ get _protectionsPanelEnabled() {
+ delete this._protectionsPanelEnabled;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_protectionsPanelEnabled",
+ "browser.protections_panel.enabled",
+ false
+ );
+ return this._protectionsPanelEnabled;
+ },
+ get _httpsOnlyModeEnabled() {
+ delete this._httpsOnlyModeEnabled;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_httpsOnlyModeEnabled",
+ "dom.security.https_only_mode"
+ );
+ return this._httpsOnlyModeEnabled;
+ },
+ get _httpsOnlyModeEnabledPBM() {
+ delete this._httpsOnlyModeEnabledPBM;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_httpsOnlyModeEnabledPBM",
+ "dom.security.https_only_mode_pbm"
+ );
+ return this._httpsOnlyModeEnabledPBM;
+ },
+ get _useGrayLockIcon() {
+ delete this._useGrayLockIcon;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_useGrayLockIcon",
+ "security.secure_connection_icon_color_gray",
+ false
+ );
+ return this._useGrayLockIcon;
+ },
+
+ /**
+ * Handles clicks on the "Clear Cookies and Site Data" button.
+ */
+ async clearSiteData(event) {
+ if (!this._uriHasHost) {
+ return;
+ }
+
+ let host = this._uri.host;
+
+ // Hide the popup before showing the removal prompt, to
+ // avoid a pretty ugly transition. Also hide it even
+ // if the update resulted in no site data, to keep the
+ // illusion that clicking the button had an effect.
+ let hidden = new Promise(c => {
+ this._identityPopup.addEventListener("popuphidden", c, { once: true });
+ });
+ PanelMultiView.hidePopup(this._identityPopup);
+ await hidden;
+
+ let baseDomain = SiteDataManager.getBaseDomainFromHost(host);
+ if (SiteDataManager.promptSiteDataRemoval(window, null, baseDomain)) {
+ let siteData = await SiteDataManager.getSites(baseDomain);
+ if (siteData && siteData.length) {
+ let hosts = siteData.map(site => site.host);
+ SiteDataManager.remove(hosts);
+ }
+ }
+
+ event.stopPropagation();
+ },
+
+ openPermissionPreferences() {
+ openPreferences("privacy-permissions");
+ },
+
+ /**
+ * Handler for mouseclicks on the "More Information" button in the
+ * "identity-popup" panel.
+ */
+ handleMoreInfoClick(event) {
+ displaySecurityInfo();
+ event.stopPropagation();
+ PanelMultiView.hidePopup(this._identityPopup);
+ },
+
+ showSecuritySubView() {
+ this._identityPopupMultiView.showSubView(
+ "identity-popup-securityView",
+ this._popupExpander
+ );
+
+ // Elements of hidden views have -moz-user-focus:ignore but setting that
+ // per CSS selector doesn't blur a focused element in those hidden views.
+ Services.focus.clearFocus(window);
+ },
+
+ disableMixedContentProtection() {
+ // Use telemetry to measure how often unblocking happens
+ const kMIXED_CONTENT_UNBLOCK_EVENT = 2;
+ let histogram = Services.telemetry.getHistogramById(
+ "MIXED_CONTENT_UNBLOCK_COUNTER"
+ );
+ histogram.add(kMIXED_CONTENT_UNBLOCK_EVENT);
+ // Reload the page with the content unblocked
+ BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT);
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ },
+
+ enableMixedContentProtection() {
+ gBrowser.selectedBrowser.sendMessageToActor(
+ "MixedContent:ReenableProtection",
+ {},
+ "BrowserTab"
+ );
+ BrowserReload();
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ },
+
+ removeCertException() {
+ if (!this._uriHasHost) {
+ Cu.reportError(
+ "Trying to revoke a cert exception on a URI without a host?"
+ );
+ return;
+ }
+ let host = this._uri.host;
+ let port = this._uri.port > 0 ? this._uri.port : 443;
+ this._overrideService.clearValidityOverride(host, port);
+ BrowserReloadSkipCache();
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ },
+
+ /**
+ * Gets the current HTTPS-Only mode permission for the current page.
+ * Values are the same as in #identity-popup-security-httpsonlymode-menulist
+ */
+ _getHttpsOnlyPermission() {
+ const { state } = SitePermissions.getForPrincipal(
+ gBrowser.contentPrincipal,
+ "https-only-load-insecure"
+ );
+ 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
+ }
+ },
+
+ /**
+ * Sets/removes HTTPS-Only Mode exception and possibly reloads the page.
+ */
+ changeHttpsOnlyPermission() {
+ // Get the new value from the menulist and the current value
+ // Note: value and permission association is laid out
+ // in _getHttpsOnlyPermission
+ const oldValue = this._getHttpsOnlyPermission();
+ let newValue = parseInt(
+ this._identityPopupHttpsOnlyModeMenuList.selectedItem.value,
+ 10
+ );
+
+ // If nothing changed, just return here
+ if (newValue === oldValue) {
+ return;
+ }
+
+ // Permissions set in PMB get deleted anyway, but to make sure, let's make
+ // the permission session-only.
+ if (newValue === 1 && PrivateBrowsingUtils.isWindowPrivate(window)) {
+ newValue = 2;
+ }
+
+ // Usually we want to set the permission for the current site and therefore
+ // the current principal...
+ let principal = gBrowser.contentPrincipal;
+ // ...but if we're on the HTTPS-Only error page, the content-principal is
+ // for HTTPS but. We always want to set the exception for HTTP. (Code should
+ // be almost identical to the one in AboutHttpsOnlyErrorParent.jsm)
+ let newURI;
+ if (this._isAboutHttpsOnlyErrorPage) {
+ newURI = gBrowser.currentURI
+ .mutate()
+ .setScheme("http")
+ .finalize();
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ newURI,
+ gBrowser.contentPrincipal.originAttributes
+ );
+ }
+
+ // Set or remove the permission
+ if (newValue === 0) {
+ SitePermissions.removeFromPrincipal(
+ principal,
+ "https-only-load-insecure"
+ );
+ } else if (newValue === 1) {
+ SitePermissions.setForPrincipal(
+ principal,
+ "https-only-load-insecure",
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW,
+ SitePermissions.SCOPE_PERSISTENT
+ );
+ } else {
+ SitePermissions.setForPrincipal(
+ principal,
+ "https-only-load-insecure",
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
+ SitePermissions.SCOPE_SESSION
+ );
+ }
+
+ // If we're on the error-page, we have to redirect the user
+ // from HTTPS to HTTP. Otherwise we can just reload the page.
+ if (this._isAboutHttpsOnlyErrorPage) {
+ gBrowser.loadURI(newURI.spec, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ });
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ return;
+ }
+ // The page only needs to reload if we switch between allow and block
+ // Because "off" is 1 and "off temporarily" is 2, we can just check if the
+ // sum of newValue and oldValue is 3.
+ if (newValue + oldValue !== 3) {
+ BrowserReloadSkipCache();
+ if (this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ return;
+ }
+ // Otherwise we just refresh the interface
+ this.refreshIdentityPopup();
+ },
+
+ /**
+ * Helper to parse out the important parts of _secInfo (of the SSL cert in
+ * particular) for use in constructing identity UI strings
+ */
+ getIdentityData() {
+ var result = {};
+ var cert = this._secInfo.serverCert;
+
+ // Human readable name of Subject
+ result.subjectOrg = cert.organization;
+
+ // SubjectName fields, broken up for individual access
+ if (cert.subjectName) {
+ result.subjectNameFields = {};
+ cert.subjectName.split(",").forEach(function(v) {
+ var field = v.split("=");
+ this[field[0]] = field[1];
+ }, result.subjectNameFields);
+
+ // Call out city, state, and country specifically
+ result.city = result.subjectNameFields.L;
+ result.state = result.subjectNameFields.ST;
+ result.country = result.subjectNameFields.C;
+ }
+
+ // Human readable name of Certificate Authority
+ result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
+ result.cert = cert;
+
+ return result;
+ },
+
+ /**
+ * Update the identity user interface for the page currently being displayed.
+ *
+ * This examines the SSL certificate metadata, if available, as well as the
+ * connection type and other security-related state information for the page.
+ *
+ * @param state
+ * Bitmask provided by nsIWebProgressListener.onSecurityChange.
+ * @param uri
+ * nsIURI for which the identity UI should be displayed, already
+ * processed by createExposableURI.
+ */
+ updateIdentity(state, uri) {
+ let shouldHidePopup = this._uri && this._uri.spec != uri.spec;
+ this._state = state;
+
+ // Firstly, populate the state properties required to display the UI. See
+ // the documentation of the individual properties for details.
+ this.setURI(uri);
+ this._secInfo = gBrowser.securityUI.secInfo;
+ this._isSecureContext = gBrowser.securityUI.isSecureContext;
+
+ // Then, update the user interface with the available data.
+ this.refreshIdentityBlock();
+ // Handle a location change while the Control Center is focused
+ // by closing the popup (bug 1207542)
+ if (shouldHidePopup && this._popupInitialized) {
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+
+ // NOTE: We do NOT update the identity popup (the control center) when
+ // we receive a new security state on the existing page (i.e. from a
+ // subframe). If the user opened the popup and looks at the provided
+ // information we don't want to suddenly change the panel contents.
+
+ // Finally, if there are warnings to issue, issue them
+ if (this._isCertDistrustImminent) {
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ let windowId = gBrowser.selectedBrowser.innerWindowID;
+ let message = gBrowserBundle.GetStringFromName(
+ "certImminentDistrust.message"
+ );
+ // Use uri.prePath instead of initWithSourceURI() so that these can be
+ // de-duplicated on the scheme+host+port combination.
+ consoleMsg.initWithWindowID(
+ message,
+ uri.prePath,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.warningFlag,
+ "SSL",
+ windowId
+ );
+ Services.console.logMessage(consoleMsg);
+ }
+ },
+
+ updateSharingIndicator() {
+ let tab = gBrowser.selectedTab;
+ this._sharingState = tab._sharingState;
+
+ this._webRTCSharingIcon.removeAttribute("paused");
+ this._webRTCSharingIcon.removeAttribute("sharing");
+ this._geoSharingIcon.removeAttribute("sharing");
+ this._xrSharingIcon.removeAttribute("sharing");
+
+ if (this._sharingState) {
+ if (
+ this._sharingState &&
+ this._sharingState.webRTC &&
+ this._sharingState.webRTC.sharing
+ ) {
+ this._webRTCSharingIcon.setAttribute(
+ "sharing",
+ this._sharingState.webRTC.sharing
+ );
+
+ if (this._sharingState.webRTC.paused) {
+ this._webRTCSharingIcon.setAttribute("paused", "true");
+ }
+ }
+ if (this._sharingState.geo) {
+ this._geoSharingIcon.setAttribute("sharing", this._sharingState.geo);
+ }
+ if (this._sharingState.xr) {
+ this._xrSharingIcon.setAttribute("sharing", this._sharingState.xr);
+ }
+ }
+
+ if (this._popupInitialized && this._identityPopup.state != "closed") {
+ this.updateSitePermissions();
+ PanelView.forNode(
+ this._identityPopupMainView
+ ).descriptionHeightWorkaround();
+ }
+ },
+
+ /**
+ * Attempt to provide proper IDN treatment for host names
+ */
+ getEffectiveHost() {
+ if (!this._IDNService) {
+ this._IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+ }
+ try {
+ return this._IDNService.convertToDisplayIDN(this._uri.host, {});
+ } catch (e) {
+ // If something goes wrong (e.g. host is an IP address) just fail back
+ // to the full domain.
+ return this._uri.host;
+ }
+ },
+
+ getHostForDisplay() {
+ let host = "";
+
+ try {
+ host = this.getEffectiveHost();
+ } catch (e) {
+ // Some URIs might have no hosts.
+ }
+
+ if (this._uri.schemeIs("about")) {
+ // For example in about:certificate the original URL is
+ // about:certificate?cert=<large base64 encoded data>&cert=<large base64 encoded data>&cert=...
+ // So, instead of showing that large string in the identity panel header, we are just showing
+ // about:certificate now. For the other about pages we are just showing about:<page>
+ host = "about:" + this._uri.filePath;
+ }
+
+ if (this._uri.schemeIs("chrome")) {
+ host = this._uri.spec;
+ }
+
+ let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay(
+ this._uri.displaySpec
+ );
+ if (readerStrippedURI) {
+ host = readerStrippedURI.host;
+ }
+
+ if (this._pageExtensionPolicy) {
+ host = this._pageExtensionPolicy.name;
+ }
+
+ // Fallback for special protocols.
+ if (!host) {
+ host = this._uri.specIgnoringRef;
+ }
+
+ return host;
+ },
+
+ /**
+ * Return the CSS class name to set on the "fullscreen-warning" element to
+ * display information about connection security in the notification shown
+ * when a site enters the fullscreen mode.
+ */
+ get pointerlockFsWarningClassName() {
+ // Note that the fullscreen warning does not handle _isSecureInternalUI.
+ if (this._uriHasHost && this._isSecureConnection) {
+ return "verifiedDomain";
+ }
+ return "unknownIdentity";
+ },
+
+ /**
+ * Returns whether the issuer of the current certificate chain is
+ * built-in (returns false) or imported (returns true).
+ */
+ _hasCustomRoot() {
+ let issuerCert = null;
+ issuerCert = this._secInfo.succeededCertChain[
+ this._secInfo.succeededCertChain.length - 1
+ ];
+
+ return !issuerCert.isBuiltInRoot;
+ },
+
+ /**
+ * Returns whether the current URI results in an "invalid"
+ * URL bar state, which effectively means hidden security
+ * indicators.
+ */
+ _hasInvalidPageProxyState() {
+ return (
+ !this._uriHasHost &&
+ this._uri &&
+ isBlankPageURL(this._uri.spec) &&
+ !this._uri.schemeIs("moz-extension")
+ );
+ },
+
+ /**
+ * Updates the security identity in the identity block.
+ */
+ _refreshIdentityIcons() {
+ let icon_label = "";
+ let tooltip = "";
+
+ if (this._isSecureInternalUI) {
+ // This is a secure internal Firefox page.
+ this._identityBox.className = "chromeUI";
+ let brandBundle = document.getElementById("bundle_brand");
+ icon_label = brandBundle.getString("brandShorterName");
+ } else if (this._pageExtensionPolicy) {
+ // This is a WebExtension page.
+ this._identityBox.className = "extensionPage";
+ let extensionName = this._pageExtensionPolicy.name;
+ icon_label = gNavigatorBundle.getFormattedString(
+ "identity.extension.label",
+ [extensionName]
+ );
+ } else if (this._uriHasHost && this._isSecureConnection) {
+ // This is a secure connection.
+ this._identityBox.className = "verifiedDomain";
+ if (this._isMixedActiveContentBlocked) {
+ this._identityBox.classList.add("mixedActiveBlocked");
+ }
+ if (!this._isCertUserOverridden) {
+ // It's a normal cert, verifier is the CA Org.
+ tooltip = gNavigatorBundle.getFormattedString(
+ "identity.identified.verifier",
+ [this.getIdentityData().caOrg]
+ );
+ }
+ } else if (this._isBrokenConnection) {
+ // This is a secure connection, but something is wrong.
+ this._identityBox.className = "unknownIdentity";
+
+ if (this._isMixedActiveContentLoaded) {
+ this._identityBox.classList.add("mixedActiveContent");
+ } else if (this._isMixedActiveContentBlocked) {
+ this._identityBox.classList.add(
+ "mixedDisplayContentLoadedActiveBlocked"
+ );
+ } else if (this._isMixedPassiveContentLoaded) {
+ this._identityBox.classList.add("mixedDisplayContent");
+ } else {
+ this._identityBox.classList.add("weakCipher");
+ }
+ } else if (this._isAboutCertErrorPage) {
+ // We show a warning lock icon for 'about:certerror' page.
+ this._identityBox.className = "certErrorPage";
+ } else if (this._isAboutHttpsOnlyErrorPage) {
+ // We show a not secure lock icon for 'about:httpsonlyerror' page.
+ this._identityBox.className = "httpsOnlyErrorPage";
+ } else if (this._isAboutNetErrorPage || this._isAboutBlockedPage) {
+ // Network errors and blocked pages get a more neutral icon
+ this._identityBox.className = "unknownIdentity";
+ } else if (this._isPotentiallyTrustworthy) {
+ // This is a local resource (and shouldn't be marked insecure).
+ this._identityBox.className = "localResource";
+ } else {
+ // This is an insecure connection.
+ let warnOnInsecure =
+ this._insecureConnectionIconEnabled ||
+ (this._insecureConnectionIconPBModeEnabled &&
+ PrivateBrowsingUtils.isWindowPrivate(window));
+ let className = warnOnInsecure ? "notSecure" : "unknownIdentity";
+ this._identityBox.className = className;
+ tooltip = warnOnInsecure
+ ? gNavigatorBundle.getString("identity.notSecure.tooltip")
+ : "";
+
+ let warnTextOnInsecure =
+ this._insecureConnectionTextEnabled ||
+ (this._insecureConnectionTextPBModeEnabled &&
+ PrivateBrowsingUtils.isWindowPrivate(window));
+ if (warnTextOnInsecure) {
+ icon_label = gNavigatorBundle.getString("identity.notSecure.label");
+ this._identityBox.classList.add("notSecureText");
+ }
+ }
+
+ if (this._isCertUserOverridden) {
+ this._identityBox.classList.add("certUserOverridden");
+ // Cert is trusted because of a security exception, verifier is a special string.
+ tooltip = gNavigatorBundle.getString(
+ "identity.identified.verified_by_you"
+ );
+ }
+
+ // Gray lock icon for secure connections if pref set
+ this._updateAttribute(
+ this._identityIcon,
+ "lock-icon-gray",
+ this._useGrayLockIcon
+ );
+
+ // Push the appropriate strings out to the UI
+ this._identityIcon.setAttribute("tooltiptext", tooltip);
+
+ if (this._pageExtensionPolicy) {
+ let extensionName = this._pageExtensionPolicy.name;
+ this._identityIcon.setAttribute(
+ "tooltiptext",
+ gNavigatorBundle.getFormattedString("identity.extension.tooltip", [
+ extensionName,
+ ])
+ );
+ }
+
+ this._identityIconLabel.setAttribute("tooltiptext", tooltip);
+ this._identityIconLabel.setAttribute("value", icon_label);
+ this._identityIconLabel.collapsed = !icon_label;
+ },
+
+ /**
+ * Updates the permissions block in the identity block.
+ */
+ _refreshPermissionIcons() {
+ let permissionAnchors = this._permissionAnchors;
+
+ // hide all permission icons
+ for (let icon of Object.values(permissionAnchors)) {
+ icon.removeAttribute("showing");
+ }
+
+ // keeps track if we should show an indicator that there are active permissions
+ let hasGrantedPermissions = false;
+
+ // show permission icons
+ let permissions = SitePermissions.getAllForBrowser(
+ gBrowser.selectedBrowser
+ );
+ for (let permission of permissions) {
+ if (
+ permission.state == SitePermissions.BLOCK ||
+ permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL
+ ) {
+ let icon = permissionAnchors[permission.id];
+ if (icon) {
+ icon.setAttribute("showing", "true");
+ }
+ } else if (permission.state != SitePermissions.UNKNOWN) {
+ hasGrantedPermissions = true;
+ }
+ }
+
+ if (hasGrantedPermissions) {
+ this._identityBox.classList.add("grantedPermissions");
+ }
+
+ // Show blocked popup icon in the identity-box if popups are blocked
+ // irrespective of popup permission capability value.
+ if (gBrowser.selectedBrowser.popupBlocker.getBlockedPopupCount()) {
+ let icon = permissionAnchors.popup;
+ icon.setAttribute("showing", "true");
+ }
+ },
+
+ /**
+ * Updates the identity block user interface with the data from this object.
+ */
+ refreshIdentityBlock() {
+ if (!this._identityBox) {
+ return;
+ }
+
+ // If this condition is true, the URL bar will have an "invalid"
+ // pageproxystate, which will hide the security indicators. Thus, we can
+ // safely avoid updating the security UI.
+ //
+ // This will also filter out intermediate about:blank loads to avoid
+ // flickering the identity block and doing unnecessary work.
+ if (this._hasInvalidPageProxyState()) {
+ return;
+ }
+
+ this._refreshIdentityIcons();
+
+ this._refreshPermissionIcons();
+
+ // Hide the shield icon if it is a chrome page.
+ gProtectionsHandler._trackingProtectionIconContainer.classList.toggle(
+ "chromeUI",
+ this._isSecureInternalUI
+ );
+ },
+
+ /**
+ * Set up the title and content messages for the identity message popup,
+ * based on the specified mode, and the details of the SSL cert, where
+ * applicable
+ */
+ refreshIdentityPopup() {
+ // Update cookies and site data information and show the
+ // "Clear Site Data" button if the site is storing local data.
+ this._clearSiteDataFooter.hidden = true;
+ if (this._uriHasHost) {
+ SiteDataManager.hasSiteData(this._uri.asciiHost).then(hasData => {
+ this._clearSiteDataFooter.hidden = !hasData;
+ });
+ }
+
+ // Update "Learn More" for Mixed Content Blocking and Insecure Login Forms.
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ this._identityPopupMixedContentLearnMore.forEach(e =>
+ e.setAttribute("href", baseURL + "mixed-content")
+ );
+
+ this._identityPopupCustomRootLearnMore.setAttribute(
+ "href",
+ baseURL + "enterprise-roots"
+ );
+
+ // This is in the properties file because the expander used to switch its tooltip.
+ this._popupExpander.tooltipText = gNavigatorBundle.getString(
+ "identity.showDetails.tooltip"
+ );
+
+ let customRoot = false;
+
+ // Determine connection security information.
+ let connection = "not-secure";
+ if (this._isSecureInternalUI) {
+ connection = "chrome";
+ } else if (this._pageExtensionPolicy) {
+ connection = "extension";
+ } else if (this._isURILoadedFromFile) {
+ connection = "file";
+ } else if (this._isEV) {
+ connection = "secure-ev";
+ } else if (this._isCertUserOverridden) {
+ connection = "secure-cert-user-overridden";
+ } else if (this._isSecureConnection) {
+ connection = "secure";
+ customRoot = this._hasCustomRoot();
+ } else if (this._isAboutCertErrorPage) {
+ connection = "cert-error-page";
+ } else if (this._isAboutHttpsOnlyErrorPage) {
+ connection = "https-only-error-page";
+ } else if (this._isAboutNetErrorPage || this._isAboutBlockedPage) {
+ connection = "not-secure";
+ } else if (this._isPotentiallyTrustworthy) {
+ connection = "file";
+ }
+
+ // Determine the mixed content state.
+ let mixedcontent = [];
+ if (this._isMixedPassiveContentLoaded) {
+ mixedcontent.push("passive-loaded");
+ }
+ if (this._isMixedActiveContentLoaded) {
+ mixedcontent.push("active-loaded");
+ } else if (this._isMixedActiveContentBlocked) {
+ mixedcontent.push("active-blocked");
+ }
+ mixedcontent = mixedcontent.join(" ");
+
+ // We have no specific flags for weak ciphers (yet). If a connection is
+ // broken and we can't detect any mixed content loaded then it's a weak
+ // cipher.
+ let ciphers = "";
+ if (
+ this._isBrokenConnection &&
+ !this._isMixedActiveContentLoaded &&
+ !this._isMixedPassiveContentLoaded
+ ) {
+ ciphers = "weak";
+ }
+
+ // Gray lock icon for secure connections if pref set
+ this._updateAttribute(
+ this._identityPopup,
+ "lock-icon-gray",
+ this._useGrayLockIcon
+ );
+
+ // If HTTPS-Only Mode is enabled, check the permission status
+ const privateBrowsingWindow = PrivateBrowsingUtils.isWindowPrivate(window);
+ let httpsOnlyStatus = "";
+ if (
+ this._httpsOnlyModeEnabled ||
+ (privateBrowsingWindow && this._httpsOnlyModeEnabledPBM)
+ ) {
+ // Note: value and permission association is laid out
+ // in _getHttpsOnlyPermission
+ let value = this._getHttpsOnlyPermission();
+
+ // Because everything in PBM is temporary anyway, we don't need to make the distinction
+ if (privateBrowsingWindow) {
+ if (value === 2) {
+ value = 1;
+ }
+ // Hide "off temporarily" option
+ this._identityPopupHttpsOnlyModeMenuListTempItem.style.display = "none";
+ } else {
+ this._identityPopupHttpsOnlyModeMenuListTempItem.style.display = "";
+ }
+
+ this._identityPopupHttpsOnlyModeMenuList.value = value;
+
+ if (value > 0) {
+ httpsOnlyStatus = "exception";
+ } else if (this._isAboutHttpsOnlyErrorPage) {
+ httpsOnlyStatus = "failed-top";
+ } else if (this._isContentHttpsOnlyModeUpgradeFailed) {
+ httpsOnlyStatus = "failed-sub";
+ } else if (this._isContentHttpsOnlyModeUpgraded) {
+ httpsOnlyStatus = "upgraded";
+ }
+ }
+
+ // Update all elements.
+ let elementIDs = ["identity-popup", "identity-popup-securityView-body"];
+
+ for (let id of elementIDs) {
+ let element = document.getElementById(id);
+ this._updateAttribute(element, "connection", connection);
+ this._updateAttribute(element, "ciphers", ciphers);
+ this._updateAttribute(element, "mixedcontent", mixedcontent);
+ this._updateAttribute(element, "isbroken", this._isBrokenConnection);
+ this._updateAttribute(element, "customroot", customRoot);
+ this._updateAttribute(element, "httpsonlystatus", httpsOnlyStatus);
+ }
+
+ // Initialize the optional strings to empty values
+ let supplemental = "";
+ let verifier = "";
+ let host = this.getHostForDisplay();
+ let owner = "";
+
+ // Fill in the CA name if we have a valid TLS certificate.
+ if (this._isSecureConnection || this._isCertUserOverridden) {
+ verifier = this._identityIconLabel.tooltipText;
+ }
+
+ // Fill in organization information if we have a valid EV certificate.
+ if (this._isEV) {
+ let iData = this.getIdentityData();
+ owner = iData.subjectOrg;
+ verifier = this._identityIconLabel.tooltipText;
+
+ // Build an appropriate supplemental block out of whatever location data we have
+ if (iData.city) {
+ supplemental += iData.city + "\n";
+ }
+ if (iData.state && iData.country) {
+ supplemental += gNavigatorBundle.getFormattedString(
+ "identity.identified.state_and_country",
+ [iData.state, iData.country]
+ );
+ } else if (iData.state) {
+ // State only
+ supplemental += iData.state;
+ } else if (iData.country) {
+ // Country only
+ supplemental += iData.country;
+ }
+ }
+
+ // Push the appropriate strings out to the UI.
+ this._identityPopupMainViewHeaderLabel.textContent = gNavigatorBundle.getFormattedString(
+ "identity.headerMainWithHost",
+ [host]
+ );
+
+ this._identityPopupSecurityView.setAttribute(
+ "title",
+ gNavigatorBundle.getFormattedString("identity.headerSecurityWithHost", [
+ host,
+ ])
+ );
+
+ this._identityPopupSecurityEVContentOwner.textContent = gNavigatorBundle.getFormattedString(
+ "identity.ev.contentOwner2",
+ [owner]
+ );
+
+ this._identityPopupContentOwner.textContent = owner;
+ this._identityPopupContentSupp.textContent = supplemental;
+ this._identityPopupContentVerif.textContent = verifier;
+
+ // Update per-site permissions section.
+ this.updateSitePermissions();
+ },
+
+ setURI(uri) {
+ if (uri.schemeIs("view-source")) {
+ uri = Services.io.newURI(uri.spec.replace(/^view-source:/i, ""));
+ }
+ this._uri = uri;
+
+ try {
+ // Account for file: urls and catch when "" is the value
+ this._uriHasHost = !!this._uri.host;
+ } catch (ex) {
+ this._uriHasHost = false;
+ }
+
+ this._isSecureInternalUI =
+ uri.schemeIs("about") && this._secureInternalPages.test(uri.pathQueryRef);
+
+ this._pageExtensionPolicy = WebExtensionPolicy.getByURI(uri);
+
+ // Create a channel for the sole purpose of getting the resolved URI
+ // of the request to determine if it's loaded from the file system.
+ this._isURILoadedFromFile = false;
+ let chanOptions = { uri: this._uri, loadUsingSystemPrincipal: true };
+ let resolvedURI;
+ try {
+ resolvedURI = NetUtil.newChannel(chanOptions).URI;
+ if (resolvedURI.schemeIs("jar")) {
+ // Given a URI "jar:<jar-file-uri>!/<jar-entry>"
+ // create a new URI using <jar-file-uri>!/<jar-entry>
+ resolvedURI = NetUtil.newURI(resolvedURI.pathQueryRef);
+ }
+ // Check the URI again after resolving.
+ this._isURILoadedFromFile = resolvedURI.schemeIs("file");
+ } catch (ex) {
+ // NetUtil's methods will throw for malformed URIs and the like
+ }
+ },
+
+ /**
+ * Click handler for the identity-box element in primary chrome.
+ */
+ handleIdentityButtonEvent(event) {
+ event.stopPropagation();
+
+ if (
+ (event.type == "click" && event.button != 0) ||
+ (event.type == "keypress" &&
+ event.charCode != KeyEvent.DOM_VK_SPACE &&
+ event.keyCode != KeyEvent.DOM_VK_RETURN)
+ ) {
+ return; // Left click, space or enter only
+ }
+
+ // Don't allow left click, space or enter if the location has been modified,
+ // so long as we're not sharing any devices.
+ // If we are sharing a device, the identity block is prevented by CSS from
+ // being focused (and therefore, interacted with) by the user. However, we
+ // want to allow opening the identity popup from the device control menu,
+ // which calls click() on the identity button, so we don't return early.
+ if (
+ !this._sharingState &&
+ gURLBar.getAttribute("pageproxystate") != "valid"
+ ) {
+ return;
+ }
+
+ // If we are in DOM full-screen, exit it before showing the identity popup
+ // (see bug 1557041)
+ if (document.fullscreen) {
+ // Open the identity popup after DOM full-screen exit
+ // We need to wait for the exit event and after that wait for the fullscreen exit transition to complete
+ // If we call _openPopup before the full-screen transition ends it can get cancelled
+ // Only waiting for painted is not sufficient because we could still be in the full-screen enter transition.
+ this._exitedEventReceived = false;
+ this._event = event;
+ Services.obs.addObserver(this, "fullscreen-painted");
+ window.addEventListener(
+ "MozDOMFullscreen:Exited",
+ () => {
+ this._exitedEventReceived = true;
+ },
+ { once: true }
+ );
+ document.exitFullscreen();
+ return;
+ }
+ this._openPopup(event);
+ },
+
+ _openPopup(event) {
+ // Make the popup available.
+ this._initializePopup();
+
+ // Remove the reload hint that we show after a user has cleared a permission.
+ this._permissionReloadHint.setAttribute("hidden", "true");
+
+ // Update the popup strings
+ this.refreshIdentityPopup();
+
+ // Add the "open" attribute to the identity box for styling
+ this._identityBox.setAttribute("open", "true");
+
+ // Check the panel state of other panels. Hide them if needed.
+ let openPanels = Array.from(document.querySelectorAll("panel[openpanel]"));
+ for (let panel of openPanels) {
+ PanelMultiView.hidePopup(panel);
+ }
+
+ // Now open the popup, anchored off the primary chrome element
+ PanelMultiView.openPopup(this._identityPopup, this._identityIcon, {
+ position: "bottomcenter topleft",
+ triggerEvent: event,
+ }).catch(Cu.reportError);
+ },
+
+ onPopupShown(event) {
+ if (event.target == this._identityPopup) {
+ window.addEventListener("focus", this, true);
+ }
+ },
+
+ onPopupHidden(event) {
+ if (event.target == this._identityPopup) {
+ window.removeEventListener("focus", this, true);
+ this._identityBox.removeAttribute("open");
+ }
+ },
+
+ handleEvent(event) {
+ let elem = document.activeElement;
+ let position = elem.compareDocumentPosition(this._identityPopup);
+
+ if (
+ !(
+ position &
+ (Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_CONTAINED_BY)
+ ) &&
+ !this._identityPopup.hasAttribute("noautohide")
+ ) {
+ // Hide the panel when focusing an element that is
+ // neither an ancestor nor descendant unless the panel has
+ // @noautohide (e.g. for a tour).
+ PanelMultiView.hidePopup(this._identityPopup);
+ }
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "perm-changed": {
+ // Exclude permissions which do not appear in the UI in order to avoid
+ // doing extra work here.
+ if (!subject) {
+ return;
+ }
+ let { type } = subject.QueryInterface(Ci.nsIPermission);
+ if (SitePermissions.isSitePermission(type)) {
+ this.refreshIdentityBlock();
+ }
+ break;
+ }
+ case "fullscreen-painted": {
+ if (subject != window || !this._exitedEventReceived) {
+ return;
+ }
+ Services.obs.removeObserver(this, "fullscreen-painted");
+ this._openPopup(this._event);
+ delete this._event;
+ break;
+ }
+ }
+ },
+
+ onDragStart(event) {
+ const TEXT_SIZE = 14;
+ const IMAGE_SIZE = 16;
+ const SPACING = 5;
+
+ if (gURLBar.getAttribute("pageproxystate") != "valid") {
+ return;
+ }
+
+ let value = gBrowser.currentURI.displaySpec;
+ let urlString = value + "\n" + gBrowser.contentTitle;
+ let htmlString = '<a href="' + value + '">' + value + "</a>";
+
+ let windowUtils = window.windowUtils;
+ let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
+ let canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = 550 * scale;
+ let ctx = canvas.getContext("2d");
+ ctx.font = `${TEXT_SIZE * scale}px sans-serif`;
+ let tabIcon = gBrowser.selectedTab.iconImage;
+ let image = new Image();
+ image.src = tabIcon.src;
+ let textWidth = ctx.measureText(value).width / scale;
+ let textHeight = parseInt(ctx.font, 10) / scale;
+ let imageHorizontalOffset, imageVerticalOffset;
+ imageHorizontalOffset = imageVerticalOffset = SPACING;
+ let textHorizontalOffset = image.width ? IMAGE_SIZE + SPACING * 2 : SPACING;
+ let textVerticalOffset = textHeight + SPACING - 1;
+ let backgroundColor = "white";
+ let textColor = "black";
+ let totalWidth = image.width
+ ? textWidth + IMAGE_SIZE + 3 * SPACING
+ : textWidth + 2 * SPACING;
+ let totalHeight = image.width
+ ? IMAGE_SIZE + 2 * SPACING
+ : textHeight + 2 * SPACING;
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, totalWidth * scale, totalHeight * scale);
+ ctx.fillStyle = textColor;
+ ctx.fillText(
+ `${value}`,
+ textHorizontalOffset * scale,
+ textVerticalOffset * scale
+ );
+ try {
+ ctx.drawImage(
+ image,
+ imageHorizontalOffset * scale,
+ imageVerticalOffset * scale,
+ IMAGE_SIZE * scale,
+ IMAGE_SIZE * scale
+ );
+ } catch (e) {
+ // Sites might specify invalid data URIs favicons that
+ // will result in errors when trying to draw, we can
+ // just ignore this case and not paint any favicon.
+ }
+
+ let dt = event.dataTransfer;
+ dt.setData("text/x-moz-url", urlString);
+ dt.setData("text/uri-list", value);
+ dt.setData("text/plain", value);
+ dt.setData("text/html", htmlString);
+ dt.setDragImage(canvas, 16, 16);
+
+ // Don't cover potential drop targets on the toolbars or in content.
+ gURLBar.view.close();
+ },
+
+ onLocationChange() {
+ if (this._popupInitialized && this._identityPopup.state != "closed") {
+ this._permissionReloadHint.setAttribute("hidden", "true");
+
+ if (this._isPermissionListEmpty()) {
+ this._permissionEmptyHint.removeAttribute("hidden");
+ }
+ }
+ },
+
+ _updateAttribute(elem, attr, value) {
+ if (value) {
+ elem.setAttribute(attr, value);
+ } else {
+ elem.removeAttribute(attr);
+ }
+ },
+
+ _isPermissionListEmpty() {
+ return !this._permissionList.querySelectorAll(
+ ".identity-popup-permission-item"
+ ).length;
+ },
+
+ updateSitePermissions() {
+ let permissionItemSelector = [
+ ".identity-popup-permission-item, .identity-popup-permission-item-container",
+ ];
+ this._permissionList
+ .querySelectorAll(permissionItemSelector)
+ .forEach(e => e.remove());
+
+ let permissions = SitePermissions.getAllPermissionDetailsForBrowser(
+ gBrowser.selectedBrowser
+ );
+
+ if (this._sharingState && this._sharingState.geo) {
+ let geoPermission = permissions.find(perm => perm.id === "geo");
+ if (geoPermission) {
+ geoPermission.sharingState = true;
+ } else {
+ permissions.push({
+ id: "geo",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_REQUEST,
+ sharingState: true,
+ });
+ }
+ }
+
+ if (this._sharingState && this._sharingState.xr) {
+ let xrPermission = permissions.find(perm => perm.id === "xr");
+ if (xrPermission) {
+ xrPermission.sharingState = true;
+ } else {
+ permissions.push({
+ id: "xr",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_REQUEST,
+ sharingState: true,
+ });
+ }
+ }
+
+ if (this._sharingState && this._sharingState.webRTC) {
+ let webrtcState = this._sharingState.webRTC;
+ // If WebRTC device or screen permissions are in use, we need to find
+ // the associated permission item to set the sharingState field.
+ for (let id of ["camera", "microphone", "screen"]) {
+ if (webrtcState[id]) {
+ let found = false;
+ for (let permission of permissions) {
+ if (permission.id != id) {
+ continue;
+ }
+ found = true;
+ permission.sharingState = webrtcState[id];
+ break;
+ }
+ if (!found) {
+ // If the permission item we were looking for doesn't exist,
+ // the user has temporarily allowed sharing and we need to add
+ // an item in the permissions array to reflect this.
+ permissions.push({
+ id,
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_REQUEST,
+ sharingState: webrtcState[id],
+ });
+ }
+ }
+ }
+ }
+
+ let totalBlockedPopups = gBrowser.selectedBrowser.popupBlocker.getBlockedPopupCount();
+ let hasBlockedPopupIndicator = false;
+ for (let permission of permissions) {
+ let [id, key] = permission.id.split(SitePermissions.PERM_KEY_DELIMITER);
+
+ if (id == "storage-access") {
+ // Ignore storage access permissions here, they are made visible inside
+ // the Content Blocking UI.
+ continue;
+ }
+
+ let item;
+ let anchor =
+ this._permissionList.querySelector(`[anchorfor="${id}"]`) ||
+ this._defaultPermissionAnchor;
+
+ if (id == "open-protocol-handler") {
+ let permContainer = this._createProtocolHandlerPermissionItem(
+ permission,
+ key
+ );
+ if (permContainer) {
+ anchor.appendChild(permContainer);
+ }
+ } else {
+ item = this._createPermissionItem({
+ permission,
+ isContainer: id == "geo" || id == "xr",
+ nowrapLabel: id == "3rdPartyStorage",
+ });
+
+ if (!item) {
+ continue;
+ }
+ anchor.appendChild(item);
+ }
+
+ if (id == "popup" && totalBlockedPopups) {
+ this._createBlockedPopupIndicator(totalBlockedPopups);
+ hasBlockedPopupIndicator = true;
+ } else if (id == "geo" && permission.state === SitePermissions.ALLOW) {
+ this._createGeoLocationLastAccessIndicator();
+ }
+ }
+
+ if (totalBlockedPopups && !hasBlockedPopupIndicator) {
+ let permission = {
+ id: "popup",
+ state: SitePermissions.getDefault("popup"),
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ };
+ let item = this._createPermissionItem({ permission });
+ this._defaultPermissionAnchor.appendChild(item);
+ this._createBlockedPopupIndicator(totalBlockedPopups);
+ }
+
+ // Show a placeholder text if there's no permission and no reload hint.
+ if (
+ this._isPermissionListEmpty() &&
+ this._permissionReloadHint.hasAttribute("hidden")
+ ) {
+ this._permissionEmptyHint.removeAttribute("hidden");
+ } else {
+ this._permissionEmptyHint.setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Creates a permission item based on the supplied options and returns it.
+ * It is up to the caller to actually insert the element somewhere.
+ *
+ * @param permission - An object containing information representing the
+ * permission, typically obtained via SitePermissions.jsm
+ * @param isContainer - If true, the permission item will be added to a vbox
+ * and the vbox will be returned.
+ * @param permClearButton - Whether to show an "x" button to clear the permission
+ * @param showStateLabel - Whether to show a label indicating the current status
+ * of the permission e.g. "Temporary Allowed"
+ * @param idNoSuffix - Some permission types have additional information suffixed
+ * to the ID - callers can pass the unsuffixed ID via this
+ * parameter to indicate the permission type manually.
+ * @param nowrapLabel - Whether to prevent the permission item's label from
+ * wrapping its text content. This allows styling text-overflow
+ * and is useful for e.g. 3rdPartyStorage permissions whose
+ * labels are origins - which could be of any length.
+ */
+ _createPermissionItem({
+ permission,
+ isContainer = false,
+ permClearButton = true,
+ showStateLabel = true,
+ idNoSuffix = permission.id,
+ nowrapLabel = false,
+ }) {
+ let container = document.createXULElement("hbox");
+ container.setAttribute("class", "identity-popup-permission-item");
+ container.setAttribute("align", "center");
+ container.setAttribute("role", "group");
+
+ let img = document.createXULElement("image");
+ img.classList.add("identity-popup-permission-icon", idNoSuffix + "-icon");
+ if (
+ permission.state == SitePermissions.BLOCK ||
+ permission.state == SitePermissions.AUTOPLAY_BLOCKED_ALL
+ ) {
+ img.classList.add("blocked-permission-icon");
+ }
+
+ if (
+ permission.sharingState ==
+ Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
+ (idNoSuffix == "screen" &&
+ permission.sharingState &&
+ !permission.sharingState.includes("Paused"))
+ ) {
+ img.classList.add("in-use");
+
+ // Synchronize control center and identity block blinking animations.
+ window
+ .promiseDocumentFlushed(() => {
+ let sharingIconBlink = this._webRTCSharingIcon.getAnimations()[0];
+ let imgBlink = img.getAnimations()[0];
+ return [sharingIconBlink, imgBlink];
+ })
+ .then(([sharingIconBlink, imgBlink]) => {
+ if (sharingIconBlink && imgBlink) {
+ imgBlink.startTime = sharingIconBlink.startTime;
+ }
+ });
+ }
+
+ let nameLabel = document.createXULElement("label");
+ nameLabel.setAttribute("flex", "1");
+ nameLabel.setAttribute("class", "identity-popup-permission-label");
+ let label = SitePermissions.getPermissionLabel(idNoSuffix);
+ if (label === null) {
+ return null;
+ }
+ if (nowrapLabel) {
+ nameLabel.setAttribute("value", label);
+ nameLabel.setAttribute("tooltiptext", label);
+ nameLabel.setAttribute("crop", "end");
+ } else {
+ nameLabel.textContent = label;
+ }
+ let nameLabelId = "identity-popup-permission-label-" + idNoSuffix;
+ nameLabel.setAttribute("id", nameLabelId);
+
+ let isPolicyPermission = [
+ SitePermissions.SCOPE_POLICY,
+ SitePermissions.SCOPE_GLOBAL,
+ ].includes(permission.scope);
+
+ if (
+ (idNoSuffix == "popup" && !isPolicyPermission) ||
+ idNoSuffix == "autoplay-media"
+ ) {
+ let menulist = document.createXULElement("menulist");
+ let menupopup = document.createXULElement("menupopup");
+ let block = document.createXULElement("vbox");
+ block.setAttribute("id", "identity-popup-popup-container");
+ block.setAttribute("class", "identity-popup-permission-item-container");
+ menulist.setAttribute("sizetopopup", "none");
+ menulist.setAttribute("id", "identity-popup-popup-menulist");
+
+ for (let state of SitePermissions.getAvailableStates(idNoSuffix)) {
+ let menuitem = document.createXULElement("menuitem");
+ // We need to correctly display the default/unknown state, which has its
+ // own integer value (0) but represents one of the other states.
+ if (state == SitePermissions.getDefault(idNoSuffix)) {
+ menuitem.setAttribute("value", "0");
+ } else {
+ menuitem.setAttribute("value", state);
+ }
+
+ menuitem.setAttribute(
+ "label",
+ SitePermissions.getMultichoiceStateLabel(idNoSuffix, state)
+ );
+ menupopup.appendChild(menuitem);
+ }
+
+ menulist.appendChild(menupopup);
+
+ if (permission.state == SitePermissions.getDefault(idNoSuffix)) {
+ menulist.value = "0";
+ } else {
+ menulist.value = permission.state;
+ }
+
+ // Avoiding listening to the "select" event on purpose. See Bug 1404262.
+ menulist.addEventListener("command", () => {
+ SitePermissions.setForPrincipal(
+ gBrowser.contentPrincipal,
+ idNoSuffix,
+ menulist.selectedItem.value
+ );
+ });
+
+ container.appendChild(img);
+ container.appendChild(nameLabel);
+ container.appendChild(menulist);
+ container.setAttribute("aria-labelledby", nameLabelId);
+ block.appendChild(container);
+
+ return block;
+ }
+
+ container.appendChild(img);
+ container.appendChild(nameLabel);
+ let labelledBy = nameLabelId;
+
+ if (showStateLabel) {
+ let stateLabel = this._createStateLabel(permission, idNoSuffix);
+ container.appendChild(stateLabel);
+ labelledBy += " " + stateLabel.id;
+ }
+
+ container.setAttribute("aria-labelledby", labelledBy);
+
+ /* We return the permission item here without a remove button if the permission is a
+ SCOPE_POLICY or SCOPE_GLOBAL permission. Policy permissions cannot be
+ removed/changed for the duration of the browser session. */
+ if (isPolicyPermission) {
+ return container;
+ }
+
+ if (isContainer) {
+ let block = document.createXULElement("vbox");
+ block.setAttribute("id", "identity-popup-" + idNoSuffix + "-container");
+ block.setAttribute("class", "identity-popup-permission-item-container");
+
+ if (permClearButton) {
+ let button = this._createPermissionClearButton(permission, block);
+ container.appendChild(button);
+ }
+
+ block.appendChild(container);
+ return block;
+ }
+
+ if (permClearButton) {
+ let button = this._createPermissionClearButton(permission, container);
+ container.appendChild(button);
+ }
+
+ return container;
+ },
+
+ _createStateLabel(aPermission, idNoSuffix) {
+ let label = document.createXULElement("label");
+ label.setAttribute("flex", "1");
+ label.setAttribute("class", "identity-popup-permission-state-label");
+ let labelId = "identity-popup-permission-state-label-" + idNoSuffix;
+ label.setAttribute("id", labelId);
+ let { state, scope } = aPermission;
+ // If the user did not permanently allow this device but it is currently
+ // used, set the variables to display a "temporarily allowed" info.
+ if (state != SitePermissions.ALLOW && aPermission.sharingState) {
+ state = SitePermissions.ALLOW;
+ scope = SitePermissions.SCOPE_REQUEST;
+ }
+ label.textContent = SitePermissions.getCurrentStateLabel(
+ state,
+ idNoSuffix,
+ scope
+ );
+ return label;
+ },
+
+ _removePermPersistentAllow(principal, id) {
+ let perm = SitePermissions.getForPrincipal(principal, id);
+ if (
+ perm.state == SitePermissions.ALLOW &&
+ perm.scope == SitePermissions.SCOPE_PERSISTENT
+ ) {
+ SitePermissions.removeFromPrincipal(principal, id);
+ }
+ },
+
+ _createPermissionClearButton(
+ aPermission,
+ container,
+ clearCallback = () => {}
+ ) {
+ let button = document.createXULElement("button");
+ button.setAttribute("class", "identity-popup-permission-remove-button");
+ let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
+ button.setAttribute("tooltiptext", tooltiptext);
+ button.addEventListener("command", () => {
+ let browser = gBrowser.selectedBrowser;
+ container.remove();
+ if (aPermission.sharingState) {
+ if (aPermission.id === "geo" || aPermission.id === "xr") {
+ let origins = browser.getDevicePermissionOrigins(aPermission.id);
+ for (let origin of origins) {
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ origin
+ );
+ this._removePermPersistentAllow(principal, aPermission.id);
+ }
+ origins.clear();
+ } else if (
+ ["camera", "microphone", "screen"].includes(aPermission.id)
+ ) {
+ let windowId = this._sharingState.webRTC.windowId;
+ if (aPermission.id == "screen") {
+ windowId = "screen:" + windowId;
+ } else {
+ // If we set persistent permissions or the sharing has
+ // started due to existing persistent permissions, we need
+ // to handle removing these even for frames with different hostnames.
+ let origins = browser.getDevicePermissionOrigins("webrtc");
+ for (let origin of origins) {
+ // It's not possible to stop sharing one of camera/microphone
+ // without the other.
+ let principal;
+ for (let id of ["camera", "microphone"]) {
+ if (this._sharingState.webRTC[id]) {
+ if (!principal) {
+ principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ origin
+ );
+ }
+ this._removePermPersistentAllow(principal, id);
+ }
+ }
+ }
+ }
+
+ let bc = this._sharingState.webRTC.browsingContext;
+ bc.currentWindowGlobal
+ .getActor("WebRTC")
+ .sendAsyncMessage("webrtc:StopSharing", windowId);
+ webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
+ }
+ }
+ SitePermissions.removeFromPrincipal(
+ gBrowser.contentPrincipal,
+ aPermission.id,
+ browser
+ );
+
+ this._permissionReloadHint.removeAttribute("hidden");
+ PanelView.forNode(
+ this._identityPopupMainView
+ ).descriptionHeightWorkaround();
+
+ if (aPermission.id === "geo") {
+ gBrowser.updateBrowserSharing(browser, { geo: false });
+ } else if (aPermission.id === "xr") {
+ gBrowser.updateBrowserSharing(browser, { xr: false });
+ }
+
+ clearCallback();
+ });
+
+ return button;
+ },
+
+ _getGeoLocationLastAccess() {
+ return new Promise(resolve => {
+ let lastAccess = null;
+ ContentPrefService2.getByDomainAndName(
+ gBrowser.currentURI.spec,
+ "permissions.geoLocation.lastAccess",
+ gBrowser.selectedBrowser.loadContext,
+ {
+ handleResult(pref) {
+ lastAccess = pref.value;
+ },
+ handleCompletion() {
+ resolve(lastAccess);
+ },
+ }
+ );
+ });
+ },
+
+ async _createGeoLocationLastAccessIndicator() {
+ let lastAccessStr = await this._getGeoLocationLastAccess();
+ let geoContainer = document.getElementById("identity-popup-geo-container");
+
+ // Check whether geoContainer still exists.
+ // We are async, the identity popup could have been closed already.
+ // Also check if it is already populated with a time label.
+ // This can happen if we update the permission panel multiple times in a
+ // short timeframe.
+ if (
+ lastAccessStr == null ||
+ !geoContainer ||
+ document.getElementById("geo-access-indicator-item")
+ ) {
+ return;
+ }
+ let lastAccess = new Date(lastAccessStr);
+ if (isNaN(lastAccess)) {
+ Cu.reportError("Invalid timestamp for last geolocation access");
+ return;
+ }
+
+ let icon = document.createXULElement("image");
+ icon.setAttribute("class", "popup-subitem");
+
+ let indicator = document.createXULElement("hbox");
+ indicator.setAttribute("class", "identity-popup-permission-item");
+ indicator.setAttribute("align", "center");
+ indicator.setAttribute("id", "geo-access-indicator-item");
+
+ let timeFormat = new Services.intl.RelativeTimeFormat(undefined, {});
+
+ let text = document.createXULElement("label");
+ text.setAttribute("flex", "1");
+ text.setAttribute("class", "identity-popup-permission-label");
+
+ text.textContent = gNavigatorBundle.getFormattedString(
+ "geolocationLastAccessIndicatorText",
+ [timeFormat.formatBestUnit(lastAccess)]
+ );
+
+ indicator.appendChild(icon);
+ indicator.appendChild(text);
+
+ geoContainer.appendChild(indicator);
+ },
+
+ _createProtocolHandlerPermissionItem(permission, key) {
+ let container = document.getElementById(
+ "identity-popup-open-protocol-handler-container"
+ );
+ let initialCall;
+
+ if (!container) {
+ // First open-protocol-handler permission, create container.
+ container = this._createPermissionItem({
+ permission,
+ isContainer: true,
+ permClearButton: false,
+ showStateLabel: false,
+ idNoSuffix: "open-protocol-handler",
+ });
+ initialCall = true;
+ }
+
+ let icon = document.createXULElement("image");
+ icon.setAttribute("class", "popup-subitem-no-arrow");
+
+ let item = document.createXULElement("hbox");
+ item.setAttribute("class", "identity-popup-permission-item");
+ item.setAttribute("align", "center");
+
+ let text = document.createXULElement("label");
+ text.setAttribute("flex", "1");
+ text.setAttribute("class", "identity-popup-permission-label-subitem");
+
+ text.textContent = gNavigatorBundle.getFormattedString(
+ "openProtocolHandlerPermissionEntryLabel",
+ [key]
+ );
+
+ let stateLabel = this._createStateLabel(
+ permission,
+ "open-protocol-handler"
+ );
+
+ item.appendChild(text);
+ item.appendChild(stateLabel);
+
+ let button = this._createPermissionClearButton(permission, item, () => {
+ // When we're clearing the last open-protocol-handler permission, clean up
+ // the empty container.
+ // (<= 1 because the heading item is also a child of the container)
+ if (container.childElementCount <= 1) {
+ container.remove();
+ }
+ });
+ item.appendChild(button);
+
+ container.appendChild(item);
+
+ // If container already exists in permission list, don't return it again.
+ return initialCall && container;
+ },
+
+ _createBlockedPopupIndicator(aTotalBlockedPopups) {
+ let indicator = document.createXULElement("hbox");
+ indicator.setAttribute("class", "identity-popup-permission-item");
+ indicator.setAttribute("align", "center");
+ indicator.setAttribute("id", "blocked-popup-indicator-item");
+
+ let icon = document.createXULElement("image");
+ icon.setAttribute("class", "popup-subitem");
+
+ let text = document.createXULElement("label", { is: "text-link" });
+ text.setAttribute("flex", "1");
+ text.setAttribute("class", "identity-popup-permission-label");
+
+ let messageBase = gNavigatorBundle.getString(
+ "popupShowBlockedPopupsIndicatorText"
+ );
+ let message = PluralForm.get(aTotalBlockedPopups, messageBase).replace(
+ "#1",
+ aTotalBlockedPopups
+ );
+ text.textContent = message;
+
+ text.addEventListener("click", () => {
+ gBrowser.selectedBrowser.popupBlocker.unblockAllPopups();
+ });
+
+ indicator.appendChild(icon);
+ indicator.appendChild(text);
+
+ document
+ .getElementById("identity-popup-popup-container")
+ .appendChild(indicator);
+ },
+};
diff --git a/browser/base/content/browser-siteProtections.js b/browser/base/content/browser-siteProtections.js
new file mode 100644
index 0000000000..1b3d2a5d92
--- /dev/null
+++ b/browser/base/content/browser-siteProtections.js
@@ -0,0 +1,2538 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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-env mozilla/browser-window */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ContentBlockingAllowList:
+ "resource://gre/modules/ContentBlockingAllowList.jsm",
+ ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+var Fingerprinting = {
+ PREF_ENABLED: "privacy.trackingprotection.fingerprinting.enabled",
+ reportBreakageLabel: "fingerprinting",
+
+ strings: {
+ get subViewBlocked() {
+ delete this.subViewBlocked;
+ return (this.subViewBlocked = gNavigatorBundle.getString(
+ "contentBlocking.fingerprintersView.blocked.label"
+ ));
+ },
+
+ get subViewTitleBlocking() {
+ delete this.subViewTitleBlocking;
+ return (this.subViewTitleBlocking = gNavigatorBundle.getString(
+ "protections.blocking.fingerprinters.title"
+ ));
+ },
+
+ get subViewTitleNotBlocking() {
+ delete this.subViewTitleNotBlocking;
+ return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
+ "protections.notBlocking.fingerprinters.title"
+ ));
+ },
+ },
+
+ init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "enabled",
+ this.PREF_ENABLED,
+ false,
+ this.updateCategoryItem.bind(this)
+ );
+ },
+
+ get categoryItem() {
+ let item = document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+ if (item) {
+ delete this.categoryItem;
+ this.categoryItem = item;
+ }
+ return item;
+ },
+
+ updateCategoryItem() {
+ // Can't get `this.categoryItem` without the popup. Using the popup instead
+ // of `this.categoryItem` to guard access, because the category item getter
+ // can trigger bug 1543537. If there's no popup, we'll be called again the
+ // first time the popup shows.
+ if (gProtectionsHandler._protectionsPopup) {
+ this.categoryItem.classList.toggle("blocked", this.enabled);
+ }
+ },
+
+ get subView() {
+ delete this.subView;
+ return (this.subView = document.getElementById(
+ "protections-popup-fingerprintersView"
+ ));
+ },
+
+ get subViewList() {
+ delete this.subViewList;
+ return (this.subViewList = document.getElementById(
+ "protections-popup-fingerprintersView-list"
+ ));
+ },
+
+ isBlocking(state) {
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT) !=
+ 0
+ );
+ },
+
+ isAllowing(state) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT) !=
+ 0
+ );
+ },
+
+ isDetected(state) {
+ return this.isBlocking(state) || this.isAllowing(state);
+ },
+
+ isShimming(state) {
+ return (
+ state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT &&
+ this.isAllowing(state)
+ );
+ },
+
+ updateSubView() {
+ let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
+ contentBlockingLog = JSON.parse(contentBlockingLog);
+
+ let fragment = document.createDocumentFragment();
+ for (let [origin, actions] of Object.entries(contentBlockingLog)) {
+ let listItem = this._createListItem(origin, actions);
+ if (listItem) {
+ fragment.appendChild(listItem);
+ }
+ }
+
+ this.subViewList.textContent = "";
+ this.subViewList.append(fragment);
+ this.subView.setAttribute(
+ "title",
+ this.enabled && !gProtectionsHandler.hasException
+ ? this.strings.subViewTitleBlocking
+ : this.strings.subViewTitleNotBlocking
+ );
+ },
+
+ _createListItem(origin, actions) {
+ let isAllowed = actions.some(
+ ([state]) => this.isAllowing(state) && !this.isShimming(state)
+ );
+ let isDetected =
+ isAllowed || actions.some(([state]) => this.isBlocking(state));
+
+ if (!isDetected) {
+ return null;
+ }
+
+ let listItem = document.createXULElement("hbox");
+ listItem.className = "protections-popup-list-item";
+ listItem.classList.toggle("allowed", isAllowed);
+ // Repeat the host in the tooltip in case it's too long
+ // and overflows in our panel.
+ listItem.tooltipText = origin;
+
+ let label = document.createXULElement("label");
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ return listItem;
+ },
+};
+
+var Cryptomining = {
+ PREF_ENABLED: "privacy.trackingprotection.cryptomining.enabled",
+ reportBreakageLabel: "cryptomining",
+
+ strings: {
+ get subViewBlocked() {
+ delete this.subViewBlocked;
+ return (this.subViewBlocked = gNavigatorBundle.getString(
+ "contentBlocking.cryptominersView.blocked.label"
+ ));
+ },
+
+ get subViewTitleBlocking() {
+ delete this.subViewTitleBlocking;
+ return (this.subViewTitleBlocking = gNavigatorBundle.getString(
+ "protections.blocking.cryptominers.title"
+ ));
+ },
+
+ get subViewTitleNotBlocking() {
+ delete this.subViewTitleNotBlocking;
+ return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
+ "protections.notBlocking.cryptominers.title"
+ ));
+ },
+ },
+
+ init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "enabled",
+ this.PREF_ENABLED,
+ false,
+ this.updateCategoryItem.bind(this)
+ );
+ },
+
+ get categoryItem() {
+ let item = document.getElementById(
+ "protections-popup-category-cryptominers"
+ );
+ if (item) {
+ delete this.categoryItem;
+ this.categoryItem = item;
+ }
+ return item;
+ },
+
+ updateCategoryItem() {
+ // Can't get `this.categoryItem` without the popup. Using the popup instead
+ // of `this.categoryItem` to guard access, because the category item getter
+ // can trigger bug 1543537. If there's no popup, we'll be called again the
+ // first time the popup shows.
+ if (gProtectionsHandler._protectionsPopup) {
+ this.categoryItem.classList.toggle("blocked", this.enabled);
+ }
+ },
+
+ get subView() {
+ delete this.subView;
+ return (this.subView = document.getElementById(
+ "protections-popup-cryptominersView"
+ ));
+ },
+
+ get subViewList() {
+ delete this.subViewList;
+ return (this.subViewList = document.getElementById(
+ "protections-popup-cryptominersView-list"
+ ));
+ },
+
+ isBlocking(state) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT) !=
+ 0
+ );
+ },
+
+ isAllowing(state) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT) != 0
+ );
+ },
+
+ isDetected(state) {
+ return this.isBlocking(state) || this.isAllowing(state);
+ },
+
+ isShimming(state) {
+ return (
+ state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT &&
+ this.isAllowing(state)
+ );
+ },
+
+ updateSubView() {
+ let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
+ contentBlockingLog = JSON.parse(contentBlockingLog);
+
+ let fragment = document.createDocumentFragment();
+ for (let [origin, actions] of Object.entries(contentBlockingLog)) {
+ let listItem = this._createListItem(origin, actions);
+ if (listItem) {
+ fragment.appendChild(listItem);
+ }
+ }
+
+ this.subViewList.textContent = "";
+ this.subViewList.append(fragment);
+ this.subView.setAttribute(
+ "title",
+ this.enabled && !gProtectionsHandler.hasException
+ ? this.strings.subViewTitleBlocking
+ : this.strings.subViewTitleNotBlocking
+ );
+ },
+
+ _createListItem(origin, actions) {
+ let isAllowed = actions.some(
+ ([state]) => this.isAllowing(state) && !this.isShimming(state)
+ );
+ let isDetected =
+ isAllowed || actions.some(([state]) => this.isBlocking(state));
+
+ if (!isDetected) {
+ return null;
+ }
+
+ let listItem = document.createXULElement("hbox");
+ listItem.className = "protections-popup-list-item";
+ listItem.classList.toggle("allowed", isAllowed);
+ // Repeat the host in the tooltip in case it's too long
+ // and overflows in our panel.
+ listItem.tooltipText = origin;
+
+ let label = document.createXULElement("label");
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ return listItem;
+ },
+};
+
+var TrackingProtection = {
+ reportBreakageLabel: "trackingprotection",
+ PREF_ENABLED_GLOBALLY: "privacy.trackingprotection.enabled",
+ PREF_ENABLED_IN_PRIVATE_WINDOWS: "privacy.trackingprotection.pbmode.enabled",
+ PREF_TRACKING_TABLE: "urlclassifier.trackingTable",
+ PREF_TRACKING_ANNOTATION_TABLE: "urlclassifier.trackingAnnotationTable",
+ PREF_ANNOTATIONS_LEVEL_2_ENABLED:
+ "privacy.annotate_channels.strict_list.enabled",
+ enabledGlobally: false,
+ enabledInPrivateWindows: false,
+
+ get categoryItem() {
+ let item = document.getElementById(
+ "protections-popup-category-tracking-protection"
+ );
+ if (item) {
+ delete this.categoryItem;
+ this.categoryItem = item;
+ }
+ return item;
+ },
+
+ get subView() {
+ delete this.subView;
+ return (this.subView = document.getElementById(
+ "protections-popup-trackersView"
+ ));
+ },
+
+ get subViewList() {
+ delete this.subViewList;
+ return (this.subViewList = document.getElementById(
+ "protections-popup-trackersView-list"
+ ));
+ },
+
+ strings: {
+ get subViewBlocked() {
+ delete this.subViewBlocked;
+ return (this.subViewBlocked = gNavigatorBundle.getString(
+ "contentBlocking.trackersView.blocked.label"
+ ));
+ },
+
+ get subViewTitleBlocking() {
+ delete this.subViewTitleBlocking;
+ return (this.subViewTitleBlocking = gNavigatorBundle.getString(
+ "protections.blocking.trackingContent.title"
+ ));
+ },
+
+ get subViewTitleNotBlocking() {
+ delete this.subViewTitleNotBlocking;
+ return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
+ "protections.notBlocking.trackingContent.title"
+ ));
+ },
+ },
+
+ init() {
+ this.updateEnabled();
+
+ Services.prefs.addObserver(this.PREF_ENABLED_GLOBALLY, this);
+ Services.prefs.addObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this);
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "trackingTable",
+ this.PREF_TRACKING_TABLE,
+ false
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "trackingAnnotationTable",
+ this.PREF_TRACKING_ANNOTATION_TABLE,
+ false
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "annotationsLevel2Enabled",
+ this.PREF_ANNOTATIONS_LEVEL_2_ENABLED,
+ false
+ );
+ },
+
+ uninit() {
+ Services.prefs.removeObserver(this.PREF_ENABLED_GLOBALLY, this);
+ Services.prefs.removeObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this);
+ },
+
+ observe() {
+ this.updateEnabled();
+ this.updateCategoryItem();
+ },
+
+ get trackingProtectionLevel2Enabled() {
+ const CONTENT_TABLE = "content-track-digest256";
+ return this.trackingTable.includes(CONTENT_TABLE);
+ },
+
+ get enabled() {
+ return (
+ this.enabledGlobally ||
+ (this.enabledInPrivateWindows &&
+ PrivateBrowsingUtils.isWindowPrivate(window))
+ );
+ },
+
+ updateEnabled() {
+ this.enabledGlobally = Services.prefs.getBoolPref(
+ this.PREF_ENABLED_GLOBALLY
+ );
+ this.enabledInPrivateWindows = Services.prefs.getBoolPref(
+ this.PREF_ENABLED_IN_PRIVATE_WINDOWS
+ );
+ },
+
+ updateCategoryItem() {
+ if (this.categoryItem) {
+ this.categoryItem.classList.toggle("blocked", this.enabled);
+ }
+ },
+
+ isBlocking(state) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) != 0
+ );
+ },
+
+ isAllowingLevel1(state) {
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT) !=
+ 0
+ );
+ },
+
+ isAllowingLevel2(state) {
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT) !=
+ 0
+ );
+ },
+
+ isAllowing(state) {
+ return this.isAllowingLevel1(state) || this.isAllowingLevel2(state);
+ },
+
+ isDetected(state) {
+ return this.isBlocking(state) || this.isAllowing(state);
+ },
+
+ isShimming(state) {
+ return (
+ state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT &&
+ this.isAllowing(state)
+ );
+ },
+
+ async updateSubView() {
+ let previousURI = gBrowser.currentURI.spec;
+ let previousWindow = gBrowser.selectedBrowser.innerWindowID;
+
+ let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
+ contentBlockingLog = JSON.parse(contentBlockingLog);
+
+ let fragment = document.createDocumentFragment();
+ for (let [origin, actions] of Object.entries(contentBlockingLog)) {
+ let listItem = await this._createListItem(origin, actions);
+ if (listItem) {
+ fragment.appendChild(listItem);
+ }
+ }
+
+ // If we don't have trackers we would usually not show the menu item
+ // allowing the user to show the sub-panel. However, in the edge case
+ // that we annotated trackers on the page using the strict list but did
+ // not detect trackers on the page using the basic list, we currently
+ // still show the panel. To reduce the confusion, tell the user that we have
+ // not detected any tracker.
+ if (!fragment.childNodes.length) {
+ let emptyBox = document.createXULElement("vbox");
+ let emptyImage = document.createXULElement("image");
+ emptyImage.classList.add("protections-popup-trackersView-empty-image");
+ emptyImage.classList.add("tracking-protection-icon");
+
+ let emptyLabel = document.createXULElement("label");
+ emptyLabel.classList.add("protections-popup-empty-label");
+ emptyLabel.textContent = gNavigatorBundle.getString(
+ "contentBlocking.trackersView.empty.label"
+ );
+
+ emptyBox.appendChild(emptyImage);
+ emptyBox.appendChild(emptyLabel);
+ fragment.appendChild(emptyBox);
+
+ this.subViewList.classList.add("empty");
+ } else {
+ this.subViewList.classList.remove("empty");
+ }
+
+ // This might have taken a while. Only update the list if we're still on the same page.
+ if (
+ previousURI == gBrowser.currentURI.spec &&
+ previousWindow == gBrowser.selectedBrowser.innerWindowID
+ ) {
+ this.subViewList.textContent = "";
+ this.subViewList.append(fragment);
+ this.subView.setAttribute(
+ "title",
+ this.enabled && !gProtectionsHandler.hasException
+ ? this.strings.subViewTitleBlocking
+ : this.strings.subViewTitleNotBlocking
+ );
+ }
+ },
+
+ async _createListItem(origin, actions) {
+ // Figure out if this list entry was actually detected by TP or something else.
+ let isAllowed = actions.some(
+ ([state]) => this.isAllowing(state) && !this.isShimming(state)
+ );
+ let isDetected =
+ isAllowed || actions.some(([state]) => this.isBlocking(state));
+
+ if (!isDetected) {
+ return null;
+ }
+
+ // Because we might use different lists for annotation vs. blocking, we
+ // need to make sure that this is a tracker that we would actually have blocked
+ // before showing it to the user.
+ if (
+ this.annotationsLevel2Enabled &&
+ !this.trackingProtectionLevel2Enabled &&
+ actions.some(
+ ([state]) =>
+ (state &
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT) !=
+ 0
+ )
+ ) {
+ return null;
+ }
+
+ let listItem = document.createXULElement("hbox");
+ listItem.className = "protections-popup-list-item";
+ listItem.classList.toggle("allowed", isAllowed);
+ // Repeat the host in the tooltip in case it's too long
+ // and overflows in our panel.
+ listItem.tooltipText = origin;
+
+ let label = document.createXULElement("label");
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ return listItem;
+ },
+};
+
+var ThirdPartyCookies = {
+ PREF_ENABLED: "network.cookie.cookieBehavior",
+ PREF_ENABLED_VALUES: [
+ // These values match the ones exposed under the Content Blocking section
+ // of the Preferences UI.
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, // Block all third-party cookies
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, // Block third-party cookies from trackers
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // Block trackers and patition third-party trackers
+ Ci.nsICookieService.BEHAVIOR_REJECT, // Block all cookies
+ ],
+
+ get categoryItem() {
+ let item = document.getElementById("protections-popup-category-cookies");
+ if (item) {
+ delete this.categoryItem;
+ this.categoryItem = item;
+ }
+ return item;
+ },
+
+ get subView() {
+ delete this.subView;
+ return (this.subView = document.getElementById(
+ "protections-popup-cookiesView"
+ ));
+ },
+
+ get subViewHeading() {
+ delete this.subViewHeading;
+ return (this.subViewHeading = document.getElementById(
+ "protections-popup-cookiesView-heading"
+ ));
+ },
+
+ get subViewList() {
+ delete this.subViewList;
+ return (this.subViewList = document.getElementById(
+ "protections-popup-cookiesView-list"
+ ));
+ },
+
+ strings: {
+ get subViewAllowed() {
+ delete this.subViewAllowed;
+ return (this.subViewAllowed = gNavigatorBundle.getString(
+ "contentBlocking.cookiesView.allowed.label"
+ ));
+ },
+
+ get subViewBlocked() {
+ delete this.subViewAllowed;
+ return (this.subViewAllowed = gNavigatorBundle.getString(
+ "contentBlocking.cookiesView.blocked.label"
+ ));
+ },
+
+ get subViewTitleNotBlocking() {
+ delete this.subViewTitleNotBlocking;
+ return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
+ "protections.notBlocking.crossSiteTrackingCookies.title"
+ ));
+ },
+ },
+
+ get reportBreakageLabel() {
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ return "nocookiesblocked";
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ return "allthirdpartycookiesblocked";
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ return "allcookiesblocked";
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ return "cookiesfromunvisitedsitesblocked";
+ default:
+ Cu.reportError(
+ `Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}`
+ );
+ // fall through
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ return "cookierestrictions";
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ return "cookierestrictionsforeignpartitioned";
+ }
+ },
+
+ init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "behaviorPref",
+ this.PREF_ENABLED,
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ this.updateCategoryItem.bind(this)
+ );
+ },
+
+ get categoryLabel() {
+ delete this.categoryLabel;
+ return (this.categoryLabel = document.getElementById(
+ "protections-popup-cookies-category-label"
+ ));
+ },
+
+ updateCategoryItem() {
+ // Can't get `this.categoryItem` without the popup. Using the popup instead
+ // of `this.categoryItem` to guard access, because the category item getter
+ // can trigger bug 1543537. If there's no popup, we'll be called again the
+ // first time the popup shows.
+ if (!gProtectionsHandler._protectionsPopup) {
+ return;
+ }
+ this.categoryItem.classList.toggle("blocked", this.enabled);
+
+ let label;
+
+ if (!this.enabled) {
+ label = "contentBlocking.cookies.blockingTrackers3.label";
+ } else {
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ label = "contentBlocking.cookies.blocking3rdParty2.label";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ label = "contentBlocking.cookies.blockingAll2.label";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ label = "contentBlocking.cookies.blockingUnvisited2.label";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ label = "contentBlocking.cookies.blockingTrackers3.label";
+ break;
+ default:
+ Cu.reportError(
+ `Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}`
+ );
+ break;
+ }
+ }
+ this.categoryLabel.textContent = label
+ ? gNavigatorBundle.getString(label)
+ : "";
+ },
+
+ get enabled() {
+ return this.PREF_ENABLED_VALUES.includes(this.behaviorPref);
+ },
+
+ isBlocking(state) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER) != 0 ||
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
+ 0 ||
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL) != 0 ||
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION) !=
+ 0 ||
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN) != 0
+ );
+ },
+
+ isDetected(state) {
+ if (this.isBlocking(state)) {
+ return true;
+ }
+
+ if (
+ [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ ].includes(this.behaviorPref)
+ ) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER) != 0 ||
+ (SocialTracking.enabled &&
+ (state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
+ 0)
+ );
+ }
+
+ // We don't have specific flags for the other cookie behaviors so just
+ // fall back to STATE_COOKIES_LOADED.
+ return (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED) != 0;
+ },
+
+ updateSubView() {
+ let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
+ contentBlockingLog = JSON.parse(contentBlockingLog);
+
+ let categories = this._processContentBlockingLog(contentBlockingLog);
+
+ this.subViewList.textContent = "";
+
+ let categoryNames = ["trackers"];
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ categoryNames.push("firstParty");
+ // eslint-disable-next-line no-fallthrough
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ categoryNames.push("thirdParty");
+ }
+
+ for (let category of categoryNames) {
+ let itemsToShow = categories[category];
+
+ if (!itemsToShow.length) {
+ continue;
+ }
+
+ let box = document.createXULElement("vbox");
+ box.className = "protections-popup-cookiesView-list-section";
+ let label = document.createXULElement("label");
+ label.className = "protections-popup-cookiesView-list-header";
+ label.textContent = gNavigatorBundle.getString(
+ `contentBlocking.cookiesView.${
+ category == "trackers" ? "trackers2" : category
+ }.label`
+ );
+ box.appendChild(label);
+
+ for (let info of itemsToShow) {
+ box.appendChild(this._createListItem(info));
+ }
+
+ this.subViewList.appendChild(box);
+ }
+
+ this.subViewHeading.hidden = false;
+ if (!this.enabled) {
+ this.subView.setAttribute("title", this.strings.subViewTitleNotBlocking);
+ return;
+ }
+
+ let title;
+ let siteException = gProtectionsHandler.hasException;
+ let titleStringPrefix = `protections.${
+ siteException ? "notBlocking" : "blocking"
+ }.cookies.`;
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ title = titleStringPrefix + "3rdParty.title";
+ this.subViewHeading.hidden = true;
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ title = titleStringPrefix + "all.title";
+ this.subViewHeading.hidden = true;
+ break;
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ title = "protections.blocking.cookies.unvisited.title";
+ this.subViewHeading.hidden = true;
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ title = siteException
+ ? "protections.notBlocking.crossSiteTrackingCookies.title"
+ : "protections.blocking.cookies.trackers.title";
+ break;
+ default:
+ Cu.reportError(
+ `Error: Unknown cookieBehavior pref when updating subview: ${this.behaviorPref}`
+ );
+ break;
+ }
+
+ this.subView.setAttribute("title", gNavigatorBundle.getString(title));
+ },
+
+ _getExceptionState(origin) {
+ for (let perm of Services.perms.getAllForPrincipal(
+ gBrowser.contentPrincipal
+ )) {
+ if (perm.type == "3rdPartyStorage^" + origin) {
+ return perm.capability;
+ }
+ }
+
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ origin
+ );
+ // Cookie exceptions get "inherited" from parent- to sub-domain, so we need to
+ // make sure to include parent domains in the permission check for "cookie".
+ return Services.perms.testPermissionFromPrincipal(principal, "cookie");
+ },
+
+ _clearException(origin) {
+ for (let perm of Services.perms.getAllForPrincipal(
+ gBrowser.contentPrincipal
+ )) {
+ if (perm.type == "3rdPartyStorage^" + origin) {
+ Services.perms.removePermission(perm);
+ }
+ }
+
+ // OAs don't matter here, so we can just use the hostname.
+ let host = Services.io.newURI(origin).host;
+
+ // Cookie exceptions get "inherited" from parent- to sub-domain, so we need to
+ // clear any cookie permissions from parent domains as well.
+ for (let perm of Services.perms.all) {
+ if (
+ perm.type == "cookie" &&
+ Services.eTLD.hasRootDomain(host, perm.principal.host)
+ ) {
+ Services.perms.removePermission(perm);
+ }
+ }
+ },
+
+ // Transforms and filters cookie entries in the content blocking log
+ // so that we can categorize and display them in the UI.
+ _processContentBlockingLog(log) {
+ let newLog = {
+ firstParty: [],
+ trackers: [],
+ thirdParty: [],
+ };
+
+ let firstPartyDomain = null;
+ try {
+ firstPartyDomain = Services.eTLD.getBaseDomain(gBrowser.currentURI);
+ } catch (e) {
+ // There are nasty edge cases here where someone is trying to set a cookie
+ // on a public suffix or an IP address. Just categorize those as third party...
+ if (
+ e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
+ e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
+ ) {
+ throw e;
+ }
+ }
+
+ for (let [origin, actions] of Object.entries(log)) {
+ if (!origin.startsWith("http")) {
+ continue;
+ }
+
+ let info = {
+ origin,
+ isAllowed: true,
+ exceptionState: this._getExceptionState(origin),
+ };
+ let hasCookie = false;
+ let isTracker = false;
+
+ // Extract information from the states entries in the content blocking log.
+ // Each state will contain a single state flag from nsIWebProgressListener.
+ // Note that we are using the same helper functions that are applied to the
+ // bit map passed to onSecurityChange (which contains multiple states), thus
+ // not checking exact equality, just presence of bits.
+ for (let [state, blocked] of actions) {
+ if (this.isDetected(state)) {
+ hasCookie = true;
+ }
+ if (TrackingProtection.isAllowing(state)) {
+ isTracker = true;
+ }
+ // blocked tells us whether the resource was actually blocked
+ // (which it may not be in case of an exception).
+ if (this.isBlocking(state)) {
+ info.isAllowed = !blocked;
+ }
+ }
+
+ if (!hasCookie) {
+ continue;
+ }
+
+ let isFirstParty = false;
+ try {
+ let uri = Services.io.newURI(origin);
+ isFirstParty = Services.eTLD.getBaseDomain(uri) == firstPartyDomain;
+ } catch (e) {
+ if (
+ e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
+ e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
+ ) {
+ throw e;
+ }
+ }
+
+ if (isFirstParty) {
+ newLog.firstParty.push(info);
+ } else if (isTracker) {
+ newLog.trackers.push(info);
+ } else {
+ newLog.thirdParty.push(info);
+ }
+ }
+
+ return newLog;
+ },
+
+ _createListItem({ origin, isAllowed, exceptionState }) {
+ let listItem = document.createXULElement("hbox");
+ listItem.className = "protections-popup-list-item";
+ // Repeat the origin in the tooltip in case it's too long
+ // and overflows in our panel.
+ listItem.tooltipText = origin;
+
+ let label = document.createXULElement("label");
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ if (
+ (isAllowed && exceptionState == Services.perms.ALLOW_ACTION) ||
+ (!isAllowed && exceptionState == Services.perms.DENY_ACTION)
+ ) {
+ let stateLabel;
+ if (isAllowed) {
+ stateLabel = document.createXULElement("label");
+ stateLabel.value = this.strings.subViewAllowed;
+ stateLabel.className = "protections-popup-list-state-label";
+ listItem.append(stateLabel);
+ listItem.classList.toggle("allowed", true);
+ } else {
+ stateLabel = document.createXULElement("label");
+ stateLabel.value = this.strings.subViewBlocked;
+ stateLabel.className = "protections-popup-list-state-label";
+ listItem.append(stateLabel);
+ }
+
+ let removeException = document.createXULElement("button");
+ removeException.className = "identity-popup-permission-remove-button";
+ removeException.tooltipText = gNavigatorBundle.getFormattedString(
+ "contentBlocking.cookiesView.removeButton.tooltip",
+ [origin]
+ );
+ removeException.addEventListener(
+ "click",
+ () => {
+ this._clearException(origin);
+ stateLabel.remove();
+ removeException.remove();
+ listItem.classList.toggle("allowed", !isAllowed);
+ },
+ { once: true }
+ );
+ listItem.append(removeException);
+ }
+
+ return listItem;
+ },
+};
+
+var SocialTracking = {
+ PREF_STP_TP_ENABLED: "privacy.trackingprotection.socialtracking.enabled",
+ PREF_STP_COOKIE_ENABLED: "privacy.socialtracking.block_cookies.enabled",
+ PREF_COOKIE_BEHAVIOR: "network.cookie.cookieBehavior",
+ reportBreakageLabel: "socialtracking",
+
+ strings: {
+ get subViewBlocked() {
+ delete this.subViewBlocked;
+ return (this.subViewBlocked = gNavigatorBundle.getString(
+ "contentBlocking.fingerprintersView.blocked.label"
+ ));
+ },
+
+ get subViewTitleBlocking() {
+ delete this.subViewTitleBlocking;
+ return (this.subViewTitleBlocking = gNavigatorBundle.getString(
+ "protections.blocking.socialMediaTrackers.title"
+ ));
+ },
+
+ get subViewTitleNotBlocking() {
+ delete this.subViewTitleNotBlocking;
+ return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
+ "protections.notBlocking.socialMediaTrackers.title"
+ ));
+ },
+ },
+
+ init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "socialTrackingProtectionEnabled",
+ this.PREF_STP_TP_ENABLED,
+ false,
+ this.updateCategoryItem.bind(this)
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "rejectTrackingCookies",
+ this.PREF_COOKIE_BEHAVIOR,
+ false,
+ this.updateCategoryItem.bind(this),
+ val =>
+ [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ].includes(val)
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "enabled",
+ this.PREF_STP_COOKIE_ENABLED,
+ false,
+ this.updateCategoryItem.bind(this)
+ );
+ },
+
+ get blockingEnabled() {
+ return (
+ (this.socialTrackingProtectionEnabled || this.rejectTrackingCookies) &&
+ this.enabled
+ );
+ },
+
+ updateCategoryItem() {
+ // Can't get `this.categoryItem` without the popup. Using the popup instead
+ // of `this.categoryItem` to guard access, because the category item getter
+ // can trigger bug 1543537. If there's no popup, we'll be called again the
+ // first time the popup shows.
+ if (!gProtectionsHandler._protectionsPopup) {
+ return;
+ }
+ if (this.enabled) {
+ this.categoryItem.removeAttribute("uidisabled");
+ } else {
+ this.categoryItem.setAttribute("uidisabled", true);
+ }
+ this.categoryItem.classList.toggle("blocked", this.blockingEnabled);
+ },
+
+ isBlocking(state) {
+ let socialtrackingContentBlocked =
+ (state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT) !=
+ 0;
+ let socialtrackingCookieBlocked =
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
+ 0;
+ return socialtrackingCookieBlocked || socialtrackingContentBlocked;
+ },
+
+ isAllowing(state) {
+ if (this.socialTrackingProtectionEnabled) {
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT) !=
+ 0
+ );
+ }
+
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
+ 0
+ );
+ },
+
+ isDetected(state) {
+ return this.isBlocking(state) || this.isAllowing(state);
+ },
+
+ isShimming(state) {
+ return (
+ state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT &&
+ this.isAllowing(state)
+ );
+ },
+
+ get categoryItem() {
+ let item = document.getElementById(
+ "protections-popup-category-socialblock"
+ );
+ if (item) {
+ delete this.categoryItem;
+ this.categoryItem = item;
+ }
+ return item;
+ },
+
+ get subView() {
+ delete this.subView;
+ return (this.subView = document.getElementById(
+ "protections-popup-socialblockView"
+ ));
+ },
+
+ get subViewList() {
+ delete this.subViewList;
+ return (this.subViewList = document.getElementById(
+ "protections-popup-socialblockView-list"
+ ));
+ },
+
+ updateSubView() {
+ let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
+ contentBlockingLog = JSON.parse(contentBlockingLog);
+
+ let fragment = document.createDocumentFragment();
+ for (let [origin, actions] of Object.entries(contentBlockingLog)) {
+ let listItem = this._createListItem(origin, actions);
+ if (listItem) {
+ fragment.appendChild(listItem);
+ }
+ }
+
+ this.subViewList.textContent = "";
+ this.subViewList.append(fragment);
+ this.subView.setAttribute(
+ "title",
+ this.blockingEnabled && !gProtectionsHandler.hasException
+ ? this.strings.subViewTitleBlocking
+ : this.strings.subViewTitleNotBlocking
+ );
+ },
+
+ _createListItem(origin, actions) {
+ let isAllowed = actions.some(
+ ([state]) => this.isAllowing(state) && !this.isShimming(state)
+ );
+ let isDetected =
+ isAllowed || actions.some(([state]) => this.isBlocking(state));
+
+ if (!isDetected) {
+ return null;
+ }
+
+ let listItem = document.createXULElement("hbox");
+ listItem.className = "protections-popup-list-item";
+ listItem.classList.toggle("allowed", isAllowed);
+ // Repeat the host in the tooltip in case it's too long
+ // and overflows in our panel.
+ listItem.tooltipText = origin;
+
+ let label = document.createXULElement("label");
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ return listItem;
+ },
+};
+
+/**
+ * Utility object to handle manipulations of the protections indicators in the UI
+ */
+var gProtectionsHandler = {
+ PREF_REPORT_BREAKAGE_URL: "browser.contentblocking.reportBreakage.url",
+ PREF_CB_CATEGORY: "browser.contentblocking.category",
+
+ _protectionsPopup: null,
+ _initializePopup() {
+ if (!this._protectionsPopup) {
+ let wrapper = document.getElementById("template-protections-popup");
+ this._protectionsPopup = wrapper.content.firstElementChild;
+ wrapper.replaceWith(wrapper.content);
+
+ this.maybeSetMilestoneCounterText();
+
+ for (let blocker of this.blockers) {
+ blocker.updateCategoryItem();
+ }
+
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ document.getElementById(
+ "protections-popup-sendReportView-learn-more"
+ ).href = baseURL + "blocking-breakage";
+ }
+ },
+
+ _hidePopup() {
+ if (this._protectionsPopup) {
+ PanelMultiView.hidePopup(this._protectionsPopup);
+ }
+ },
+
+ // smart getters
+ get iconBox() {
+ delete this.iconBox;
+ return (this.iconBox = document.getElementById(
+ "tracking-protection-icon-box"
+ ));
+ },
+ get animatedIcon() {
+ delete this.animatedIcon;
+ return (this.animatedIcon = document.getElementById(
+ "tracking-protection-icon-animatable-image"
+ ));
+ },
+ get _protectionsIconBox() {
+ delete this._protectionsIconBox;
+ return (this._protectionsIconBox = document.getElementById(
+ "tracking-protection-icon-animatable-box"
+ ));
+ },
+ get _protectionsPopupMultiView() {
+ delete this._protectionsPopupMultiView;
+ return (this._protectionsPopupMultiView = document.getElementById(
+ "protections-popup-multiView"
+ ));
+ },
+ get _protectionsPopupMainView() {
+ delete this._protectionsPopupMainView;
+ return (this._protectionsPopupMainView = document.getElementById(
+ "protections-popup-mainView"
+ ));
+ },
+ get _protectionsPopupMainViewHeaderLabel() {
+ delete this._protectionsPopupMainViewHeaderLabel;
+ return (this._protectionsPopupMainViewHeaderLabel = document.getElementById(
+ "protections-popup-mainView-panel-header-span"
+ ));
+ },
+ get _protectionsPopupTPSwitchBreakageLink() {
+ delete this._protectionsPopupTPSwitchBreakageLink;
+ return (this._protectionsPopupTPSwitchBreakageLink = document.getElementById(
+ "protections-popup-tp-switch-breakage-link"
+ ));
+ },
+ get _protectionsPopupTPSwitchBreakageFixedLink() {
+ delete this._protectionsPopupTPSwitchBreakageFixedLink;
+ return (this._protectionsPopupTPSwitchBreakageFixedLink = document.getElementById(
+ "protections-popup-tp-switch-breakage-fixed-link"
+ ));
+ },
+ get _protectionsPopupTPSwitchSection() {
+ delete this._protectionsPopupTPSwitchSection;
+ return (this._protectionsPopupTPSwitchSection = document.getElementById(
+ "protections-popup-tp-switch-section"
+ ));
+ },
+ get _protectionsPopupTPSwitch() {
+ delete this._protectionsPopupTPSwitch;
+ return (this._protectionsPopupTPSwitch = document.getElementById(
+ "protections-popup-tp-switch"
+ ));
+ },
+ get _protectionsPopupBlockingHeader() {
+ delete this._protectionsPopupBlockingHeader;
+ return (this._protectionsPopupBlockingHeader = document.getElementById(
+ "protections-popup-blocking-section-header"
+ ));
+ },
+ get _protectionsPopupNotBlockingHeader() {
+ delete this._protectionsPopupNotBlockingHeader;
+ return (this._protectionsPopupNotBlockingHeader = document.getElementById(
+ "protections-popup-not-blocking-section-header"
+ ));
+ },
+ get _protectionsPopupNotFoundHeader() {
+ delete this._protectionsPopupNotFoundHeader;
+ return (this._protectionsPopupNotFoundHeader = document.getElementById(
+ "protections-popup-not-found-section-header"
+ ));
+ },
+ get _protectionsPopupSettingsButton() {
+ delete this._protectionsPopupSettingsButton;
+ return (this._protectionsPopupSettingsButton = document.getElementById(
+ "protections-popup-settings-button"
+ ));
+ },
+ get _protectionsPopupFooter() {
+ delete this._protectionsPopupFooter;
+ return (this._protectionsPopupFooter = document.getElementById(
+ "protections-popup-footer"
+ ));
+ },
+ get _protectionsPopupTrackersCounterBox() {
+ delete this._protectionsPopupTrackersCounterBox;
+ return (this._protectionsPopupTrackersCounterBox = document.getElementById(
+ "protections-popup-trackers-blocked-counter-box"
+ ));
+ },
+ get _protectionsPopupTrackersCounterDescription() {
+ delete this._protectionsPopupTrackersCounterDescription;
+ return (this._protectionsPopupTrackersCounterDescription = document.getElementById(
+ "protections-popup-trackers-blocked-counter-description"
+ ));
+ },
+ get _protectionsPopupFooterProtectionTypeLabel() {
+ delete this._protectionsPopupFooterProtectionTypeLabel;
+ return (this._protectionsPopupFooterProtectionTypeLabel = document.getElementById(
+ "protections-popup-footer-protection-type-label"
+ ));
+ },
+ get _protectionsPopupSiteNotWorkingTPSwitch() {
+ delete this._protectionsPopupSiteNotWorkingTPSwitch;
+ return (this._protectionsPopupSiteNotWorkingTPSwitch = document.getElementById(
+ "protections-popup-siteNotWorking-tp-switch"
+ ));
+ },
+ get _protectionsPopupSiteNotWorkingReportError() {
+ delete this._protectionsPopupSiteNotWorkingReportError;
+ return (this._protectionsPopupSiteNotWorkingReportError = document.getElementById(
+ "protections-popup-sendReportView-report-error"
+ ));
+ },
+ get _protectionsPopupSendReportURL() {
+ delete this._protectionsPopupSendReportURL;
+ return (this._protectionsPopupSendReportURL = document.getElementById(
+ "protections-popup-sendReportView-collection-url"
+ ));
+ },
+ get _protectionsPopupSendReportButton() {
+ delete this._protectionsPopupSendReportButton;
+ return (this._protectionsPopupSendReportButton = document.getElementById(
+ "protections-popup-sendReportView-submit"
+ ));
+ },
+ get _trackingProtectionIconTooltipLabel() {
+ delete this._trackingProtectionIconTooltipLabel;
+ return (this._trackingProtectionIconTooltipLabel = document.getElementById(
+ "tracking-protection-icon-tooltip-label"
+ ));
+ },
+ get _trackingProtectionIconContainer() {
+ delete this._trackingProtectionIconContainer;
+ return (this._trackingProtectionIconContainer = document.getElementById(
+ "tracking-protection-icon-container"
+ ));
+ },
+
+ get noTrackersDetectedDescription() {
+ delete this.noTrackersDetectedDescription;
+ return (this.noTrackersDetectedDescription = document.getElementById(
+ "protections-popup-no-trackers-found-description"
+ ));
+ },
+
+ get _protectionsPopupMilestonesText() {
+ delete this._protectionsPopupMilestonesText;
+ return (this._protectionsPopupMilestonesText = document.getElementById(
+ "protections-popup-milestones-text"
+ ));
+ },
+
+ get _notBlockingWhyLink() {
+ delete this._notBlockingWhyLink;
+ return (this._notBlockingWhyLink = document.getElementById(
+ "protections-popup-not-blocking-section-why"
+ ));
+ },
+
+ strings: {
+ get activeTooltipText() {
+ delete this.activeTooltipText;
+ return (this.activeTooltipText = gNavigatorBundle.getString(
+ "trackingProtection.icon.activeTooltip2"
+ ));
+ },
+
+ get disabledTooltipText() {
+ delete this.disabledTooltipText;
+ return (this.disabledTooltipText = gNavigatorBundle.getString(
+ "trackingProtection.icon.disabledTooltip2"
+ ));
+ },
+
+ get noTrackerTooltipText() {
+ delete this.noTrackerTooltipText;
+ return (this.noTrackerTooltipText = gNavigatorBundle.getFormattedString(
+ "trackingProtection.icon.noTrackersDetectedTooltip",
+ [gBrandBundle.GetStringFromName("brandShortName")]
+ ));
+ },
+ },
+
+ // A list of blockers that will be displayed in the categories list
+ // when blockable content is detected. A blocker must be an object
+ // with at least the following two properties:
+ // - enabled: Whether the blocker is currently turned on.
+ // - isDetected(state): Given a content blocking state, whether the blocker has
+ // either allowed or blocked elements.
+ // - categoryItem: The DOM item that represents the entry in the category list.
+ //
+ // It may also contain an init() and uninit() function, which will be called
+ // on gProtectionsHandler.init() and gProtectionsHandler.uninit().
+ // The buttons in the protections panel will appear in the same order as this array.
+ blockers: [
+ SocialTracking,
+ ThirdPartyCookies,
+ TrackingProtection,
+ Fingerprinting,
+ Cryptomining,
+ ],
+
+ init() {
+ this.animatedIcon.addEventListener("animationend", () =>
+ this.iconBox.removeAttribute("animate")
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_protectionsPopupToastTimeout",
+ "browser.protections_panel.toast.timeout",
+ 3000
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestoneListPref",
+ "browser.contentblocking.cfr-milestone.milestones",
+ [],
+ () => this.maybeSetMilestoneCounterText(),
+ val => JSON.parse(val)
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestonePref",
+ "browser.contentblocking.cfr-milestone.milestone-achieved",
+ 0,
+ () => this.maybeSetMilestoneCounterText()
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestoneTimestampPref",
+ "browser.contentblocking.cfr-milestone.milestone-shown-time",
+ 0,
+ null,
+ val => parseInt(val)
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestonesEnabledPref",
+ "browser.contentblocking.cfr-milestone.enabled",
+ false,
+ () => this.maybeSetMilestoneCounterText()
+ );
+
+ for (let blocker of this.blockers) {
+ if (blocker.init) {
+ blocker.init();
+ }
+ }
+
+ // Add an observer to observe that the history has been cleared.
+ Services.obs.addObserver(this, "browser:purge-session-history");
+ },
+
+ uninit() {
+ for (let blocker of this.blockers) {
+ if (blocker.uninit) {
+ blocker.uninit();
+ }
+ }
+
+ Services.obs.removeObserver(this, "browser:purge-session-history");
+ },
+
+ getTrackingProtectionLabel() {
+ const value = Services.prefs.getStringPref(this.PREF_CB_CATEGORY);
+
+ switch (value) {
+ case "strict":
+ return "protections-popup-footer-protection-label-strict";
+ case "custom":
+ return "protections-popup-footer-protection-label-custom";
+ case "standard":
+ /* fall through */
+ default:
+ return "protections-popup-footer-protection-label-standard";
+ }
+ },
+
+ openPreferences(origin) {
+ openPreferences("privacy-trackingprotection", { origin });
+ },
+
+ openProtections(relatedToCurrent = false) {
+ switchToTabHavingURI("about:protections", true, {
+ replaceQueryString: true,
+ relatedToCurrent,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ // Don't show the milestones section anymore.
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.cfr-milestone.milestone-shown-time"
+ );
+ },
+
+ async showTrackersSubview(event) {
+ await TrackingProtection.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-trackersView"
+ );
+ },
+
+ async showSocialblockerSubview(event) {
+ await SocialTracking.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-socialblockView"
+ );
+ },
+
+ async showCookiesSubview(event) {
+ await ThirdPartyCookies.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-cookiesView"
+ );
+ },
+
+ async showFingerprintersSubview(event) {
+ await Fingerprinting.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-fingerprintersView"
+ );
+ },
+
+ async showCryptominersSubview(event) {
+ await Cryptomining.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-cryptominersView"
+ );
+ },
+
+ recordClick(object, value = null, source = "protectionspopup") {
+ Services.telemetry.recordEvent(
+ `security.ui.${source}`,
+ "click",
+ object,
+ value
+ );
+ },
+
+ shieldHistogramAdd(value) {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+ Services.telemetry
+ .getHistogramById("TRACKING_PROTECTION_SHIELD")
+ .add(value);
+ },
+
+ cryptominersHistogramAdd(value) {
+ Services.telemetry
+ .getHistogramById("CRYPTOMINERS_BLOCKED_COUNT")
+ .add(value);
+ },
+
+ fingerprintersHistogramAdd(value) {
+ Services.telemetry
+ .getHistogramById("FINGERPRINTERS_BLOCKED_COUNT")
+ .add(value);
+ },
+
+ handleProtectionsButtonEvent(event) {
+ event.stopPropagation();
+ if (
+ (event.type == "click" && event.button != 0) ||
+ (event.type == "keypress" &&
+ event.charCode != KeyEvent.DOM_VK_SPACE &&
+ event.keyCode != KeyEvent.DOM_VK_RETURN)
+ ) {
+ return; // Left click, space or enter only
+ }
+
+ this.showProtectionsPopup({ event });
+ },
+
+ onPopupShown(event) {
+ if (event.target == this._protectionsPopup) {
+ window.addEventListener("focus", this, true);
+
+ // Add the "open" attribute to the tracking protection icon container
+ // for styling.
+ this._trackingProtectionIconContainer.setAttribute("open", "true");
+
+ // Insert the info message if needed. This will be shown once and then
+ // remain collapsed.
+ ToolbarPanelHub.insertProtectionPanelMessage(event);
+
+ if (!event.target.hasAttribute("toast")) {
+ Services.telemetry.recordEvent(
+ "security.ui.protectionspopup",
+ "open",
+ "protections_popup"
+ );
+ }
+ }
+ },
+
+ onPopupHidden(event) {
+ if (event.target == this._protectionsPopup) {
+ window.removeEventListener("focus", this, true);
+ this._trackingProtectionIconContainer.removeAttribute("open");
+ }
+ },
+
+ onHeaderClicked(event) {
+ // Display the whole protections panel if the toast has been clicked.
+ if (this._protectionsPopup.hasAttribute("toast")) {
+ // Hide the toast first.
+ PanelMultiView.hidePopup(this._protectionsPopup);
+
+ // Open the full protections panel.
+ this.showProtectionsPopup({ event });
+ }
+ },
+
+ async onTrackingProtectionIconHoveredOrFocused() {
+ // We would try to pre-fetch the data whenever the shield icon is hovered or
+ // focused. We check focus event here due to the keyboard navigation.
+ if (this._updatingFooter) {
+ return;
+ }
+ this._updatingFooter = true;
+
+ // Take the popup out of its template.
+ this._initializePopup();
+
+ // Get the tracker count and set it to the counter in the footer.
+ const trackerCount = await TrackingDBService.sumAllEvents();
+ this.setTrackersBlockedCounter(trackerCount);
+
+ // Set tracking protection label
+ const l10nId = this.getTrackingProtectionLabel();
+ const elem = this._protectionsPopupFooterProtectionTypeLabel;
+ document.l10n.setAttributes(elem, l10nId);
+
+ // Try to get the earliest recorded date in case that there was no record
+ // during the initiation but new records come after that.
+ await this.maybeUpdateEarliestRecordedDateTooltip();
+
+ this._updatingFooter = false;
+ },
+
+ // This triggers from top level location changes.
+ onLocationChange() {
+ if (this._showToastAfterRefresh) {
+ this._showToastAfterRefresh = false;
+
+ // We only display the toast if we're still on the same page.
+ if (
+ this._previousURI == gBrowser.currentURI.spec &&
+ this._previousOuterWindowID == gBrowser.selectedBrowser.outerWindowID
+ ) {
+ this.showProtectionsPopup({
+ toast: true,
+ });
+ }
+ }
+
+ // Reset blocking and exception status so that we can send telemetry
+ this.hadShieldState = false;
+
+ // Don't deal with about:, file: etc.
+ if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser)) {
+ // We hide the icon and thus avoid showing the doorhanger, since
+ // the information contained there would mostly be broken and/or
+ // irrelevant anyway.
+ this._trackingProtectionIconContainer.hidden = true;
+ return;
+ }
+ this._trackingProtectionIconContainer.hidden = false;
+
+ // Check whether the user has added an exception for this site.
+ this.hasException = ContentBlockingAllowList.includes(
+ gBrowser.selectedBrowser
+ );
+
+ if (this._protectionsPopup) {
+ this._protectionsPopup.toggleAttribute("hasException", this.hasException);
+ }
+ this.iconBox.toggleAttribute("hasException", this.hasException);
+
+ // Add to telemetry per page load as a baseline measurement.
+ this.fingerprintersHistogramAdd("pageLoad");
+ this.cryptominersHistogramAdd("pageLoad");
+ this.shieldHistogramAdd(0);
+ },
+
+ notifyContentBlockingEvent(event) {
+ // We don't notify observers until the document stops loading, therefore
+ // a merged event can be sent, which gives an opportunity to decide the
+ // priority by the handler.
+ // Content blocking events coming after stopping will not be merged, and are
+ // sent directly.
+ if (!this._isStoppedState || !this.anyDetected) {
+ return;
+ }
+
+ let uri = gBrowser.currentURI;
+ let uriHost = uri.asciiHost ? uri.host : uri.spec;
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ browser: gBrowser.selectedBrowser,
+ host: uriHost,
+ event,
+ },
+ },
+ "SiteProtection:ContentBlockingEvent"
+ );
+ },
+
+ onStateChange(aWebProgress, stateFlags) {
+ if (!aWebProgress.isTopLevel) {
+ return;
+ }
+
+ this._isStoppedState = !!(
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ );
+ this.notifyContentBlockingEvent(
+ gBrowser.selectedBrowser.getContentBlockingEvents()
+ );
+ },
+
+ /**
+ * Update the in-panel UI given a blocking event. Called when the popup
+ * is being shown, or when the popup is open while a new event comes in.
+ */
+ updatePanelForBlockingEvent(event, isShown) {
+ // Update the categories:
+ for (let blocker of this.blockers) {
+ if (blocker.categoryItem.hasAttribute("uidisabled")) {
+ continue;
+ }
+ blocker.categoryItem.classList.toggle(
+ "notFound",
+ !blocker.isDetected(event)
+ );
+ }
+
+ // And the popup attributes:
+ this._protectionsPopup.toggleAttribute("detected", this.anyDetected);
+ this._protectionsPopup.toggleAttribute("blocking", this.anyBlocking);
+ this._protectionsPopup.toggleAttribute("hasException", this.hasException);
+
+ this.noTrackersDetectedDescription.hidden = this.anyDetected;
+
+ if (this.anyDetected) {
+ // Reorder categories if any are in use.
+ this.reorderCategoryItems();
+
+ if (isShown) {
+ // Until we encounter a site that triggers them, category elements might
+ // be invisible when descriptionHeightWorkaround gets called, i.e. they
+ // are omitted from the workaround and the content overflows the panel.
+ // Solution: call it manually here.
+ PanelMultiView.forNode(
+ this._protectionsPopupMainView
+ ).descriptionHeightWorkaround();
+ }
+ }
+ },
+
+ reportBlockingEventTelemetry(event, isSimulated, previousState) {
+ if (!isSimulated) {
+ if (this.hasException && !this.hadShieldState) {
+ this.hadShieldState = true;
+ this.shieldHistogramAdd(1);
+ } else if (
+ !this.hasException &&
+ this.anyBlocking &&
+ !this.hadShieldState
+ ) {
+ this.hadShieldState = true;
+ this.shieldHistogramAdd(2);
+ }
+ }
+
+ // We report up to one instance of fingerprinting and cryptomining
+ // blocking and/or allowing per page load.
+ let fingerprintingBlocking =
+ Fingerprinting.isBlocking(event) &&
+ !Fingerprinting.isBlocking(previousState);
+ let fingerprintingAllowing =
+ Fingerprinting.isAllowing(event) &&
+ !Fingerprinting.isAllowing(previousState);
+ let cryptominingBlocking =
+ Cryptomining.isBlocking(event) && !Cryptomining.isBlocking(previousState);
+ let cryptominingAllowing =
+ Cryptomining.isAllowing(event) && !Cryptomining.isAllowing(previousState);
+
+ if (fingerprintingBlocking) {
+ this.fingerprintersHistogramAdd("blocked");
+ } else if (fingerprintingAllowing) {
+ this.fingerprintersHistogramAdd("allowed");
+ }
+
+ if (cryptominingBlocking) {
+ this.cryptominersHistogramAdd("blocked");
+ } else if (cryptominingAllowing) {
+ this.cryptominersHistogramAdd("allowed");
+ }
+ },
+
+ onContentBlockingEvent(event, webProgress, isSimulated, previousState) {
+ // Don't deal with about:, file: etc.
+ if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser)) {
+ this.iconBox.removeAttribute("animate");
+ this.iconBox.removeAttribute("active");
+ this.iconBox.removeAttribute("hasException");
+ return;
+ }
+
+ // First update all our internal state based on the allowlist and the
+ // different blockers:
+ this.anyDetected = false;
+ this.anyBlocking = false;
+ this._lastEvent = event;
+
+ // Check whether the user has added an exception for this site.
+ this.hasException = ContentBlockingAllowList.includes(
+ gBrowser.selectedBrowser
+ );
+
+ // Update blocker state and find if they detected or blocked anything.
+ for (let blocker of this.blockers) {
+ if (blocker.categoryItem?.hasAttribute("uidisabled")) {
+ continue;
+ }
+ // Store data on whether the blocker is activated for reporting it
+ // using the "report breakage" dialog. Under normal circumstances this
+ // dialog should only be able to open in the currently selected tab
+ // and onSecurityChange runs on tab switch, so we can avoid associating
+ // the data with the document directly.
+ blocker.activated = blocker.isBlocking(event);
+ this.anyDetected = this.anyDetected || blocker.isDetected(event);
+ this.anyBlocking = this.anyBlocking || blocker.activated;
+ }
+
+ this._categoryItemOrderInvalidated = true;
+
+ // Now, update the icon UI:
+
+ // Reset the animation in case the user is switching tabs or if no blockers were detected
+ // (this is most likely happening because the user navigated on to a different site). This
+ // allows us to play it from the start without choppiness next time.
+ if (isSimulated || !this.anyBlocking) {
+ this.iconBox.removeAttribute("animate");
+ // Only play the animation when the shield is not already shown on the page (the visibility
+ // of the shield based on this onSecurityChange be determined afterwards).
+ } else if (this.anyBlocking && !this.iconBox.hasAttribute("active")) {
+ this.iconBox.setAttribute("animate", "true");
+ }
+
+ // We consider the shield state "active" when some kind of blocking activity
+ // occurs on the page. Note that merely allowing the loading of content that
+ // we could have blocked does not trigger the appearance of the shield.
+ // This state will be overriden later if there's an exception set for this site.
+ this.iconBox.toggleAttribute("active", this.anyBlocking);
+ this.iconBox.toggleAttribute("hasException", this.hasException);
+
+ // Update the icon's tooltip:
+ if (this.hasException) {
+ this.showDisabledTooltipForTPIcon();
+ } else if (this.anyBlocking) {
+ this.showActiveTooltipForTPIcon();
+ } else {
+ this.showNoTrackerTooltipForTPIcon();
+ }
+
+ // Update the panel if it's open.
+ let isPanelOpen = ["showing", "open"].includes(
+ this._protectionsPopup?.state
+ );
+ if (isPanelOpen) {
+ this.updatePanelForBlockingEvent(event, true);
+ }
+
+ // Notify other consumers, like CFR.
+ // Don't send a content blocking event to CFR for
+ // tab switches since this will already be done via
+ // onStateChange.
+ if (!isSimulated) {
+ this.notifyContentBlockingEvent(event);
+ }
+
+ // Finally, report telemetry.
+ this.reportBlockingEventTelemetry(event, isSimulated, previousState);
+ },
+
+ // We handle focus here when the panel is shown.
+ handleEvent(event) {
+ let elem = document.activeElement;
+ let position = elem.compareDocumentPosition(this._protectionsPopup);
+
+ if (
+ !(
+ position &
+ (Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_CONTAINED_BY)
+ ) &&
+ !this._protectionsPopup.hasAttribute("noautohide")
+ ) {
+ // Hide the panel when focusing an element that is
+ // neither an ancestor nor descendant unless the panel has
+ // @noautohide (e.g. for a tour).
+ PanelMultiView.hidePopup(this._protectionsPopup);
+ }
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "browser:purge-session-history":
+ // We need to update the earliest recorded date if history has been
+ // cleared.
+ this._hasEarliestRecord = false;
+ this.maybeUpdateEarliestRecordedDateTooltip();
+ break;
+ }
+ },
+
+ /**
+ * Update the popup contents. Only called when the popup has been taken
+ * out of the template and is shown or about to be shown.
+ */
+ refreshProtectionsPopup() {
+ let host = gIdentityHandler.getHostForDisplay();
+
+ // Push the appropriate strings out to the UI.
+ this._protectionsPopupMainViewHeaderLabel.textContent = gNavigatorBundle.getFormattedString(
+ "protections.header",
+ [host]
+ );
+
+ let currentlyEnabled = !this.hasException;
+
+ for (let tpSwitch of [
+ this._protectionsPopupTPSwitch,
+ this._protectionsPopupSiteNotWorkingTPSwitch,
+ ]) {
+ tpSwitch.toggleAttribute("enabled", currentlyEnabled);
+ }
+
+ this._notBlockingWhyLink.setAttribute(
+ "tooltip",
+ currentlyEnabled
+ ? "protections-popup-not-blocking-why-etp-on-tooltip"
+ : "protections-popup-not-blocking-why-etp-off-tooltip"
+ );
+
+ // Toggle the breakage link according to the current enable state.
+ this.toggleBreakageLink();
+
+ // Display a short TP switch section depending on the enable state. We need
+ // to use a separate attribute here since the 'hasException' attribute will
+ // be toggled as well as the TP switch, we cannot rely on that to decide the
+ // height of TP switch section, or it will change when toggling the switch,
+ // which is not desirable for us. So, we need to use a different attribute
+ // here.
+ this._protectionsPopupTPSwitchSection.toggleAttribute(
+ "short",
+ !currentlyEnabled
+ );
+
+ // Give the button an accessible label for screen readers.
+ if (currentlyEnabled) {
+ this._protectionsPopupTPSwitch.setAttribute(
+ "aria-label",
+ gNavigatorBundle.getFormattedString("protections.disableAriaLabel", [
+ host,
+ ])
+ );
+ } else {
+ this._protectionsPopupTPSwitch.setAttribute(
+ "aria-label",
+ gNavigatorBundle.getFormattedString("protections.enableAriaLabel", [
+ host,
+ ])
+ );
+ }
+
+ // Update the tooltip of the blocked tracker counter.
+ this.maybeUpdateEarliestRecordedDateTooltip();
+
+ let today = Date.now();
+ let threeDaysMillis = 72 * 60 * 60 * 1000;
+ let expired = today - this.milestoneTimestampPref > threeDaysMillis;
+
+ if (this._milestoneTextSet && !expired) {
+ this._protectionsPopup.setAttribute("milestone", this.milestonePref);
+ } else {
+ this._protectionsPopup.removeAttribute("milestone");
+ }
+
+ this._protectionsPopup.toggleAttribute("detected", this.anyDetected);
+ this._protectionsPopup.toggleAttribute("blocking", this.anyBlocking);
+ this._protectionsPopup.toggleAttribute("hasException", this.hasException);
+ },
+
+ /*
+ * This function sorts the category items into the Blocked/Allowed/None Detected
+ * sections. It's called immediately in onContentBlockingEvent if the popup
+ * is presently open. Otherwise, the next time the popup is shown.
+ */
+ reorderCategoryItems() {
+ if (!this._categoryItemOrderInvalidated) {
+ return;
+ }
+
+ delete this._categoryItemOrderInvalidated;
+
+ // Hide all the headers to start with.
+ this._protectionsPopupBlockingHeader.hidden = true;
+ this._protectionsPopupNotBlockingHeader.hidden = true;
+ this._protectionsPopupNotFoundHeader.hidden = true;
+
+ for (let { categoryItem } of this.blockers) {
+ if (
+ categoryItem.classList.contains("notFound") ||
+ categoryItem.hasAttribute("uidisabled")
+ ) {
+ // Add the item to the bottom of the list. This will be under
+ // the "None Detected" section.
+ categoryItem.parentNode.insertAdjacentElement(
+ "beforeend",
+ categoryItem
+ );
+ categoryItem.setAttribute("disabled", true);
+ // We have an undetected category, show the header.
+ this._protectionsPopupNotFoundHeader.hidden = false;
+ continue;
+ }
+
+ // Clear the disabled attribute in case we are moving the item out of
+ // "None Detected"
+ categoryItem.removeAttribute("disabled");
+
+ if (categoryItem.classList.contains("blocked") && !this.hasException) {
+ // Add the item just above the "Allowed" section - this will be the
+ // bottom of the "Blocked" section.
+ categoryItem.parentNode.insertBefore(
+ categoryItem,
+ this._protectionsPopupNotBlockingHeader
+ );
+ // We have a blocking category, show the header.
+ this._protectionsPopupBlockingHeader.hidden = false;
+ continue;
+ }
+
+ // Add the item just above the "None Detected" section - this will be the
+ // bottom of the "Allowed" section.
+ categoryItem.parentNode.insertBefore(
+ categoryItem,
+ this._protectionsPopupNotFoundHeader
+ );
+ // We have an allowing category, show the header.
+ this._protectionsPopupNotBlockingHeader.hidden = false;
+ }
+ },
+
+ disableForCurrentPage(shouldReload = true) {
+ ContentBlockingAllowList.add(gBrowser.selectedBrowser);
+ if (shouldReload) {
+ this._hidePopup();
+ BrowserReload();
+ }
+ },
+
+ enableForCurrentPage(shouldReload = true) {
+ ContentBlockingAllowList.remove(gBrowser.selectedBrowser);
+ if (shouldReload) {
+ this._hidePopup();
+ BrowserReload();
+ }
+ },
+
+ async onTPSwitchCommand(event) {
+ // When the switch is clicked, we wait 500ms and then disable/enable
+ // protections, causing the page to refresh, and close the popup.
+ // We need to ensure we don't handle more clicks during the 500ms delay,
+ // so we keep track of state and return early if needed.
+ if (this._TPSwitchCommanding) {
+ return;
+ }
+
+ this._TPSwitchCommanding = true;
+
+ // Toggling the 'hasException' on the protections panel in order to do some
+ // styling after toggling the TP switch.
+ let newExceptionState = this._protectionsPopup.toggleAttribute(
+ "hasException"
+ );
+ for (let tpSwitch of [
+ this._protectionsPopupTPSwitch,
+ this._protectionsPopupSiteNotWorkingTPSwitch,
+ ]) {
+ tpSwitch.toggleAttribute("enabled", !newExceptionState);
+ }
+
+ // Toggle the breakage link if needed.
+ this.toggleBreakageLink();
+
+ // Change the tooltip of the tracking protection icon.
+ if (newExceptionState) {
+ this.showDisabledTooltipForTPIcon();
+ } else {
+ this.showNoTrackerTooltipForTPIcon();
+ }
+
+ // Change the state of the tracking protection icon.
+ this.iconBox.toggleAttribute("hasException", newExceptionState);
+
+ // Indicating that we need to show a toast after refreshing the page.
+ // And caching the current URI and window ID in order to only show the mini
+ // panel if it's still on the same page.
+ this._showToastAfterRefresh = true;
+ this._previousURI = gBrowser.currentURI.spec;
+ this._previousOuterWindowID = gBrowser.selectedBrowser.outerWindowID;
+
+ if (newExceptionState) {
+ this.disableForCurrentPage(false);
+ this.recordClick("etp_toggle_off");
+ } else {
+ this.enableForCurrentPage(false);
+ this.recordClick("etp_toggle_on");
+ }
+
+ // We need to flush the TP state change immediately without waiting the
+ // 500ms delay if the Tab get switched out.
+ let targetTab = gBrowser.selectedTab;
+ let onTabSelectHandler;
+ let tabSelectPromise = new Promise(resolve => {
+ onTabSelectHandler = () => resolve();
+ gBrowser.tabContainer.addEventListener("TabSelect", onTabSelectHandler);
+ });
+ let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
+
+ await Promise.race([tabSelectPromise, timeoutPromise]);
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelectHandler);
+ PanelMultiView.hidePopup(this._protectionsPopup);
+ gBrowser.reloadTab(targetTab);
+
+ delete this._TPSwitchCommanding;
+ },
+
+ setTrackersBlockedCounter(trackerCount) {
+ let forms = gNavigatorBundle.getString(
+ "protections.footer.blockedTrackerCounter.description"
+ );
+ this._protectionsPopupTrackersCounterDescription.textContent = PluralForm.get(
+ trackerCount,
+ forms
+ ).replace(
+ "#1",
+ trackerCount.toLocaleString(Services.locale.appLocalesAsBCP47)
+ );
+
+ // Show the counter if the number of tracker is not zero.
+ this._protectionsPopupTrackersCounterBox.toggleAttribute(
+ "showing",
+ trackerCount != 0
+ );
+ },
+
+ // Whenever one of the milestone prefs are changed, we attempt to update
+ // the milestone section string. This requires us to fetch the earliest
+ // recorded date from the Tracking DB, hence this process is async.
+ // When completed, we set _milestoneSetText to signal that the section
+ // is populated and ready to be shown - which happens next time we call
+ // refreshProtectionsPopup.
+ _milestoneTextSet: false,
+ async maybeSetMilestoneCounterText() {
+ if (!this._protectionsPopup) {
+ return;
+ }
+ let trackerCount = this.milestonePref;
+ if (
+ !this.milestonesEnabledPref ||
+ !trackerCount ||
+ !this.milestoneListPref.includes(trackerCount)
+ ) {
+ this._milestoneTextSet = false;
+ return;
+ }
+
+ let date = await TrackingDBService.getEarliestRecordedDate();
+ let dateLocaleStr = new Date(date).toLocaleDateString("default", {
+ month: "long",
+ year: "numeric",
+ });
+
+ let desc = PluralForm.get(
+ trackerCount,
+ gNavigatorBundle.getString("protections.milestone.description")
+ );
+
+ this._protectionsPopupMilestonesText.textContent = desc
+ .replace("#1", gBrandBundle.GetStringFromName("brandShortName"))
+ .replace(
+ "#2",
+ trackerCount.toLocaleString(Services.locale.appLocalesAsBCP47)
+ )
+ .replace("#3", dateLocaleStr);
+
+ this._milestoneTextSet = true;
+ },
+
+ showDisabledTooltipForTPIcon() {
+ this._trackingProtectionIconTooltipLabel.textContent = this.strings.disabledTooltipText;
+ this._trackingProtectionIconContainer.setAttribute(
+ "aria-label",
+ this.strings.disabledTooltipText
+ );
+ },
+
+ showActiveTooltipForTPIcon() {
+ this._trackingProtectionIconTooltipLabel.textContent = this.strings.activeTooltipText;
+ this._trackingProtectionIconContainer.setAttribute(
+ "aria-label",
+ this.strings.activeTooltipText
+ );
+ },
+
+ showNoTrackerTooltipForTPIcon() {
+ this._trackingProtectionIconTooltipLabel.textContent = this.strings.noTrackerTooltipText;
+ this._trackingProtectionIconContainer.setAttribute(
+ "aria-label",
+ this.strings.noTrackerTooltipText
+ );
+ },
+
+ /**
+ * Showing the protections popup.
+ *
+ * @param {Object} options
+ * The object could have two properties.
+ * event:
+ * The event triggers the protections popup to be opened.
+ * toast:
+ * A boolean to indicate if we need to open the protections
+ * popup as a toast. A toast only has a header section and
+ * will be hidden after a certain amount of time.
+ */
+ showProtectionsPopup(options = {}) {
+ const { event, toast } = options;
+
+ this._initializePopup();
+
+ // Ensure we've updated category state based on the last blocking event:
+ if (this.hasOwnProperty("_lastEvent")) {
+ this.updatePanelForBlockingEvent(this._lastEvent);
+ delete this._lastEvent;
+ }
+
+ // We need to clear the toast timer if it exists before showing the
+ // protections popup.
+ if (this._toastPanelTimer) {
+ clearTimeout(this._toastPanelTimer);
+ delete this._toastPanelTimer;
+ }
+
+ this._protectionsPopup.toggleAttribute("toast", !!toast);
+ if (!toast) {
+ // Refresh strings if we want to open it as a standard protections popup.
+ this.refreshProtectionsPopup();
+ }
+
+ if (toast) {
+ this._protectionsPopup.addEventListener(
+ "popupshown",
+ () => {
+ this._toastPanelTimer = setTimeout(() => {
+ PanelMultiView.hidePopup(this._protectionsPopup);
+ delete this._toastPanelTimer;
+ }, this._protectionsPopupToastTimeout);
+ },
+ { once: true }
+ );
+ }
+
+ // Add the "open" attribute to the tracking protection icon container
+ // for styling.
+ this._trackingProtectionIconContainer.setAttribute("open", "true");
+
+ // Check the panel state of other panels. Hide them if needed.
+ let openPanels = Array.from(document.querySelectorAll("panel[openpanel]"));
+ for (let panel of openPanels) {
+ PanelMultiView.hidePopup(panel);
+ }
+
+ // Now open the popup, anchored off the primary chrome element
+ PanelMultiView.openPopup(
+ this._protectionsPopup,
+ this._trackingProtectionIconContainer,
+ {
+ position: "bottomcenter topleft",
+ triggerEvent: event,
+ }
+ ).catch(Cu.reportError);
+ },
+
+ showSiteNotWorkingView() {
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-siteNotWorkingView"
+ );
+ },
+
+ showSendReportView() {
+ // Save this URI to make sure that the user really only submits the location
+ // they see in the report breakage dialog.
+ this.reportURI = gBrowser.currentURI;
+ let urlWithoutQuery = this.reportURI.asciiSpec.replace(
+ "?" + this.reportURI.query,
+ ""
+ );
+ let commentsTextarea = document.getElementById(
+ "protections-popup-sendReportView-collection-comments"
+ );
+ commentsTextarea.value = "";
+ this._protectionsPopupSendReportURL.value = urlWithoutQuery;
+ this._protectionsPopupSiteNotWorkingReportError.hidden = true;
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-sendReportView"
+ );
+ },
+
+ toggleBreakageLink() {
+ // The breakage link will only be shown if tracking protection is enabled
+ // for the site and the TP toggle state is on. And we won't show the
+ // link as toggling TP switch to On from Off. In order to do so, we need to
+ // know the previous TP state. We check the ContentBlockingAllowList instead
+ // of 'hasException' attribute of the protection popup for the previous
+ // since the 'hasException' will also be toggled as well as toggling the TP
+ // switch. We won't be able to know the previous TP state through the
+ // 'hasException' attribute. So we fallback to check the
+ // ContentBlockingAllowList here.
+ this._protectionsPopupTPSwitchBreakageLink.hidden =
+ ContentBlockingAllowList.includes(gBrowser.selectedBrowser) ||
+ !this.anyBlocking ||
+ !this._protectionsPopupTPSwitch.hasAttribute("enabled");
+ // The "Site Fixed?" link behaves similarly but for the opposite state.
+ this._protectionsPopupTPSwitchBreakageFixedLink.hidden =
+ !ContentBlockingAllowList.includes(gBrowser.selectedBrowser) ||
+ this._protectionsPopupTPSwitch.hasAttribute("enabled");
+ },
+
+ submitBreakageReport(uri) {
+ let reportEndpoint = Services.prefs.getStringPref(
+ this.PREF_REPORT_BREAKAGE_URL
+ );
+ if (!reportEndpoint) {
+ return;
+ }
+
+ let commentsTextarea = document.getElementById(
+ "protections-popup-sendReportView-collection-comments"
+ );
+
+ let formData = new FormData();
+ formData.set("title", uri.host);
+
+ // Leave the ? at the end of the URL to signify that this URL had its query stripped.
+ let urlWithoutQuery = uri.asciiSpec.replace(uri.query, "");
+ let body = `Full URL: ${urlWithoutQuery}\n`;
+ body += `userAgent: ${navigator.userAgent}\n`;
+
+ body += "\n**Preferences**\n";
+ body += `${
+ TrackingProtection.PREF_ENABLED_GLOBALLY
+ }: ${Services.prefs.getBoolPref(
+ TrackingProtection.PREF_ENABLED_GLOBALLY
+ )}\n`;
+ body += `${
+ TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS
+ }: ${Services.prefs.getBoolPref(
+ TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS
+ )}\n`;
+ body += `urlclassifier.trackingTable: ${Services.prefs.getStringPref(
+ "urlclassifier.trackingTable"
+ )}\n`;
+ body += `network.http.referer.defaultPolicy: ${Services.prefs.getIntPref(
+ "network.http.referer.defaultPolicy"
+ )}\n`;
+ body += `network.http.referer.defaultPolicy.pbmode: ${Services.prefs.getIntPref(
+ "network.http.referer.defaultPolicy.pbmode"
+ )}\n`;
+ body += `${ThirdPartyCookies.PREF_ENABLED}: ${Services.prefs.getIntPref(
+ ThirdPartyCookies.PREF_ENABLED
+ )}\n`;
+ body += `network.cookie.lifetimePolicy: ${Services.prefs.getIntPref(
+ "network.cookie.lifetimePolicy"
+ )}\n`;
+ body += `privacy.annotate_channels.strict_list.enabled: ${Services.prefs.getBoolPref(
+ "privacy.annotate_channels.strict_list.enabled"
+ )}\n`;
+ body += `privacy.restrict3rdpartystorage.expiration: ${Services.prefs.getIntPref(
+ "privacy.restrict3rdpartystorage.expiration"
+ )}\n`;
+ body += `${Fingerprinting.PREF_ENABLED}: ${Services.prefs.getBoolPref(
+ Fingerprinting.PREF_ENABLED
+ )}\n`;
+ body += `${Cryptomining.PREF_ENABLED}: ${Services.prefs.getBoolPref(
+ Cryptomining.PREF_ENABLED
+ )}\n`;
+ body += `\nhasException: ${this.hasException}\n`;
+
+ body += "\n**Comments**\n" + commentsTextarea.value;
+
+ formData.set("body", body);
+
+ let activatedBlockers = [];
+ for (let blocker of this.blockers) {
+ if (blocker.activated) {
+ activatedBlockers.push(blocker.reportBreakageLabel);
+ }
+ }
+
+ formData.set("labels", activatedBlockers.join(","));
+
+ this._protectionsPopupSendReportButton.disabled = true;
+
+ fetch(reportEndpoint, {
+ method: "POST",
+ credentials: "omit",
+ body: formData,
+ })
+ .then(response => {
+ this._protectionsPopupSendReportButton.disabled = false;
+ if (!response.ok) {
+ Cu.reportError(
+ `Content Blocking report to ${reportEndpoint} failed with status ${response.status}`
+ );
+ this._protectionsPopupSiteNotWorkingReportError.hidden = false;
+ } else {
+ this._protectionsPopup.hidePopup();
+ ConfirmationHint.show(this.iconBox, "breakageReport");
+ }
+ })
+ .catch(Cu.reportError);
+ },
+
+ onSendReportClicked() {
+ this.submitBreakageReport(this.reportURI);
+ },
+
+ async maybeUpdateEarliestRecordedDateTooltip() {
+ // If we've already updated or the popup isn't in the DOM yet, don't bother
+ // doing this:
+ if (this._hasEarliestRecord || !this._protectionsPopup) {
+ return;
+ }
+
+ let date = await TrackingDBService.getEarliestRecordedDate();
+
+ // If there is no record for any blocked tracker, we don't have to do anything
+ // since the tracker counter won't be shown.
+ if (!date) {
+ return;
+ }
+ this._hasEarliestRecord = true;
+
+ const dateLocaleStr = new Date(date).toLocaleDateString("default", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ });
+
+ const tooltipStr = gNavigatorBundle.getFormattedString(
+ "protections.footer.blockedTrackerCounter.tooltip",
+ [dateLocaleStr]
+ );
+
+ this._protectionsPopupTrackersCounterDescription.setAttribute(
+ "tooltiptext",
+ tooltipStr
+ );
+ },
+};
diff --git a/browser/base/content/browser-sync.js b/browser/base/content/browser-sync.js
new file mode 100644
index 0000000000..3f5879fea8
--- /dev/null
+++ b/browser/base/content/browser-sync.js
@@ -0,0 +1,1609 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "FxAccounts",
+ "resource://gre/modules/FxAccounts.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "EnsureFxAccountsWebChannel",
+ "resource://gre/modules/FxAccountsWebChannel.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Weave",
+ "resource://services-sync/main.js"
+);
+
+const MIN_STATUS_ANIMATION_DURATION = 1600;
+
+var gSync = {
+ _initialized: false,
+ // The last sync start time. Used to calculate the leftover animation time
+ // once syncing completes (bug 1239042).
+ _syncStartTime: 0,
+ _syncAnimationTimer: 0,
+ _obs: ["weave:engine:sync:finish", "quit-application", UIState.ON_UPDATE],
+
+ get log() {
+ if (!this._log) {
+ const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+ let syncLog = Log.repository.getLogger("Sync.Browser");
+ syncLog.manageLevelFromPref("services.sync.log.logger.browser");
+ this._log = syncLog;
+ }
+ return this._log;
+ },
+
+ get fxaStrings() {
+ delete this.fxaStrings;
+ return (this.fxaStrings = Services.strings.createBundle(
+ "chrome://browser/locale/accounts.properties"
+ ));
+ },
+
+ get syncStrings() {
+ delete this.syncStrings;
+ // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
+ // but for now just make it work
+ return (this.syncStrings = Services.strings.createBundle(
+ "chrome://weave/locale/sync.properties"
+ ));
+ },
+
+ // Returns true if FxA is configured, but the send tab targets list isn't
+ // ready yet.
+ get sendTabConfiguredAndLoading() {
+ return (
+ UIState.get().status == UIState.STATUS_SIGNED_IN &&
+ !fxAccounts.device.recentDeviceList
+ );
+ },
+
+ get isSignedIn() {
+ return UIState.get().status == UIState.STATUS_SIGNED_IN;
+ },
+
+ getSendTabTargets() {
+ // If sync is not enabled, then there's no point looking for sync clients.
+ // If sync is simply not ready or hasn't yet synced the clients engine, we
+ // just assume the fxa device doesn't have a sync record - in practice,
+ // that just means we don't attempt to fall back to the "old" sendtab should
+ // "new" sendtab fail.
+ // We should just kill "old" sendtab now all our mobile browsers support
+ // "new".
+ let getClientRecord = () => undefined;
+ if (UIState.get().syncEnabled && Weave.Service.clientsEngine) {
+ getClientRecord = id =>
+ Weave.Service.clientsEngine.getClientByFxaDeviceId(id);
+ }
+ let targets = [];
+ if (!fxAccounts.device.recentDeviceList) {
+ return targets;
+ }
+ for (let d of fxAccounts.device.recentDeviceList) {
+ if (d.isCurrentDevice) {
+ continue;
+ }
+
+ let clientRecord = getClientRecord(d.id);
+ if (clientRecord || fxAccounts.commands.sendTab.isDeviceCompatible(d)) {
+ targets.push({
+ clientRecord,
+ ...d,
+ });
+ }
+ }
+ return targets.sort((a, b) => a.name.localeCompare(b.name));
+ },
+
+ _generateNodeGetters() {
+ for (let k of ["Status", "Avatar", "Label"]) {
+ let prop = "appMenu" + k;
+ let suffix = k.toLowerCase();
+ delete this[prop];
+ this.__defineGetter__(prop, function() {
+ delete this[prop];
+ return (this[prop] = document.getElementById("appMenu-fxa-" + suffix));
+ });
+ }
+ },
+
+ _definePrefGetters() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "UNSENDABLE_URL_REGEXP",
+ "services.sync.engine.tabs.filteredUrls",
+ null,
+ null,
+ rx => {
+ try {
+ return new RegExp(rx, "i");
+ } catch (e) {
+ Cu.reportError(
+ `Failed to build url filter regexp for send tab: ${e}`
+ );
+ return null;
+ }
+ }
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "FXA_ENABLED",
+ "identity.fxaccounts.enabled"
+ );
+ },
+
+ maybeUpdateUIState() {
+ // Update the UI.
+ if (UIState.isReady()) {
+ const state = UIState.get();
+ // If we are not configured, the UI is already in the right state when
+ // we open the window. We can avoid a repaint.
+ if (state.status != UIState.STATUS_NOT_CONFIGURED) {
+ this.updateAllUI(state);
+ }
+ }
+ },
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+
+ this._definePrefGetters();
+
+ if (!this.FXA_ENABLED) {
+ this.onFxaDisabled();
+ return;
+ }
+
+ MozXULElement.insertFTLIfNeeded("browser/sync.ftl");
+
+ this._generateNodeGetters();
+
+ // Label for the sync buttons.
+ const appMenuLabel = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-label"
+ );
+ if (!appMenuLabel) {
+ // We are in a window without our elements - just abort now, without
+ // setting this._initialized, so we don't attempt to remove observers.
+ return;
+ }
+ // We start with every menuitem hidden (except for the "setup sync" state),
+ // so that we don't need to init the sync UI on windows like pageInfo.xhtml
+ // (see bug 1384856).
+ // maybeUpdateUIState() also optimizes for this - if we should be in the
+ // "setup sync" state, that function assumes we are already in it and
+ // doesn't re-initialize the UI elements.
+ document.getElementById("sync-setup").hidden = false;
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-remotetabs-setupsync"
+ ).hidden = false;
+
+ for (let topic of this._obs) {
+ Services.obs.addObserver(this, topic, true);
+ }
+
+ this.maybeUpdateUIState();
+
+ EnsureFxAccountsWebChannel();
+
+ this._initialized = true;
+ },
+
+ uninit() {
+ if (!this._initialized) {
+ return;
+ }
+
+ for (let topic of this._obs) {
+ Services.obs.removeObserver(this, topic);
+ }
+
+ this._initialized = false;
+ },
+
+ observe(subject, topic, data) {
+ if (!this._initialized) {
+ Cu.reportError("browser-sync observer called after unload: " + topic);
+ return;
+ }
+ switch (topic) {
+ case UIState.ON_UPDATE:
+ const state = UIState.get();
+ this.updateAllUI(state);
+ break;
+ case "quit-application":
+ // Stop the animation timer on shutdown, since we can't update the UI
+ // after this.
+ clearTimeout(this._syncAnimationTimer);
+ break;
+ case "weave:engine:sync:finish":
+ if (data != "clients") {
+ return;
+ }
+ this.onClientsSynced();
+ this.updateFxAPanel(UIState.get());
+ break;
+ }
+ },
+
+ updateAllUI(state) {
+ this.updatePanelPopup(state);
+ this.updateState(state);
+ this.updateSyncButtonsTooltip(state);
+ this.updateSyncStatus(state);
+ this.updateFxAPanel(state);
+ // Ensure we have something in the device list in the background.
+ this.ensureFxaDevices();
+ },
+
+ // Ensure we have *something* in `fxAccounts.device.recentDeviceList` as some
+ // of our UI logic depends on it not being null. When FxA is notified of a
+ // device change it will auto refresh `recentDeviceList`, and all UI which
+ // shows the device list will start with `recentDeviceList`, but should also
+ // force a refresh, both of which should mean in the worst-case, the UI is up
+ // to date after a very short delay.
+ async ensureFxaDevices(options) {
+ if (UIState.get().status != UIState.STATUS_SIGNED_IN) {
+ console.info("Skipping device list refresh; not signed in");
+ return;
+ }
+ if (!fxAccounts.device.recentDeviceList) {
+ if (await this.refreshFxaDevices()) {
+ // Assuming we made the call successfully it should be impossible to end
+ // up with a falsey recentDeviceList, so make noise if that's false.
+ if (!fxAccounts.device.recentDeviceList) {
+ console.warn("Refreshing device list didn't find any devices.");
+ }
+ }
+ }
+ },
+
+ // Force a refresh of the fxa device list. Note that while it's theoretically
+ // OK to call `fxAccounts.device.refreshDeviceList` multiple times concurrently
+ // and regularly, this call tells it to avoid those protections, so will always
+ // hit the FxA servers - therefore, you should be very careful how often you
+ // call this.
+ // Returns Promise<bool> to indicate whether a refresh was actually done.
+ async refreshFxaDevices() {
+ if (UIState.get().status != UIState.STATUS_SIGNED_IN) {
+ console.info("Skipping device list refresh; not signed in");
+ return false;
+ }
+ try {
+ // Do the actual refresh telling it to avoid the "flooding" protections.
+ await fxAccounts.device.refreshDeviceList({ ignoreCached: true });
+ return true;
+ } catch (e) {
+ this.log.error("Refreshing device list failed.", e);
+ return false;
+ }
+ },
+
+ updateSendToDeviceTitle() {
+ let string = gBrowserBundle.GetStringFromName("sendTabsToDevice.label");
+ let title = PluralForm.get(1, string).replace("#1", 1);
+ if (gBrowser.selectedTab.multiselected) {
+ let tabCount = gBrowser.selectedTabs.length;
+ title = PluralForm.get(tabCount, string).replace("#1", tabCount);
+ }
+
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-button"
+ ).setAttribute("label", title);
+ },
+
+ showSendToDeviceView(anchor) {
+ PanelUI.showSubView("PanelUI-sendTabToDevice", anchor);
+ let panelViewNode = document.getElementById("PanelUI-sendTabToDevice");
+ this.populateSendTabToDevicesView(panelViewNode);
+ },
+
+ showSendToDeviceViewFromFxaMenu(anchor) {
+ const { status } = UIState.get();
+ if (status === UIState.STATUS_NOT_CONFIGURED) {
+ PanelUI.showSubView("PanelUI-fxa-menu-sendtab-not-configured", anchor);
+ return;
+ }
+
+ const targets = this.sendTabConfiguredAndLoading
+ ? []
+ : this.getSendTabTargets();
+ if (!targets.length) {
+ PanelUI.showSubView("PanelUI-fxa-menu-sendtab-no-devices", anchor);
+ return;
+ }
+
+ this.showSendToDeviceView(anchor);
+ this.emitFxaToolbarTelemetry("send_tab", anchor);
+ },
+
+ showRemoteTabsFromFxaMenu(panel) {
+ PanelUI.showSubView("PanelUI-remotetabs", panel);
+ this.emitFxaToolbarTelemetry("sync_tabs", panel);
+ },
+
+ showSidebarFromFxaMenu(panel) {
+ SidebarUI.toggle("viewTabsSidebar");
+ this.emitFxaToolbarTelemetry("sync_tabs_sidebar", panel);
+ },
+
+ populateSendTabToDevicesView(panelViewNode, reloadDevices = true) {
+ let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+ let panelNode = panelViewNode.closest("panel");
+ let browser = gBrowser.selectedBrowser;
+ let url = browser.currentURI.spec;
+ let title = browser.contentTitle;
+ let multiselected = gBrowser.selectedTab.multiselected;
+
+ // This is on top because it also clears the device list between state
+ // changes.
+ this.populateSendTabToDevicesMenu(
+ bodyNode,
+ url,
+ title,
+ multiselected,
+ (clientId, name, clientType, lastModified) => {
+ if (!name) {
+ return document.createXULElement("toolbarseparator");
+ }
+ let item = document.createXULElement("toolbarbutton");
+ item.classList.add("pageAction-sendToDevice-device", "subviewbutton");
+ if (clientId) {
+ item.classList.add("subviewbutton-iconic");
+ if (lastModified) {
+ item.setAttribute(
+ "tooltiptext",
+ gSync.formatLastSyncDate(lastModified)
+ );
+ }
+ }
+
+ item.addEventListener("command", event => {
+ if (panelNode) {
+ PanelMultiView.hidePopup(panelNode);
+ }
+ });
+ return item;
+ }
+ );
+
+ bodyNode.removeAttribute("state");
+ // If the app just started, we won't have fetched the device list yet. Sync
+ // does this automatically ~10 sec after startup, but there's no trigger for
+ // this if we're signed in to FxA, but not Sync.
+ if (gSync.sendTabConfiguredAndLoading) {
+ bodyNode.setAttribute("state", "notready");
+ }
+ if (reloadDevices) {
+ // We will only pick up new Fennec clients if we sync the clients engine,
+ // but all other send-tab targets can be identified purely from the fxa
+ // device list. Syncing the clients engine doesn't force a refresh of the
+ // fxa list, and it seems overkill to force *both* a clients engine sync
+ // and an fxa device list refresh, especially given (a) the clients engine
+ // will sync by itself every 10 minutes and (b) Fennec is (at time of
+ // writing) about to be replaced by Fenix.
+ // So we suck up the fact that new Fennec clients may not appear for 10
+ // minutes and don't bother syncing the clients engine.
+
+ // Force a refresh of the fxa device list in case the user connected a new
+ // device, and is waiting for it to show up.
+ this.refreshFxaDevices().then(_ => {
+ if (!window.closed) {
+ this.populateSendTabToDevicesView(panelViewNode, false);
+ }
+ });
+ }
+ },
+
+ toggleAccountPanel(
+ viewId,
+ anchor = document.getElementById("fxa-toolbar-menu-button"),
+ aEvent
+ ) {
+ // Don't show the panel if the window is in customization mode.
+ if (document.documentElement.hasAttribute("customizing")) {
+ return;
+ }
+
+ if (
+ (aEvent.type == "mousedown" && aEvent.button != 0) ||
+ (aEvent.type == "keypress" &&
+ aEvent.charCode != KeyEvent.DOM_VK_SPACE &&
+ aEvent.keyCode != KeyEvent.DOM_VK_RETURN)
+ ) {
+ return;
+ }
+
+ if (!gFxaToolbarAccessed) {
+ Services.prefs.setBoolPref("identity.fxaccounts.toolbar.accessed", true);
+ }
+
+ this.enableSendTabIfValidTab();
+
+ if (anchor.getAttribute("open") == "true") {
+ PanelUI.hide();
+ } else {
+ this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
+ PanelUI.showSubView(viewId, anchor, aEvent);
+ }
+ },
+
+ updateFxAPanel(state = {}) {
+ const mainWindowEl = document.documentElement;
+
+ // The Firefox Account toolbar currently handles 3 different states for
+ // users. The default `not_configured` state shows an empty avatar, `unverified`
+ // state shows an avatar with an email icon, `login-failed` state shows an avatar
+ // with a danger icon and the `verified` state will show the users
+ // custom profile image or a filled avatar.
+ let stateValue = "not_configured";
+
+ const menuHeaderTitleEl = PanelMultiView.getViewNode(
+ document,
+ "fxa-menu-header-title"
+ );
+ const menuHeaderDescriptionEl = PanelMultiView.getViewNode(
+ document,
+ "fxa-menu-header-description"
+ );
+
+ const cadButtonEl = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-connect-device-button"
+ );
+
+ const syncSetupButtonEl = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-setup-sync-button"
+ );
+
+ const syncPrefsButtonEl = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sync-prefs-button"
+ );
+
+ const syncNowButtonEl = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-syncnow-button"
+ );
+ const fxaMenuPanel = PanelMultiView.getViewNode(document, "PanelUI-fxa");
+
+ const fxaMenuAccountButtonEl = PanelMultiView.getViewNode(
+ document,
+ "fxa-manage-account-button"
+ );
+
+ let headerTitle = menuHeaderTitleEl.getAttribute("defaultLabel");
+ let headerDescription = menuHeaderDescriptionEl.getAttribute(
+ "defaultLabel"
+ );
+
+ const appMenuFxAButtonEl = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-label"
+ );
+
+ let panelTitle = this.fxaStrings.GetStringFromName("account.title");
+
+ fxaMenuPanel.removeAttribute("title");
+ cadButtonEl.setAttribute("disabled", true);
+ syncNowButtonEl.setAttribute("hidden", true);
+ fxaMenuAccountButtonEl.classList.remove("subviewbutton-nav");
+ fxaMenuAccountButtonEl.removeAttribute("closemenu");
+ syncPrefsButtonEl.setAttribute("hidden", true);
+ syncSetupButtonEl.removeAttribute("hidden");
+
+ if (state.status === UIState.STATUS_NOT_CONFIGURED) {
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ } else if (state.status === UIState.STATUS_LOGIN_FAILED) {
+ stateValue = "login-failed";
+ headerTitle = this.fxaStrings.GetStringFromName("account.reconnectToFxA");
+ headerDescription = state.email;
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ } else if (state.status === UIState.STATUS_NOT_VERIFIED) {
+ stateValue = "unverified";
+ headerTitle = this.fxaStrings.GetStringFromName(
+ "account.finishAccountSetup"
+ );
+ headerDescription = state.email;
+ } else if (state.status === UIState.STATUS_SIGNED_IN) {
+ stateValue = "signedin";
+ if (state.avatarURL && !state.avatarIsDefault) {
+ // The user has specified a custom avatar, attempt to load the image on all the menu buttons.
+ const bgImage = `url("${state.avatarURL}")`;
+ let img = new Image();
+ img.onload = () => {
+ // If the image has successfully loaded, update the menu buttons else
+ // we will use the default avatar image.
+ mainWindowEl.style.setProperty("--avatar-image-url", bgImage);
+ };
+ img.onerror = () => {
+ // If the image failed to load, remove the property and default
+ // to standard avatar.
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ };
+ img.src = state.avatarURL;
+ } else {
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ }
+
+ cadButtonEl.removeAttribute("disabled");
+
+ if (state.syncEnabled) {
+ syncNowButtonEl.removeAttribute("hidden");
+ syncPrefsButtonEl.removeAttribute("hidden");
+ syncSetupButtonEl.setAttribute("hidden", true);
+ }
+
+ fxaMenuAccountButtonEl.classList.add("subviewbutton-nav");
+ fxaMenuAccountButtonEl.setAttribute("closemenu", "none");
+
+ headerTitle = state.email;
+ headerDescription = this.fxaStrings.GetStringFromName(
+ "account.accountSettings"
+ );
+
+ panelTitle = state.displayName ? state.displayName : panelTitle;
+ }
+ mainWindowEl.setAttribute("fxastatus", stateValue);
+
+ menuHeaderTitleEl.value = headerTitle;
+ menuHeaderDescriptionEl.value = headerDescription;
+ appMenuFxAButtonEl.setAttribute("label", headerTitle);
+
+ fxaMenuPanel.setAttribute("title", panelTitle);
+ },
+
+ enableSendTabIfValidTab() {
+ // All tabs selected must be sendable for the Send Tab button to be enabled
+ // on the FxA menu.
+ let canSendAllURIs = gBrowser.selectedTabs.every(t =>
+ this.isSendableURI(t.linkedBrowser.currentURI.spec)
+ );
+
+ if (canSendAllURIs) {
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-button"
+ ).removeAttribute("disabled");
+ } else {
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-button"
+ ).setAttribute("disabled", true);
+ }
+ },
+
+ emitFxaToolbarTelemetry(type, panel) {
+ if (UIState.isReady() && panel) {
+ const state = UIState.get();
+ const hasAvatar = state.avatarURL && !state.avatarIsDefault;
+ let extraOptions = {
+ fxa_status: state.status,
+ fxa_avatar: hasAvatar ? "true" : "false",
+ };
+
+ // When the fxa avatar panel is within the Firefox app menu,
+ // we emit different telemetry.
+ let eventName = "fxa_avatar_menu";
+ if (this.isPanelInsideAppMenu(panel)) {
+ eventName = "fxa_app_menu";
+ }
+
+ Services.telemetry.recordEvent(
+ eventName,
+ "click",
+ type,
+ null,
+ extraOptions
+ );
+ }
+ },
+
+ isPanelInsideAppMenu(panel = undefined) {
+ const appMenuPanel = document.getElementById("appMenu-popup");
+ if (panel && appMenuPanel.contains(panel)) {
+ return true;
+ }
+ return false;
+ },
+
+ updatePanelPopup(state) {
+ const appMenuStatus = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-status"
+ );
+ const appMenuLabel = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-label"
+ );
+ const appMenuAvatar = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-avatar"
+ );
+
+ let defaultLabel = appMenuStatus.getAttribute("defaultlabel");
+ const status = state.status;
+ // Reset the status bar to its original state.
+ appMenuLabel.setAttribute("label", defaultLabel);
+ appMenuStatus.removeAttribute("fxastatus");
+ appMenuAvatar.style.removeProperty("list-style-image");
+ appMenuLabel.classList.remove("subviewbutton-nav");
+
+ if (status == UIState.STATUS_NOT_CONFIGURED) {
+ return;
+ }
+
+ // At this point we consider sync to be configured (but still can be in an error state).
+ if (status == UIState.STATUS_LOGIN_FAILED) {
+ let tooltipDescription = this.fxaStrings.formatStringFromName(
+ "reconnectDescription",
+ [state.email]
+ );
+ let errorLabel = appMenuStatus.getAttribute("errorlabel");
+ appMenuStatus.setAttribute("fxastatus", "login-failed");
+ appMenuLabel.setAttribute("label", errorLabel);
+ appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
+ return;
+ } else if (status == UIState.STATUS_NOT_VERIFIED) {
+ let tooltipDescription = this.fxaStrings.formatStringFromName(
+ "verifyDescription",
+ [state.email]
+ );
+ let unverifiedLabel = appMenuStatus.getAttribute("unverifiedlabel");
+ appMenuStatus.setAttribute("fxastatus", "unverified");
+ appMenuLabel.setAttribute("label", unverifiedLabel);
+ appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
+ return;
+ }
+
+ // At this point we consider sync to be logged-in.
+ appMenuStatus.setAttribute("fxastatus", "signedin");
+ appMenuLabel.setAttribute("label", state.displayName || state.email);
+ appMenuLabel.classList.add("subviewbutton-nav");
+ appMenuStatus.removeAttribute("tooltiptext");
+ },
+
+ updateState(state) {
+ for (let [shown, menuId, boxId] of [
+ [
+ state.status == UIState.STATUS_NOT_CONFIGURED,
+ "sync-setup",
+ "PanelUI-remotetabs-setupsync",
+ ],
+ [
+ state.status == UIState.STATUS_SIGNED_IN && !state.syncEnabled,
+ "sync-enable",
+ "PanelUI-remotetabs-syncdisabled",
+ ],
+ [
+ state.status == UIState.STATUS_LOGIN_FAILED,
+ "sync-reauthitem",
+ "PanelUI-remotetabs-reauthsync",
+ ],
+ [
+ state.status == UIState.STATUS_NOT_VERIFIED,
+ "sync-unverifieditem",
+ "PanelUI-remotetabs-unverified",
+ ],
+ [
+ state.status == UIState.STATUS_SIGNED_IN && state.syncEnabled,
+ "sync-syncnowitem",
+ "PanelUI-remotetabs-main",
+ ],
+ ]) {
+ document.getElementById(menuId).hidden = PanelMultiView.getViewNode(
+ document,
+ boxId
+ ).hidden = !shown;
+ }
+ },
+
+ updateSyncStatus(state) {
+ let syncNow =
+ document.querySelector(".syncNowBtn") ||
+ document
+ .getElementById("appMenu-viewCache")
+ .content.querySelector(".syncNowBtn");
+ const syncingUI = syncNow.getAttribute("syncstatus") == "active";
+ if (state.syncing != syncingUI) {
+ // Do we need to update the UI?
+ state.syncing ? this.onActivityStart() : this.onActivityStop();
+ }
+ },
+
+ async openSignInAgainPage(entryPoint) {
+ const url = await FxAccounts.config.promiseForceSigninURI(entryPoint);
+ switchToTabHavingURI(url, true, {
+ replaceQueryString: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ },
+
+ async openDevicesManagementPage(entryPoint) {
+ let url = await FxAccounts.config.promiseManageDevicesURI(entryPoint);
+ switchToTabHavingURI(url, true, {
+ replaceQueryString: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ },
+
+ async openConnectAnotherDevice(entryPoint) {
+ const url = await FxAccounts.config.promiseConnectDeviceURI(entryPoint);
+ openTrustedLinkIn(url, "tab");
+ },
+
+ async openConnectAnotherDeviceFromFxaMenu(panel = undefined) {
+ this.emitFxaToolbarTelemetry("cad", panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openConnectAnotherDevice(entryPoint);
+ },
+
+ openSendToDevicePromo() {
+ const url = Services.urlFormatter.formatURLPref(
+ "identity.sendtabpromo.url"
+ );
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ async clickFxAMenuHeaderButton(panel = undefined) {
+ // Depending on the current logged in state of a user,
+ // clicking the FxA header will either open
+ // a sign-in page, account management page, or sync
+ // preferences page.
+ const { status } = UIState.get();
+ switch (status) {
+ case UIState.STATUS_NOT_CONFIGURED:
+ this.openFxAEmailFirstPageFromFxaMenu(panel);
+ break;
+ case UIState.STATUS_LOGIN_FAILED:
+ case UIState.STATUS_NOT_VERIFIED:
+ this.openPrefsFromFxaMenu("sync_settings", panel);
+ break;
+ case UIState.STATUS_SIGNED_IN:
+ PanelUI.showSubView("PanelUI-fxa-menu-account-panel", panel);
+ }
+ },
+
+ async openFxAEmailFirstPage(entryPoint) {
+ const url = await FxAccounts.config.promiseConnectAccountURI(entryPoint);
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ async openFxAEmailFirstPageFromFxaMenu(panel = undefined) {
+ this.emitFxaToolbarTelemetry("login", panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openFxAEmailFirstPage(entryPoint);
+ },
+
+ async openFxAManagePage(entryPoint) {
+ const url = await FxAccounts.config.promiseManageURI(entryPoint);
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ async openFxAManagePageFromFxaMenu(panel = undefined) {
+ this.emitFxaToolbarTelemetry("account_settings", panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openFxAManagePage(entryPoint);
+ },
+
+ async openSendFromFxaMenu(panel) {
+ this.emitFxaToolbarTelemetry("open_send", panel);
+ this.launchFxaService(gFxaSendLoginUrl);
+ },
+
+ async openMonitorFromFxaMenu(panel) {
+ this.emitFxaToolbarTelemetry("open_monitor", panel);
+ this.launchFxaService(gFxaMonitorLoginUrl);
+ },
+
+ launchFxaService(serviceUrl, panel) {
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+
+ const url = new URL(serviceUrl);
+ url.searchParams.set("utm_source", "fxa-toolbar");
+ url.searchParams.set("utm_medium", "referral");
+ url.searchParams.set("entrypoint", entryPoint);
+
+ const state = UIState.get();
+ if (state.status == UIState.STATUS_SIGNED_IN) {
+ url.searchParams.set("email", state.email);
+ }
+
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ // Returns true if we managed to send the tab to any targets, false otherwise.
+ async sendTabToDevice(url, targets, title) {
+ const fxaCommandsDevices = [];
+ const oldSendTabClients = [];
+ for (const target of targets) {
+ if (fxAccounts.commands.sendTab.isDeviceCompatible(target)) {
+ fxaCommandsDevices.push(target);
+ } else if (target.clientRecord) {
+ oldSendTabClients.push(target.clientRecord);
+ } else {
+ this.log.error(`Target ${target.id} unsuitable for send tab.`);
+ }
+ }
+ // If a master-password is enabled then it must be unlocked so FxA can get
+ // the encryption keys from the login manager. (If we end up using the "sync"
+ // fallback that would end up prompting by itself, but the FxA command route
+ // will not) - so force that here.
+ let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService(
+ Ci.nsILoginManagerCrypto
+ );
+ if (!cryptoSDR.isLoggedIn) {
+ if (cryptoSDR.uiBusy) {
+ this.log.info("Master password UI is busy - not sending the tabs");
+ return false;
+ }
+ try {
+ cryptoSDR.encrypt("bacon"); // forces the mp prompt.
+ } catch (e) {
+ this.log.info(
+ "Master password remains unlocked - not sending the tabs"
+ );
+ return false;
+ }
+ }
+ let numFailed = 0;
+ if (fxaCommandsDevices.length) {
+ this.log.info(
+ `Sending a tab to ${fxaCommandsDevices
+ .map(d => d.id)
+ .join(", ")} using FxA commands.`
+ );
+ const report = await fxAccounts.commands.sendTab.send(
+ fxaCommandsDevices,
+ { url, title }
+ );
+ for (let { device, error } of report.failed) {
+ this.log.error(
+ `Failed to send a tab with FxA commands for ${device.id}.`,
+ error
+ );
+ numFailed++;
+ }
+ }
+ for (let client of oldSendTabClients) {
+ try {
+ this.log.info(`Sending a tab to ${client.id} using Sync.`);
+ await Weave.Service.clientsEngine.sendURIToClientForDisplay(
+ url,
+ client.id,
+ title
+ );
+ } catch (e) {
+ numFailed++;
+ this.log.error("Could not send tab to device.", e);
+ }
+ }
+ return numFailed < targets.length; // Good enough.
+ },
+
+ populateSendTabToDevicesMenu(
+ devicesPopup,
+ url,
+ title,
+ multiselected,
+ createDeviceNodeFn
+ ) {
+ if (!createDeviceNodeFn) {
+ createDeviceNodeFn = (targetId, name, targetType, lastModified) => {
+ let eltName = name ? "menuitem" : "menuseparator";
+ return document.createXULElement(eltName);
+ };
+ }
+
+ // remove existing menu items
+ for (let i = devicesPopup.children.length - 1; i >= 0; --i) {
+ let child = devicesPopup.children[i];
+ if (child.classList.contains("sync-menuitem")) {
+ child.remove();
+ }
+ }
+
+ if (gSync.sendTabConfiguredAndLoading) {
+ // We can only be in this case in the page action menu.
+ return;
+ }
+
+ const fragment = document.createDocumentFragment();
+
+ const state = UIState.get();
+ if (state.status == UIState.STATUS_SIGNED_IN) {
+ const targets = this.getSendTabTargets();
+ if (targets.length) {
+ this._appendSendTabDeviceList(
+ targets,
+ fragment,
+ createDeviceNodeFn,
+ url,
+ title,
+ multiselected
+ );
+ } else {
+ this._appendSendTabSingleDevice(fragment, createDeviceNodeFn);
+ }
+ } else if (
+ state.status == UIState.STATUS_NOT_VERIFIED ||
+ state.status == UIState.STATUS_LOGIN_FAILED
+ ) {
+ this._appendSendTabVerify(fragment, createDeviceNodeFn);
+ } /* status is STATUS_NOT_CONFIGURED */ else {
+ this._appendSendTabUnconfigured(fragment, createDeviceNodeFn);
+ }
+
+ devicesPopup.appendChild(fragment);
+ },
+
+ _appendSendTabDeviceList(
+ targets,
+ fragment,
+ createDeviceNodeFn,
+ url,
+ title,
+ multiselected
+ ) {
+ let tabsToSend = multiselected
+ ? gBrowser.selectedTabs.map(t => {
+ return {
+ url: t.linkedBrowser.currentURI.spec,
+ title: t.linkedBrowser.contentTitle,
+ };
+ })
+ : [{ url, title }];
+
+ const send = to => {
+ Promise.all(
+ tabsToSend.map(t =>
+ // sendTabToDevice does not reject.
+ this.sendTabToDevice(t.url, to, t.title)
+ )
+ ).then(results => {
+ if (results.includes(true)) {
+ let action = PageActions.actionForID("sendToDevice");
+ showBrowserPageActionFeedback(action);
+ }
+ fxAccounts.flushLogFile();
+ });
+ };
+ const onSendAllCommand = event => {
+ send(targets);
+ };
+ const onTargetDeviceCommand = event => {
+ const targetId = event.target.getAttribute("clientId");
+ const target = targets.find(t => t.id == targetId);
+ send([target]);
+ };
+
+ function addTargetDevice(targetId, name, targetType, lastModified) {
+ const targetDevice = createDeviceNodeFn(
+ targetId,
+ name,
+ targetType,
+ lastModified
+ );
+ targetDevice.addEventListener(
+ "command",
+ targetId ? onTargetDeviceCommand : onSendAllCommand,
+ true
+ );
+ targetDevice.classList.add("sync-menuitem", "sendtab-target");
+ targetDevice.setAttribute("clientId", targetId);
+ targetDevice.setAttribute("clientType", targetType);
+ targetDevice.setAttribute("label", name);
+ fragment.appendChild(targetDevice);
+ }
+
+ for (let target of targets) {
+ let type, lastModified;
+ if (target.clientRecord) {
+ type = Weave.Service.clientsEngine.getClientType(
+ target.clientRecord.id
+ );
+ lastModified = new Date(target.clientRecord.serverLastModified * 1000);
+ } else {
+ // For phones, FxA uses "mobile" and Sync clients uses "phone".
+ type = target.type == "mobile" ? "phone" : target.type;
+ lastModified = target.lastAccessTime
+ ? new Date(target.lastAccessTime)
+ : null;
+ }
+ addTargetDevice(target.id, target.name, type, lastModified);
+ }
+
+ if (targets.length > 1) {
+ // "Send to All Devices" menu item
+ const separator = createDeviceNodeFn();
+ separator.classList.add("sync-menuitem");
+ fragment.appendChild(separator);
+ const allDevicesLabel = this.fxaStrings.GetStringFromName(
+ "sendToAllDevices.menuitem"
+ );
+ addTargetDevice("", allDevicesLabel, "");
+
+ // "Manage devices" menu item
+ const manageDevicesLabel = this.fxaStrings.GetStringFromName(
+ "manageDevices.menuitem"
+ );
+ // We piggyback on the createDeviceNodeFn implementation,
+ // it's a big disgusting.
+ const targetDevice = createDeviceNodeFn(
+ null,
+ manageDevicesLabel,
+ null,
+ null
+ );
+ targetDevice.addEventListener(
+ "command",
+ () => gSync.openDevicesManagementPage("sendtab"),
+ true
+ );
+ targetDevice.classList.add("sync-menuitem", "sendtab-target");
+ targetDevice.setAttribute("label", manageDevicesLabel);
+ fragment.appendChild(targetDevice);
+ }
+ },
+
+ _appendSendTabSingleDevice(fragment, createDeviceNodeFn) {
+ const noDevices = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.singledevice.status"
+ );
+ const learnMore = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.singledevice"
+ );
+ const connectDevice = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.connectdevice"
+ );
+ const actions = [
+ {
+ label: connectDevice,
+ command: () => this.openConnectAnotherDevice("sendtab"),
+ },
+ { label: learnMore, command: () => this.openSendToDevicePromo() },
+ ];
+ this._appendSendTabInfoItems(
+ fragment,
+ createDeviceNodeFn,
+ noDevices,
+ actions
+ );
+ },
+
+ _appendSendTabVerify(fragment, createDeviceNodeFn) {
+ const notVerified = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.verify.status"
+ );
+ const verifyAccount = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.verify"
+ );
+ const actions = [
+ { label: verifyAccount, command: () => this.openPrefs("sendtab") },
+ ];
+ this._appendSendTabInfoItems(
+ fragment,
+ createDeviceNodeFn,
+ notVerified,
+ actions
+ );
+ },
+
+ _appendSendTabUnconfigured(fragment, createDeviceNodeFn) {
+ const brandProductName = gBrandBundle.GetStringFromName("brandProductName");
+ const notConnected = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.unconfigured.label2"
+ );
+ const learnMore = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.unconfigured"
+ );
+ const actions = [
+ { label: learnMore, command: () => this.openSendToDevicePromo() },
+ ];
+ this._appendSendTabInfoItems(
+ fragment,
+ createDeviceNodeFn,
+ notConnected,
+ actions
+ );
+
+ // Now add a 'sign in to Firefox' item above the 'learn more' item.
+ const signInToFxA = this.fxaStrings.formatStringFromName(
+ "sendTabToDevice.signintofxa",
+ [brandProductName]
+ );
+ let signInItem = createDeviceNodeFn(null, signInToFxA, null);
+ signInItem.classList.add("sync-menuitem");
+ signInItem.setAttribute("label", signInToFxA);
+ // Show an icon if opened in the page action panel:
+ if (signInItem.classList.contains("subviewbutton")) {
+ signInItem.classList.add("subviewbutton-iconic", "signintosync");
+ }
+ signInItem.addEventListener("command", () => {
+ this.openPrefs("sendtab");
+ });
+ fragment.insertBefore(signInItem, fragment.lastElementChild);
+ },
+
+ _appendSendTabInfoItems(fragment, createDeviceNodeFn, statusLabel, actions) {
+ const status = createDeviceNodeFn(null, statusLabel, null);
+ status.setAttribute("label", statusLabel);
+ status.setAttribute("disabled", true);
+ status.classList.add("sync-menuitem");
+ fragment.appendChild(status);
+
+ const separator = createDeviceNodeFn(null, null, null);
+ separator.classList.add("sync-menuitem");
+ fragment.appendChild(separator);
+
+ for (let { label, command } of actions) {
+ const actionItem = createDeviceNodeFn(null, label, null);
+ actionItem.addEventListener("command", command, true);
+ actionItem.classList.add("sync-menuitem");
+ actionItem.setAttribute("label", label);
+ fragment.appendChild(actionItem);
+ }
+ },
+
+ isSendableURI(aURISpec) {
+ if (!aURISpec) {
+ return false;
+ }
+ // Disallow sending tabs with more than 65535 characters.
+ if (aURISpec.length > 65535) {
+ return false;
+ }
+ if (this.UNSENDABLE_URL_REGEXP) {
+ return !this.UNSENDABLE_URL_REGEXP.test(aURISpec);
+ }
+ // The preference has been removed, or is an invalid regexp, so we treat it
+ // as a valid URI. We've already logged an error when trying to construct
+ // the regexp, and the more problematic case is the length, which we've
+ // already addressed.
+ return true;
+ },
+
+ // "Send Tab to Device" menu item
+ updateTabContextMenu(aPopupMenu, aTargetTab) {
+ // We may get here before initialisation. This situation
+ // can lead to a empty label for 'Send To Device' Menu.
+ this.init();
+
+ if (!this.FXA_ENABLED) {
+ // These items are hidden in onFxaDisabled(). No need to do anything.
+ return;
+ }
+ let hasASendableURI = false;
+ for (let tab of aTargetTab.multiselected
+ ? gBrowser.selectedTabs
+ : [aTargetTab]) {
+ if (this.isSendableURI(tab.linkedBrowser.currentURI.spec)) {
+ hasASendableURI = true;
+ break;
+ }
+ }
+ const enabled = !this.sendTabConfiguredAndLoading && hasASendableURI;
+
+ let sendTabsToDevice = document.getElementById("context_sendTabToDevice");
+ sendTabsToDevice.disabled = !enabled;
+
+ let tabCount = aTargetTab.multiselected
+ ? gBrowser.multiSelectedTabsCount
+ : 1;
+ sendTabsToDevice.label = PluralForm.get(
+ tabCount,
+ gNavigatorBundle.getString("sendTabsToDevice.label")
+ ).replace("#1", tabCount.toLocaleString());
+ sendTabsToDevice.accessKey = gNavigatorBundle.getString(
+ "sendTabsToDevice.accesskey"
+ );
+ },
+
+ // "Send Page to Device" and "Send Link to Device" menu items
+ updateContentContextMenu(contextMenu) {
+ if (!this.FXA_ENABLED) {
+ // These items are hidden by default. No need to do anything.
+ return;
+ }
+ // showSendLink and showSendPage are mutually exclusive
+ const showSendLink =
+ contextMenu.onSaveableLink || contextMenu.onPlainTextLink;
+ const showSendPage =
+ !showSendLink &&
+ !(
+ contextMenu.isContentSelected ||
+ contextMenu.onImage ||
+ contextMenu.onCanvas ||
+ contextMenu.onVideo ||
+ contextMenu.onAudio ||
+ contextMenu.onLink ||
+ contextMenu.onTextInput
+ );
+
+ // Avoids double separator on images with links.
+ const hideSeparator =
+ contextMenu.isContentSelected &&
+ contextMenu.onLink &&
+ contextMenu.onImage;
+ [
+ "context-sendpagetodevice",
+ ...(hideSeparator ? [] : ["context-sep-sendpagetodevice"]),
+ ].forEach(id => contextMenu.showItem(id, showSendPage));
+ [
+ "context-sendlinktodevice",
+ ...(hideSeparator ? [] : ["context-sep-sendlinktodevice"]),
+ ].forEach(id => contextMenu.showItem(id, showSendLink));
+
+ if (!showSendLink && !showSendPage) {
+ return;
+ }
+
+ const targetURI = showSendLink
+ ? contextMenu.linkURL
+ : contextMenu.browser.currentURI.spec;
+ const enabled =
+ !this.sendTabConfiguredAndLoading && this.isSendableURI(targetURI);
+ contextMenu.setItemAttr(
+ showSendPage ? "context-sendpagetodevice" : "context-sendlinktodevice",
+ "disabled",
+ !enabled || null
+ );
+ },
+
+ // Functions called by observers
+ onActivityStart() {
+ clearTimeout(this._syncAnimationTimer);
+ this._syncStartTime = Date.now();
+
+ document.querySelectorAll(".syncNowBtn").forEach(el => {
+ el.setAttribute("syncstatus", "active");
+ el.setAttribute("disabled", "true");
+ document.l10n.setAttributes(el, el.getAttribute("syncinglabel"));
+ });
+
+ document
+ .getElementById("appMenu-viewCache")
+ .content.querySelectorAll(".syncNowBtn")
+ .forEach(el => {
+ el.setAttribute("syncstatus", "active");
+ el.setAttribute("disabled", "true");
+ document.l10n.setAttributes(el, el.getAttribute("syncinglabel"));
+ });
+ },
+
+ _onActivityStop() {
+ if (!gBrowser) {
+ return;
+ }
+
+ document.querySelectorAll(".syncNowBtn").forEach(el => {
+ el.removeAttribute("syncstatus");
+ el.removeAttribute("disabled");
+ document.l10n.setAttributes(el, "fxa-toolbar-sync-now");
+ });
+
+ document
+ .getElementById("appMenu-viewCache")
+ .content.querySelectorAll(".syncNowBtn")
+ .forEach(el => {
+ el.removeAttribute("syncstatus");
+ el.removeAttribute("disabled");
+ document.l10n.setAttributes(el, "fxa-toolbar-sync-now");
+ });
+
+ Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
+ },
+
+ onActivityStop() {
+ let now = Date.now();
+ let syncDuration = now - this._syncStartTime;
+
+ if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
+ let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
+ clearTimeout(this._syncAnimationTimer);
+ this._syncAnimationTimer = setTimeout(
+ () => this._onActivityStop(),
+ animationTime
+ );
+ } else {
+ this._onActivityStop();
+ }
+ },
+
+ // Disconnect from sync, and optionally disconnect from the FxA account.
+ // Returns true if the disconnection happened (ie, if the user didn't decline
+ // when asked to confirm)
+ async disconnect({ confirm = true, disconnectAccount = true } = {}) {
+ if (disconnectAccount) {
+ let deleteLocalData = false;
+ if (confirm) {
+ let options = await this._confirmFxaAndSyncDisconnect();
+ if (!options.userConfirmedDisconnect) {
+ return false;
+ }
+ deleteLocalData = options.deleteLocalData;
+ }
+ return this._disconnectFxaAndSync(deleteLocalData);
+ }
+
+ if (confirm && !(await this._confirmSyncDisconnect())) {
+ return false;
+ }
+ return this._disconnectSync();
+ },
+
+ // Prompt the user to confirm disconnect from FxA and sync with the option
+ // to delete syncable data from the device.
+ async _confirmFxaAndSyncDisconnect() {
+ let options = {
+ userConfirmedDisconnect: false,
+ };
+
+ window.openDialog(
+ "chrome://browser/content/browser-fxaSignout.xhtml",
+ "_blank",
+ "chrome,modal,centerscreen,resizable=no",
+ { hideDeleteDataOption: !UIState.get().syncEnabled },
+ options
+ );
+
+ return options;
+ },
+
+ async _disconnectFxaAndSync(deleteLocalData) {
+ const { SyncDisconnect } = ChromeUtils.import(
+ "resource://services-sync/SyncDisconnect.jsm"
+ );
+ // Record telemetry.
+ await fxAccounts.telemetry.recordDisconnection(null, "ui");
+
+ await SyncDisconnect.disconnect(deleteLocalData).catch(e => {
+ console.error("Failed to disconnect.", e);
+ });
+
+ return true;
+ },
+
+ // Prompt the user to confirm disconnect from sync. In this case the data
+ // on the device is not deleted.
+ async _confirmSyncDisconnect() {
+ const l10nPrefix = "sync-disconnect-dialog";
+
+ const [title, body, button] = await document.l10n.formatValues([
+ { id: `${l10nPrefix}-title` },
+ { id: `${l10nPrefix}-body` },
+ { id: "sync-disconnect-dialog-button" },
+ ]);
+
+ const flags =
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;
+
+ // buttonPressed will be 0 for disconnect, 1 for cancel.
+ const buttonPressed = Services.prompt.confirmEx(
+ window,
+ title,
+ body,
+ flags,
+ button,
+ null,
+ null,
+ null,
+ {}
+ );
+ return buttonPressed == 0;
+ },
+
+ async _disconnectSync() {
+ await fxAccounts.telemetry.recordDisconnection("sync", "ui");
+
+ await Weave.Service.promiseInitialized;
+ await Weave.Service.startOver();
+
+ return true;
+ },
+
+ // doSync forces a sync - it *does not* return a promise as it is called
+ // via the various UI components.
+ doSync() {
+ if (!UIState.isReady()) {
+ return;
+ }
+ // Note we don't bother checking if sync is actually enabled - none of the
+ // UI which calls this function should be visible in that case.
+ const state = UIState.get();
+ if (state.status == UIState.STATUS_SIGNED_IN) {
+ this.updateSyncStatus({ syncing: true });
+ Services.tm.dispatchToMainThread(() => {
+ // We are pretty confident that push helps us pick up all FxA commands,
+ // but some users might have issues with push, so let's unblock them
+ // by fetching the missed FxA commands on manual sync.
+ fxAccounts.commands.pollDeviceCommands().catch(e => {
+ this.log.error("Fetching missed remote commands failed.", e);
+ });
+ Weave.Service.sync();
+ });
+ }
+ },
+
+ doSyncFromFxaMenu(panel) {
+ this.doSync();
+ this.emitFxaToolbarTelemetry("sync_now", panel);
+ },
+
+ openPrefs(entryPoint = "syncbutton", origin = undefined) {
+ window.openPreferences("paneSync", {
+ origin,
+ urlParams: { entrypoint: entryPoint },
+ });
+ },
+
+ openPrefsFromFxaMenu(type, panel) {
+ this.emitFxaToolbarTelemetry(type, panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openPrefs(entryPoint);
+ },
+
+ openSyncedTabsPanel() {
+ let placement = CustomizableUI.getPlacementOfWidget("sync-button");
+ let area = placement && placement.area;
+ let anchor =
+ document.getElementById("sync-button") ||
+ document.getElementById("PanelUI-menu-button");
+ if (area == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
+ // The button is in the overflow panel, so we need to show the panel,
+ // then show our subview.
+ let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+ navbar.overflowable.show().then(() => {
+ PanelUI.showSubView("PanelUI-remotetabs", anchor);
+ }, Cu.reportError);
+ } else {
+ // It is placed somewhere else - just try and show it.
+ PanelUI.showSubView("PanelUI-remotetabs", anchor);
+ }
+ },
+
+ refreshSyncButtonsTooltip() {
+ const state = UIState.get();
+ this.updateSyncButtonsTooltip(state);
+ },
+
+ /* Update the tooltip for the sync icon in the main menu and in Synced Tabs.
+ If Sync is configured, the tooltip is when the last sync occurred,
+ otherwise the tooltip reflects the fact that Sync needs to be
+ (re-)configured.
+ */
+ updateSyncButtonsTooltip(state) {
+ const status = state.status;
+
+ // This is a little messy as the Sync buttons are 1/2 Sync related and
+ // 1/2 FxA related - so for some strings we use Sync strings, but for
+ // others we reach into gSync for strings.
+ let tooltiptext;
+ if (status == UIState.STATUS_NOT_VERIFIED) {
+ // "needs verification"
+ tooltiptext = this.fxaStrings.formatStringFromName("verifyDescription", [
+ state.email,
+ ]);
+ } else if (status == UIState.STATUS_NOT_CONFIGURED) {
+ // "needs setup".
+ tooltiptext = this.syncStrings.GetStringFromName(
+ "signInToSync.description"
+ );
+ } else if (status == UIState.STATUS_LOGIN_FAILED) {
+ // "need to reconnect/re-enter your password"
+ tooltiptext = this.fxaStrings.formatStringFromName(
+ "reconnectDescription",
+ [state.email]
+ );
+ } else {
+ // Sync appears configured - format the "last synced at" time.
+ tooltiptext = this.formatLastSyncDate(state.lastSync);
+ }
+
+ document.querySelectorAll(".syncNowBtn").forEach(el => {
+ if (tooltiptext) {
+ el.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ el.removeAttribute("tooltiptext");
+ }
+ });
+
+ document
+ .getElementById("appMenu-viewCache")
+ .content.querySelectorAll(".syncNowBtn")
+ .forEach(el => {
+ if (tooltiptext) {
+ el.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ el.removeAttribute("tooltiptext");
+ }
+ });
+ },
+
+ get relativeTimeFormat() {
+ delete this.relativeTimeFormat;
+ return (this.relativeTimeFormat = new Services.intl.RelativeTimeFormat(
+ undefined,
+ { style: "long" }
+ ));
+ },
+
+ formatLastSyncDate(date) {
+ if (!date) {
+ // Date can be null before the first sync!
+ return null;
+ }
+ try {
+ const relativeDateStr = this.relativeTimeFormat.formatBestUnit(date);
+ return this.syncStrings.formatStringFromName("lastSync2.label", [
+ relativeDateStr,
+ ]);
+ } catch (ex) {
+ // shouldn't happen, but one client having an invalid date shouldn't
+ // break the entire feature.
+ this.log.warn("failed to format lastSync time", date, ex);
+ return null;
+ }
+ },
+
+ onClientsSynced() {
+ // Note that this element is only shown if Sync is enabled.
+ let element = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-remotetabs-main"
+ );
+ if (element) {
+ if (Weave.Service.clientsEngine.stats.numClients > 1) {
+ element.setAttribute("devices-status", "multi");
+ } else {
+ element.setAttribute("devices-status", "single");
+ }
+ }
+ },
+
+ onFxaDisabled() {
+ document.documentElement.setAttribute("fxadisabled", true);
+
+ const toHide = [...document.querySelectorAll(".sync-ui-item")];
+ for (const item of toHide) {
+ item.hidden = true;
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+};
diff --git a/browser/base/content/browser-tabsintitlebar.js b/browser/base/content/browser-tabsintitlebar.js
new file mode 100644
index 0000000000..3184f7b448
--- /dev/null
+++ b/browser/base/content/browser-tabsintitlebar.js
@@ -0,0 +1,125 @@
+/* -*- 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/. */
+
+var TabsInTitlebar;
+
+{
+ // start private TabsInTitlebar scope
+ TabsInTitlebar = {
+ init() {
+ this._readPref();
+ Services.prefs.addObserver(this._prefName, this);
+
+ dragSpaceObserver.init();
+ this._initialized = true;
+ this._update();
+ },
+
+ allowedBy(condition, allow) {
+ if (allow) {
+ if (condition in this._disallowed) {
+ delete this._disallowed[condition];
+ this._update();
+ }
+ } else if (!(condition in this._disallowed)) {
+ this._disallowed[condition] = null;
+ this._update();
+ }
+ },
+
+ get systemSupported() {
+ let isSupported = false;
+ switch (AppConstants.MOZ_WIDGET_TOOLKIT) {
+ case "windows":
+ case "cocoa":
+ isSupported = true;
+ break;
+ case "gtk":
+ isSupported = window.matchMedia("(-moz-gtk-csd-available)").matches;
+ break;
+ }
+ delete this.systemSupported;
+ return (this.systemSupported = isSupported);
+ },
+
+ get enabled() {
+ return document.documentElement.getAttribute("tabsintitlebar") == "true";
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ this._readPref();
+ }
+ },
+
+ _initialized: false,
+ _disallowed: {},
+ _prefName: "browser.tabs.drawInTitlebar",
+
+ _readPref() {
+ let hiddenTitlebar = Services.prefs.getBoolPref(
+ "browser.tabs.drawInTitlebar",
+ window.matchMedia("(-moz-gtk-csd-hide-titlebar-by-default)").matches
+ );
+ this.allowedBy("pref", hiddenTitlebar);
+ },
+
+ _update() {
+ if (!this._initialized) {
+ return;
+ }
+
+ let allowed =
+ this.systemSupported &&
+ !window.fullScreen &&
+ !Object.keys(this._disallowed).length;
+ if (allowed) {
+ document.documentElement.setAttribute("tabsintitlebar", "true");
+ if (AppConstants.platform == "macosx") {
+ document.documentElement.setAttribute("chromemargin", "0,-1,-1,-1");
+ document.documentElement.removeAttribute("drawtitle");
+ } else {
+ document.documentElement.setAttribute("chromemargin", "0,2,2,2");
+ }
+ } else {
+ document.documentElement.removeAttribute("tabsintitlebar");
+ document.documentElement.removeAttribute("chromemargin");
+ if (AppConstants.platform == "macosx") {
+ document.documentElement.setAttribute("drawtitle", "true");
+ }
+ }
+
+ ToolbarIconColor.inferFromText("tabsintitlebar", allowed);
+ },
+
+ uninit() {
+ Services.prefs.removeObserver(this._prefName, this);
+ dragSpaceObserver.uninit();
+ },
+ };
+
+ // Adds additional drag space to the window by listening to
+ // the corresponding preference.
+ let dragSpaceObserver = {
+ pref: "browser.tabs.extraDragSpace",
+
+ init() {
+ Services.prefs.addObserver(this.pref, this);
+ this.observe();
+ },
+
+ uninit() {
+ Services.prefs.removeObserver(this.pref, this);
+ },
+
+ observe() {
+ if (Services.prefs.getBoolPref(this.pref)) {
+ document.documentElement.setAttribute("extradragspace", "true");
+ } else {
+ document.documentElement.removeAttribute("extradragspace");
+ }
+ },
+ };
+} // end private TabsInTitlebar scope
diff --git a/browser/base/content/browser-thumbnails.js b/browser/base/content/browser-thumbnails.js
new file mode 100644
index 0000000000..3456d14b3b
--- /dev/null
+++ b/browser/base/content/browser-thumbnails.js
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Keeps thumbnails of open web pages up-to-date.
+ */
+var gBrowserThumbnails = {
+ /**
+ * Pref that controls whether we can store SSL content on disk
+ */
+ PREF_DISK_CACHE_SSL: "browser.cache.disk_cache_ssl",
+
+ _captureDelayMS: 1000,
+
+ /**
+ * Used to keep track of disk_cache_ssl preference
+ */
+ _sslDiskCacheEnabled: null,
+
+ /**
+ * Map of capture() timeouts assigned to their browsers.
+ */
+ _timeouts: null,
+
+ /**
+ * Top site URLs refresh timer.
+ */
+ _topSiteURLsRefreshTimer: null,
+
+ /**
+ * List of tab events we want to listen for.
+ */
+ _tabEvents: ["TabClose", "TabSelect"],
+
+ init: function Thumbnails_init() {
+ gBrowser.addTabsProgressListener(this);
+ Services.prefs.addObserver(this.PREF_DISK_CACHE_SSL, this);
+
+ this._sslDiskCacheEnabled = Services.prefs.getBoolPref(
+ this.PREF_DISK_CACHE_SSL
+ );
+
+ this._tabEvents.forEach(function(aEvent) {
+ gBrowser.tabContainer.addEventListener(aEvent, this);
+ }, this);
+
+ this._timeouts = new WeakMap();
+ },
+
+ uninit: function Thumbnails_uninit() {
+ gBrowser.removeTabsProgressListener(this);
+ Services.prefs.removeObserver(this.PREF_DISK_CACHE_SSL, this);
+
+ if (this._topSiteURLsRefreshTimer) {
+ this._topSiteURLsRefreshTimer.cancel();
+ this._topSiteURLsRefreshTimer = null;
+ }
+
+ this._tabEvents.forEach(function(aEvent) {
+ gBrowser.tabContainer.removeEventListener(aEvent, this);
+ }, this);
+ },
+
+ handleEvent: function Thumbnails_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "scroll":
+ let browser = aEvent.currentTarget;
+ if (this._timeouts.has(browser)) {
+ this._delayedCapture(browser);
+ }
+ break;
+ case "TabSelect":
+ this._delayedCapture(aEvent.target.linkedBrowser);
+ break;
+ case "TabClose": {
+ this._cancelDelayedCapture(aEvent.target.linkedBrowser);
+ break;
+ }
+ }
+ },
+
+ observe: function Thumbnails_observe(subject, topic, data) {
+ switch (data) {
+ case this.PREF_DISK_CACHE_SSL:
+ this._sslDiskCacheEnabled = Services.prefs.getBoolPref(
+ this.PREF_DISK_CACHE_SSL
+ );
+ break;
+ }
+ },
+
+ clearTopSiteURLCache: function Thumbnails_clearTopSiteURLCache() {
+ if (this._topSiteURLsRefreshTimer) {
+ this._topSiteURLsRefreshTimer.cancel();
+ this._topSiteURLsRefreshTimer = null;
+ }
+ // Delete the defined property
+ delete this._topSiteURLs;
+ XPCOMUtils.defineLazyGetter(this, "_topSiteURLs", getTopSiteURLs);
+ },
+
+ notify: function Thumbnails_notify(timer) {
+ gBrowserThumbnails._topSiteURLsRefreshTimer = null;
+ gBrowserThumbnails.clearTopSiteURLCache();
+ },
+
+ /**
+ * State change progress listener for all tabs.
+ */
+ onStateChange: function Thumbnails_onStateChange(
+ aBrowser,
+ aWebProgress,
+ aRequest,
+ aStateFlags,
+ aStatus
+ ) {
+ if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
+ ) {
+ this._delayedCapture(aBrowser);
+ }
+ },
+
+ async _capture(aBrowser) {
+ // Only capture about:newtab top sites.
+ const topSites = await this._topSiteURLs;
+ if (!aBrowser.currentURI || !topSites.includes(aBrowser.currentURI.spec)) {
+ return;
+ }
+ if (await this._shouldCapture(aBrowser)) {
+ await PageThumbs.captureAndStoreIfStale(aBrowser);
+ }
+ },
+
+ _delayedCapture: function Thumbnails_delayedCapture(aBrowser) {
+ if (this._timeouts.has(aBrowser)) {
+ this._cancelDelayedCallbacks(aBrowser);
+ } else {
+ aBrowser.addEventListener("scroll", this, true);
+ }
+
+ let idleCallback = () => {
+ this._cancelDelayedCapture(aBrowser);
+ this._capture(aBrowser);
+ };
+
+ // setTimeout to set a guarantee lower bound for the requestIdleCallback
+ // (and therefore the delayed capture)
+ let timeoutId = setTimeout(() => {
+ let idleCallbackId = requestIdleCallback(idleCallback, {
+ timeout: this._captureDelayMS * 30,
+ });
+ this._timeouts.set(aBrowser, { isTimeout: false, id: idleCallbackId });
+ }, this._captureDelayMS);
+
+ this._timeouts.set(aBrowser, { isTimeout: true, id: timeoutId });
+ },
+
+ _shouldCapture: async function Thumbnails_shouldCapture(aBrowser) {
+ // Capture only if it's the currently selected tab and not an about: page.
+ if (
+ aBrowser != gBrowser.selectedBrowser ||
+ gBrowser.currentURI.schemeIs("about")
+ ) {
+ return false;
+ }
+ return PageThumbs.shouldStoreThumbnail(aBrowser);
+ },
+
+ _cancelDelayedCapture: function Thumbnails_cancelDelayedCapture(aBrowser) {
+ if (this._timeouts.has(aBrowser)) {
+ aBrowser.removeEventListener("scroll", this);
+ this._cancelDelayedCallbacks(aBrowser);
+ this._timeouts.delete(aBrowser);
+ }
+ },
+
+ _cancelDelayedCallbacks: function Thumbnails_cancelDelayedCallbacks(
+ aBrowser
+ ) {
+ let timeoutData = this._timeouts.get(aBrowser);
+
+ if (timeoutData.isTimeout) {
+ clearTimeout(timeoutData.id);
+ } else {
+ // idle callback dispatched
+ window.cancelIdleCallback(timeoutData.id);
+ }
+ },
+};
+
+async function getTopSiteURLs() {
+ // The _topSiteURLs getter can be expensive to run, but its return value can
+ // change frequently on new profiles, so as a compromise we cache its return
+ // value as a lazy getter for 1 minute every time it's called.
+ gBrowserThumbnails._topSiteURLsRefreshTimer = Cc[
+ "@mozilla.org/timer;1"
+ ].createInstance(Ci.nsITimer);
+ gBrowserThumbnails._topSiteURLsRefreshTimer.initWithCallback(
+ gBrowserThumbnails,
+ 60 * 1000,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ let sites = [];
+ // Get both the top sites returned by the query, and also any pinned sites
+ // that the user might have added manually that also need a screenshot.
+ // Also include top sites that don't have rich icons
+ let topSites = await NewTabUtils.activityStreamLinks.getTopSites();
+ sites.push(...topSites.filter(link => !(link.faviconSize >= 96)));
+ sites.push(...NewTabUtils.pinnedLinks.links);
+ return sites.reduce((urls, link) => {
+ if (link) {
+ urls.push(link.url);
+ }
+ return urls;
+ }, []);
+}
+
+XPCOMUtils.defineLazyGetter(gBrowserThumbnails, "_topSiteURLs", getTopSiteURLs);
diff --git a/browser/base/content/browser-toolbarKeyNav.js b/browser/base/content/browser-toolbarKeyNav.js
new file mode 100644
index 0000000000..d9e5f7aa2a
--- /dev/null
+++ b/browser/base/content/browser-toolbarKeyNav.js
@@ -0,0 +1,423 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Handle keyboard navigation for toolbars.
+ * Having separate tab stops for every toolbar control results in an
+ * unmanageable number of tab stops. Therefore, we group buttons under a single
+ * tab stop and allow movement between them using left/right arrows.
+ * However, text inputs use the arrow keys for their own purposes, so they need
+ * their own tab stop. There are also groups of buttons before and after the
+ * URL bar input which should get their own tab stop. The subsequent buttons on
+ * the toolbar are then another tab stop after that.
+ * Tab stops for groups of buttons are set using the <toolbartabstop/> element.
+ * This element is invisible, but gets included in the tab order. When one of
+ * these gets focus, it redirects focus to the appropriate button. This avoids
+ * the need to continually manage the tabindex of toolbar buttons in response to
+ * toolbarchanges.
+ * In addition to linear navigation with tab and arrows, users can also type
+ * the first (or first few) characters of a button's name to jump directly to
+ * that button.
+ */
+
+ToolbarKeyboardNavigator = {
+ // Toolbars we want to be keyboard navigable.
+ kToolbars: [CustomizableUI.AREA_NAVBAR, CustomizableUI.AREA_BOOKMARKS],
+ // Delay (in ms) after which to clear any search text typed by the user if
+ // the user hasn't typed anything further.
+ kSearchClearTimeout: 1000,
+
+ _isButton(aElem) {
+ return (
+ aElem.tagName == "toolbarbutton" || aElem.getAttribute("role") == "button"
+ );
+ },
+
+ // Get a TreeWalker which includes only controls which should be keyboard
+ // navigable.
+ _getWalker(aRoot) {
+ if (aRoot._toolbarKeyNavWalker) {
+ return aRoot._toolbarKeyNavWalker;
+ }
+
+ let filter = aNode => {
+ if (aNode.tagName == "toolbartabstop") {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+
+ // Special case for the "View site information" button, which isn't
+ // actionable in some cases but is still visible.
+ if (
+ aNode.id == "identity-box" &&
+ document.getElementById("urlbar").getAttribute("pageproxystate") ==
+ "invalid"
+ ) {
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ // Skip invisible or disabled elements.
+ if (
+ aNode.hidden ||
+ aNode.disabled ||
+ aNode.style.visibility == "hidden"
+ ) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ // This width check excludes the overflow button when there's no overflow.
+ let bounds = window.windowUtils.getBoundsWithoutFlushing(aNode);
+ if (bounds.width == 0) {
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ if (this._isButton(aNode)) {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ return NodeFilter.FILTER_SKIP;
+ };
+ aRoot._toolbarKeyNavWalker = document.createTreeWalker(
+ aRoot,
+ NodeFilter.SHOW_ELEMENT,
+ filter
+ );
+ return aRoot._toolbarKeyNavWalker;
+ },
+
+ _initTabStops(aRoot) {
+ for (let stop of aRoot.getElementsByTagName("toolbartabstop")) {
+ // These are invisible, but because they need to be in the tab order,
+ // they can't get display: none or similar. They must therefore be
+ // explicitly hidden for accessibility.
+ stop.setAttribute("aria-hidden", "true");
+ stop.addEventListener("focus", this);
+ }
+ },
+
+ init() {
+ for (let id of this.kToolbars) {
+ let toolbar = document.getElementById(id);
+ // When enabled, no toolbar buttons should themselves be tabbable.
+ // We manage toolbar focus completely. This attribute ensures that CSS
+ // doesn't set -moz-user-focus: normal.
+ toolbar.setAttribute("keyNav", "true");
+ this._initTabStops(toolbar);
+ toolbar.addEventListener("keydown", this);
+ toolbar.addEventListener("keypress", this);
+ }
+ CustomizableUI.addListener(this);
+ },
+
+ uninit() {
+ for (let id of this.kToolbars) {
+ let toolbar = document.getElementById(id);
+ for (let stop of toolbar.getElementsByTagName("toolbartabstop")) {
+ stop.removeEventListener("focus", this);
+ }
+ toolbar.removeEventListener("keydown", this);
+ toolbar.removeEventListener("keypress", this);
+ toolbar.removeAttribute("keyNav");
+ }
+ CustomizableUI.removeListener(this);
+ },
+
+ // CustomizableUI event handler
+ onWidgetAdded(aWidgetId, aArea, aPosition) {
+ if (!this.kToolbars.includes(aArea)) {
+ return;
+ }
+ let widget = document.getElementById(aWidgetId);
+ if (!widget) {
+ return;
+ }
+ this._initTabStops(widget);
+ },
+
+ _focusButton(aButton) {
+ // Toolbar buttons aren't focusable because if they were, clicking them
+ // would focus them, which is undesirable. Therefore, we must make a
+ // button focusable only when we want to focus it.
+ aButton.setAttribute("tabindex", "-1");
+ aButton.focus();
+ // We could remove tabindex now, but even though the button keeps DOM
+ // focus, a11y gets confused because the button reports as not being
+ // focusable. This results in weirdness if the user switches windows and
+ // then switches back. It also means that focus can't be restored to the
+ // button when a panel is closed. Instead, remove tabindex when the button
+ // loses focus.
+ aButton.addEventListener("blur", this);
+ },
+
+ _onButtonBlur(aEvent) {
+ if (document.activeElement == aEvent.target) {
+ // This event was fired because the user switched windows. This button
+ // will get focus again when the user returns.
+ return;
+ }
+ if (aEvent.target.getAttribute("open") == "true") {
+ // The button activated a panel. The button should remain
+ // focusable so that focus can be restored when the panel closes.
+ return;
+ }
+ aEvent.target.removeEventListener("blur", this);
+ aEvent.target.removeAttribute("tabindex");
+ },
+
+ _onTabStopFocus(aEvent) {
+ let toolbar = aEvent.target.closest("toolbar");
+ let walker = this._getWalker(toolbar);
+
+ let oldFocus = aEvent.relatedTarget;
+ if (oldFocus) {
+ // Save this because we might rewind focus and the subsequent focus event
+ // won't get a relatedTarget.
+ this._isFocusMovingBackward =
+ oldFocus.compareDocumentPosition(aEvent.target) &
+ Node.DOCUMENT_POSITION_PRECEDING;
+ if (this._isFocusMovingBackward && oldFocus && this._isButton(oldFocus)) {
+ // Shift+tabbing from a button will land on its toolbartabstop. Skip it.
+ document.commandDispatcher.rewindFocus();
+ return;
+ }
+ }
+
+ walker.currentNode = aEvent.target;
+ let button = walker.nextNode();
+ if (!button || !this._isButton(button)) {
+ // If we think we're moving backward, and focus came from outside the
+ // toolbox, we might actually have wrapped around. This currently only
+ // happens in popup windows (because in normal windows, focus first
+ // goes to the tabstrip, where we don't have tabstops). In this case,
+ // the event target was the first tabstop. If we can't find a button,
+ // e.g. because we're in a popup where most buttons are hidden, we
+ // should ensure focus keeps moving forward:
+ if (
+ oldFocus &&
+ this._isFocusMovingBackward &&
+ !gNavToolbox.contains(oldFocus)
+ ) {
+ let allStops = Array.from(
+ gNavToolbox.querySelectorAll("toolbartabstop")
+ );
+ // Find the previous toolbartabstop:
+ let earlierVisibleStopIndex = allStops.indexOf(aEvent.target) - 1;
+ // Then work out if any of the earlier ones are in a visible
+ // toolbar:
+ while (earlierVisibleStopIndex >= 0) {
+ let stopToolbar = allStops[earlierVisibleStopIndex].closest(
+ "toolbar"
+ );
+ if (
+ window.windowUtils.getBoundsWithoutFlushing(stopToolbar).height > 0
+ ) {
+ break;
+ }
+ earlierVisibleStopIndex--;
+ }
+ // If we couldn't find any earlier visible stops, we're not moving
+ // backwards, we're moving forwards and wrapped around:
+ if (earlierVisibleStopIndex == -1) {
+ this._isFocusMovingBackward = false;
+ }
+ }
+ // No navigable buttons for this tab stop. Skip it.
+ if (this._isFocusMovingBackward) {
+ document.commandDispatcher.rewindFocus();
+ } else {
+ document.commandDispatcher.advanceFocus();
+ }
+ return;
+ }
+
+ this._focusButton(button);
+ },
+
+ navigateButtons(aToolbar, aPrevious) {
+ let oldFocus = document.activeElement;
+ let walker = this._getWalker(aToolbar);
+ // Start from the current control and walk to the next/previous control.
+ walker.currentNode = oldFocus;
+ let newFocus;
+ if (aPrevious) {
+ newFocus = walker.previousNode();
+ } else {
+ newFocus = walker.nextNode();
+ }
+ if (!newFocus || newFocus.tagName == "toolbartabstop") {
+ // There are no more controls or we hit a tab stop placeholder.
+ return;
+ }
+ this._focusButton(newFocus);
+ },
+
+ _onKeyDown(aEvent) {
+ let focus = document.activeElement;
+ if (
+ aEvent.key != " " &&
+ aEvent.key.length == 1 &&
+ this._isButton(focus) &&
+ // Don't handle characters if the user is focused in a panel anchored
+ // to the toolbar.
+ !focus.closest("panel")
+ ) {
+ this._onSearchChar(aEvent.currentTarget, aEvent.key);
+ return;
+ }
+ // Anything that doesn't trigger search should clear the search.
+ this._clearSearch();
+
+ if (
+ aEvent.altKey ||
+ aEvent.controlKey ||
+ aEvent.metaKey ||
+ aEvent.shiftKey ||
+ !this._isButton(focus)
+ ) {
+ return;
+ }
+
+ switch (aEvent.key) {
+ case "ArrowLeft":
+ // Previous if UI is LTR, next if UI is RTL.
+ this.navigateButtons(aEvent.currentTarget, !window.RTL_UI);
+ break;
+ case "ArrowRight":
+ // Previous if UI is RTL, next if UI is LTR.
+ this.navigateButtons(aEvent.currentTarget, window.RTL_UI);
+ break;
+ default:
+ return;
+ }
+ aEvent.preventDefault();
+ },
+
+ _clearSearch() {
+ this._searchText = "";
+ if (this._clearSearchTimeout) {
+ clearTimeout(this._clearSearchTimeout);
+ this._clearSearchTimeout = null;
+ }
+ },
+
+ _onSearchChar(aToolbar, aChar) {
+ if (this._clearSearchTimeout) {
+ // The user just typed a character, so reset the timer.
+ clearTimeout(this._clearSearchTimeout);
+ }
+ // Convert to lower case so we can do case insensitive searches.
+ let char = aChar.toLowerCase();
+ // If the user has only typed a single character and they type the same
+ // character again, they want to move to the next item starting with that
+ // same character. Effectively, it's as if there was no existing search.
+ // In that case, we just leave this._searchText alone.
+ if (!this._searchText) {
+ this._searchText = char;
+ } else if (this._searchText != char) {
+ this._searchText += char;
+ }
+ // Clear the search if the user doesn't type anything more within the timeout.
+ this._clearSearchTimeout = setTimeout(
+ this._clearSearch.bind(this),
+ this.kSearchClearTimeout
+ );
+
+ let oldFocus = document.activeElement;
+ let walker = this._getWalker(aToolbar);
+ // Search forward after the current control.
+ walker.currentNode = oldFocus;
+ for (
+ let newFocus = walker.nextNode();
+ newFocus;
+ newFocus = walker.nextNode()
+ ) {
+ if (this._doesSearchMatch(newFocus)) {
+ this._focusButton(newFocus);
+ return;
+ }
+ }
+ // No match, so search from the start until the current control.
+ walker.currentNode = walker.root;
+ for (
+ let newFocus = walker.firstChild();
+ newFocus && newFocus != oldFocus;
+ newFocus = walker.nextNode()
+ ) {
+ if (this._doesSearchMatch(newFocus)) {
+ this._focusButton(newFocus);
+ return;
+ }
+ }
+ },
+
+ _doesSearchMatch(aElem) {
+ if (!this._isButton(aElem)) {
+ return false;
+ }
+ for (let attrib of ["aria-label", "label", "tooltiptext"]) {
+ let label = aElem.getAttribute(attrib);
+ if (!label) {
+ continue;
+ }
+ // Convert to lower case so we do a case insensitive comparison.
+ // (this._searchText is already lower case.)
+ label = label.toLowerCase();
+ if (label.startsWith(this._searchText)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ _onKeyPress(aEvent) {
+ let focus = document.activeElement;
+ if (
+ (aEvent.key != "Enter" && aEvent.key != " ") ||
+ !this._isButton(focus)
+ ) {
+ return;
+ }
+
+ if (focus.getAttribute("type") == "menu") {
+ focus.open = true;
+ } else {
+ // Several buttons specifically don't use command events; e.g. because
+ // they want to activate for middle click. Therefore, simulate a
+ // click event.
+ // If this button does handle command events, that won't trigger here.
+ // Command events have their own keyboard handling: keypress for enter
+ // and keyup for space. We rely on that behavior, since there's no way
+ // for us to reliably know what events a button handles.
+ focus.dispatchEvent(
+ new MouseEvent("click", {
+ bubbles: true,
+ ctrlKey: aEvent.ctrlKey,
+ altKey: aEvent.altKey,
+ shiftKey: aEvent.shiftKey,
+ metaKey: aEvent.metaKey,
+ })
+ );
+ }
+ // We deliberately don't call aEvent.preventDefault() here so that enter
+ // will trigger a command event handler if appropriate.
+ aEvent.stopPropagation();
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "focus":
+ this._onTabStopFocus(aEvent);
+ break;
+ case "keydown":
+ this._onKeyDown(aEvent);
+ break;
+ case "keypress":
+ this._onKeyPress(aEvent);
+ break;
+ case "blur":
+ this._onButtonBlur(aEvent);
+ break;
+ }
+ },
+};
diff --git a/browser/base/content/browser-webrtc.js b/browser/base/content/browser-webrtc.js
new file mode 100644
index 0000000000..a586bd9436
--- /dev/null
+++ b/browser/base/content/browser-webrtc.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/browser-window */
+
+/**
+ * Utility object to handle WebRTC shared tab warnings.
+ */
+var gSharedTabWarning = {
+ /**
+ * Called externally by gBrowser to determine if we're
+ * in a state such that we'd want to cancel the tab switch
+ * and show the tab switch warning panel instead.
+ *
+ * @param tab (<tab>)
+ * The tab being switched to.
+ * @returns boolean
+ * True if the panel will be shown, and the tab switch should
+ * be cancelled.
+ */
+ willShowSharedTabWarning(tab) {
+ if (!this._sharedTabWarningEnabled) {
+ return false;
+ }
+
+ let shareState = webrtcUI.getWindowShareState(window);
+ if (shareState == webrtcUI.SHARING_NONE) {
+ return false;
+ }
+
+ if (!webrtcUI.shouldShowSharedTabWarning(tab)) {
+ return false;
+ }
+
+ this._createSharedTabWarningIfNeeded();
+ let panel = document.getElementById("sharing-tabs-warning-panel");
+ let hbox = panel.firstChild;
+
+ if (shareState == webrtcUI.SHARING_SCREEN) {
+ hbox.setAttribute("type", "screen");
+ panel.setAttribute(
+ "aria-labelledby",
+ "sharing-screen-warning-panel-header-span"
+ );
+ } else {
+ hbox.setAttribute("type", "window");
+ panel.setAttribute(
+ "aria-labelledby",
+ "sharing-window-warning-panel-header-span"
+ );
+ }
+
+ let allowForSessionCheckbox = document.getElementById(
+ "sharing-warning-disable-for-session"
+ );
+ allowForSessionCheckbox.checked = false;
+
+ panel.openPopup(tab, "bottomcenter topleft", 0, 0);
+
+ return true;
+ },
+
+ /**
+ * Called by the tab switch warning panel after it has
+ * shown.
+ */
+ sharedTabWarningShown() {
+ let allowButton = document.getElementById("sharing-warning-proceed-to-tab");
+ allowButton.focus();
+ },
+
+ /**
+ * Called by the button in the tab switch warning panel
+ * to allow the switch to occur.
+ */
+ allowSharedTabSwitch() {
+ let panel = document.getElementById("sharing-tabs-warning-panel");
+ let allowForSession = document.getElementById(
+ "sharing-warning-disable-for-session"
+ ).checked;
+
+ let tab = panel.anchorNode;
+ webrtcUI.allowSharedTabSwitch(tab, allowForSession);
+ this._hideSharedTabWarning();
+ },
+
+ /**
+ * Called externally by gBrowser when a tab has been added.
+ * When this occurs, if we're sharing this window, we notify
+ * the webrtcUI module to exempt the new tab from the tab switch
+ * warning, since the user opened it while they were already
+ * sharing.
+ *
+ * @param tab (<tab>)
+ * The tab being opened.
+ */
+ tabAdded(tab) {
+ if (this._sharedTabWarningEnabled) {
+ let shareState = webrtcUI.getWindowShareState(window);
+ if (shareState != webrtcUI.SHARING_NONE) {
+ webrtcUI.tabAddedWhileSharing(tab);
+ }
+ }
+ },
+
+ get _sharedTabWarningEnabled() {
+ delete this._sharedTabWarningEnabled;
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_sharedTabWarningEnabled",
+ "privacy.webrtc.sharedTabWarning"
+ );
+ return this._sharedTabWarningEnabled;
+ },
+
+ /**
+ * Internal method for hiding the tab switch warning panel.
+ */
+ _hideSharedTabWarning() {
+ let panel = document.getElementById("sharing-tabs-warning-panel");
+ if (panel) {
+ panel.hidePopup();
+ }
+ },
+
+ /**
+ * Inserts the tab switch warning panel into the DOM
+ * if it hasn't been done already yet.
+ */
+ _createSharedTabWarningIfNeeded() {
+ // Lazy load the panel the first time we need to display it.
+ if (!document.getElementById("sharing-tabs-warning-panel")) {
+ let template = document.getElementById(
+ "sharing-tabs-warning-panel-template"
+ );
+ template.replaceWith(template.content);
+ }
+ },
+};
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
new file mode 100644
index 0000000000..8ac29cad6c
--- /dev/null
+++ b/browser/base/content/browser.css
@@ -0,0 +1,1578 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+:root,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ overflow: clip;
+}
+
+:root {
+ text-rendering: optimizeLegibility;
+ min-height: 95px;
+ min-width: 95px;
+
+ /* variables */
+ --panelui-subview-transition-duration: 150ms;
+ --lwt-additional-images: none;
+ --lwt-background-alignment: right top;
+ --lwt-background-tiling: no-repeat;
+
+ --toolbar-bgcolor: var(--toolbar-non-lwt-bgcolor);
+ --toolbar-bgimage: var(--toolbar-non-lwt-bgimage);
+ --toolbar-color: var(--toolbar-non-lwt-textcolor);
+}
+
+:root:-moz-locale-dir(rtl) {
+ direction: rtl;
+}
+
+:root:-moz-lwtheme {
+ --toolbar-bgcolor: rgba(255,255,255,.4);
+ --toolbar-bgimage: none;
+ --toolbar-color: var(--lwt-text-color, inherit);
+
+ background-color: var(--lwt-accent-color);
+ color: var(--lwt-text-color);
+}
+
+:root:-moz-lwtheme[lwtheme-image] {
+ background-image: var(--lwt-header-image) !important;
+ background-repeat: no-repeat;
+ background-position: right top !important;
+}
+
+:root:-moz-lwtheme:-moz-window-inactive {
+ background-color: var(--lwt-accent-color-inactive, var(--lwt-accent-color));
+}
+
+:root:not([chromehidden~="toolbar"]) {
+ min-width: 450px;
+}
+
+:root[customizing] {
+ min-width: -moz-fit-content;
+}
+
+/* Prevent shrinking the page content to 0 height and width */
+.browserStack {
+ min-height: 25px;
+ min-width: 25px;
+}
+
+body {
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+}
+
+/* Set additional backgrounds alignment relative to toolbox */
+
+#navigator-toolbox:-moz-lwtheme {
+ background-image: var(--lwt-additional-images);
+ background-position: var(--lwt-background-alignment);
+ background-repeat: var(--lwt-background-tiling);
+}
+
+.search-one-offs[compact=true] .search-setting-button,
+.search-one-offs:not([compact=true]) .search-setting-button-compact {
+ display: none;
+}
+
+%ifdef MENUBAR_CAN_AUTOHIDE
+#toolbar-menubar[autohide="true"] {
+ overflow: hidden;
+}
+
+#toolbar-menubar[autohide="true"][inactive="true"]:not([customizing="true"]) {
+ min-height: 0 !important;
+ height: 0 !important;
+ appearance: none !important;
+}
+%endif
+
+%ifdef XP_MACOSX
+#toolbar-menubar {
+ visibility: collapse;
+}
+%endif
+
+panelmultiview {
+ -moz-box-align: start;
+}
+
+panelmultiview[transitioning] {
+ pointer-events: none;
+}
+
+panelview {
+ -moz-box-orient: vertical;
+}
+
+panelview:not([visible]) {
+ visibility: collapse;
+}
+
+/* Hide the header when a subview is reused as a main view. */
+panelview[mainview] > .panel-header {
+ display: none;
+}
+
+.panel-viewcontainer {
+ overflow: hidden;
+}
+
+.panel-viewcontainer[panelopen] {
+ transition-property: height;
+ transition-timing-function: var(--animation-easing-function);
+ transition-duration: var(--panelui-subview-transition-duration);
+ will-change: height;
+}
+
+.panel-viewcontainer.offscreen {
+ display: block;
+}
+
+.panel-viewstack {
+ overflow: visible;
+ transition: height var(--panelui-subview-transition-duration);
+}
+
+@supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") {
+ #tabbrowser-tabs {
+ /* Without this, the tabs container width extends beyond the window width */
+ width: 0;
+ }
+ .tab-stack {
+ /* Without this, pinned tabs get a bit too tall when the tabstrip overflows. */
+ vertical-align: top;
+ }
+}
+
+@supports not -moz-bool-pref("browser.tabs.tabmanager.enabled") {
+ #tabbrowser-tabs:not([overflow="true"], [hashiddentabs]) ~ #alltabs-button {
+ display: none;
+ }
+ #tabbrowser-tabs:not([overflow="true"])[using-closing-tabs-spacer] ~ #alltabs-button {
+ /* temporary space to keep a tab's close button under the cursor */
+ display: -moz-box;
+ visibility: hidden;
+ }
+}
+
+#tabbrowser-tabs[hasadjacentnewtabbutton]:not([overflow="true"]) ~ #new-tab-button,
+#tabbrowser-tabs[overflow="true"] > #tabbrowser-arrowscrollbox > #tabs-newtab-button,
+#tabbrowser-tabs:not([hasadjacentnewtabbutton]) > #tabbrowser-arrowscrollbox > #tabs-newtab-button,
+#TabsToolbar[customizing="true"] #tabs-newtab-button {
+ display: none;
+}
+
+.tabbrowser-tab:not([pinned]) {
+ -moz-box-flex: 100;
+ max-width: 225px;
+ min-width: var(--tab-min-width);
+ width: 0;
+ transition: min-width 100ms ease-out,
+ max-width 100ms ease-out;
+}
+
+:root[uidensity=touch] .tabbrowser-tab:not([pinned]) {
+ /* Touch mode needs additional space for the close button. */
+ min-width: calc(var(--tab-min-width) + 10px);
+}
+
+.tabbrowser-tab:not([pinned], [fadein]) {
+ max-width: 0.1px;
+ min-width: 0.1px;
+ visibility: hidden;
+}
+
+.tab-icon-image[fadein],
+.tab-close-button[fadein],
+.tabbrowser-tab[fadein]::after,
+.tab-background[fadein] {
+ /* This transition is only wanted for opening tabs. */
+ transition: visibility 0ms 25ms;
+}
+
+.tab-icon-pending:not([fadein]),
+.tab-icon-image:not([fadein]),
+.tab-close-button:not([fadein]),
+.tabbrowser-tab:not([fadein])::after,
+.tab-background:not([fadein]) {
+ visibility: hidden;
+}
+
+.tab-label:not([fadein]),
+.tab-throbber:not([fadein]) {
+ display: none;
+}
+
+%ifdef NIGHTLY_BUILD
+@supports -moz-bool-pref("browser.tabs.hideThrobber") {
+ .tab-throbber {
+ display: none !important;
+ }
+}
+%endif
+
+#tabbrowser-tabs[positionpinnedtabs] > #tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned] {
+ position: fixed !important;
+ display: block;
+}
+
+#tabbrowser-tabs[movingtab] > #tabbrowser-arrowscrollbox > .tabbrowser-tab[selected],
+#tabbrowser-tabs[movingtab] > #tabbrowser-arrowscrollbox > .tabbrowser-tab[multiselected] {
+ position: relative;
+ z-index: 2;
+ pointer-events: none; /* avoid blocking dragover events on scroll buttons */
+}
+
+.tabbrowser-tab[tab-grouping],
+.tabbrowser-tab[tabdrop-samewindow] {
+ transition: transform 200ms var(--animation-easing-function);
+}
+
+.tabbrowser-tab[tab-grouping][multiselected]:not([selected]) {
+ z-index: 2;
+}
+
+/* Make it easier to drag tabs by expanding the drag area downwards. */
+#tabbrowser-tabs[movingtab] {
+ padding-bottom: 15px;
+ margin-bottom: -15px;
+}
+
+#navigator-toolbox[movingtab] > #nav-bar {
+ pointer-events: none;
+}
+
+/* Allow dropping a tab on buttons with associated drop actions. */
+#navigator-toolbox[movingtab] > #nav-bar > #nav-bar-customization-target > #personal-bookmarks,
+#navigator-toolbox[movingtab] > #nav-bar > #nav-bar-customization-target > #home-button,
+#navigator-toolbox[movingtab] > #nav-bar > #nav-bar-customization-target > #downloads-button,
+#navigator-toolbox[movingtab] > #nav-bar > #nav-bar-customization-target > #bookmarks-menu-button {
+ pointer-events: auto;
+}
+
+/* The address bar needs to be able to render outside of the toolbar, but as
+ * long as it's within the toolbar's bounds we can clip the toolbar so that the
+ * rendering pipeline doesn't reserve an enormous texture for it. */
+#nav-bar:not([urlbar-exceeds-toolbar-bounds]),
+/* When customizing, overflowable toolbars move automatically moved items back
+ * from the overflow menu, but we still don't want to render them outside of
+ * the customization target. */
+toolbar[overflowable][customizing] > .customization-target {
+ overflow: clip;
+}
+
+toolbar:not([overflowing]) > .overflow-button,
+toolbar[customizing] > .overflow-button {
+ display: none;
+}
+
+toolbar[customizing] #ion-button,
+toolbar[customizing] #whats-new-menu-button {
+ display: none;
+}
+
+:root:not([chromehidden~="toolbar"]) #nav-bar[nonemptyoverflow] > .overflow-button,
+#nav-bar[customizing] > .overflow-button {
+ display: -moz-box;
+}
+
+/* The ids are ugly, but this should be reasonably performant, and
+ * using a tagname as the last item would be less so.
+ */
+#widget-overflow-list:empty + #widget-overflow-fixed-separator,
+#widget-overflow:not([hasfixeditems]) #widget-overflow-fixed-separator {
+ display: none;
+}
+
+
+%ifdef MENUBAR_CAN_AUTOHIDE
+:root:not([chromehidden~="menubar"]) #toolbar-menubar:not([autohide=true]) + #TabsToolbar > .titlebar-buttonbox-container,
+:root:not([chromehidden~="menubar"]) #toolbar-menubar:not([autohide=true]) + #TabsToolbar .titlebar-spacer,
+%endif
+%ifndef MOZ_WIDGET_COCOA
+%ifndef MOZ_WIDGET_GTK
+:root:not([sizemode=normal]) .titlebar-spacer[type="pre-tabs"],
+%endif
+%endif
+:root:not([chromemargin]) .titlebar-buttonbox-container,
+:root[inFullscreen] .titlebar-buttonbox-container,
+:root[inFullscreen] .titlebar-spacer,
+:root:not([tabsintitlebar]) .titlebar-spacer {
+ display: none;
+}
+%ifdef MOZ_WIDGET_GTK
+@media (-moz-gtk-csd-reversed-placement: 0) {
+ :root:not([sizemode=normal]) .titlebar-spacer[type="pre-tabs"],
+ :root[gtktiledwindow=true] .titlebar-spacer[type="pre-tabs"] {
+ display: none;
+ }
+}
+@media (-moz-gtk-csd-reversed-placement) {
+ :root:not([sizemode=normal]) .titlebar-spacer[type="post-tabs"],
+ :root[gtktiledwindow=true] .titlebar-spacer[type="post-tabs"] {
+ display: none;
+ }
+}
+%endif
+
+:root:not([sizemode=maximized]) .titlebar-restore,
+:root[sizemode=maximized] .titlebar-max {
+ display: none;
+}
+
+%ifdef MENUBAR_CAN_AUTOHIDE
+#toolbar-menubar[autohide=true]:not([inactive]) + #TabsToolbar > .titlebar-buttonbox-container {
+ visibility: hidden;
+}
+%endif
+
+#titlebar {
+ -moz-window-dragging: drag;
+}
+
+:root[tabsintitlebar] .titlebar-buttonbox {
+ position: relative;
+}
+
+:root:not([tabsintitlebar]) .titlebar-buttonbox {
+ display: none;
+}
+
+.titlebar-buttonbox {
+ appearance: auto;
+ -moz-default-appearance: -moz-window-button-box;
+ position: relative;
+}
+
+#personal-toolbar-empty-description {
+ -moz-window-dragging: no-drag;
+}
+
+#personal-bookmarks {
+ -moz-window-dragging: inherit;
+}
+
+toolbarpaletteitem {
+ -moz-window-dragging: no-drag;
+ -moz-box-pack: start;
+}
+
+.titlebar-buttonbox-container {
+ -moz-box-ordinal-group: 1000;
+}
+
+%ifdef XP_MACOSX
+#titlebar-fullscreen-button {
+ appearance: auto;
+ -moz-default-appearance: -moz-mac-fullscreen-button;
+}
+
+/**
+ * On macOS, the window caption buttons are on the left side of the window titlebar,
+ * even when using the RTL UI direction. Similarly, the fullscreen button is on the
+ * right side of the window titlebar, even when using the RTL UI direction. These
+ * next rules enforce that ordering.
+ */
+#titlebar-secondary-buttonbox:-moz-locale-dir(ltr) {
+ -moz-box-ordinal-group: 1000;
+}
+
+#titlebar-secondary-buttonbox:-moz-locale-dir(rtl),
+.titlebar-buttonbox-container:-moz-locale-dir(ltr) {
+ -moz-box-ordinal-group: 0;
+}
+%endif
+
+:root[inDOMFullscreen] #navigator-toolbox,
+:root[inDOMFullscreen] #fullscr-toggler,
+:root[inDOMFullscreen] #sidebar-box,
+:root[inDOMFullscreen] #sidebar-splitter,
+:root[inFullscreen]:not([OSXLionFullscreen]) toolbar:not([fullscreentoolbar=true]),
+:root[inFullscreen] .global-notificationbox {
+ visibility: collapse;
+}
+
+#navigator-toolbox[fullscreenShouldAnimate] {
+ transition: 0.8s margin-top ease-out;
+}
+
+/* Rules to help integrate WebExtension buttons */
+
+.webextension-browser-action > .toolbarbutton-badge-stack > .toolbarbutton-icon {
+ height: 16px;
+ width: 16px;
+}
+
+@media not all and (min-resolution: 1.1dppx) {
+ .webextension-browser-action {
+ list-style-image: var(--webextension-toolbar-image, inherit);
+ }
+
+ toolbar[brighttext] .webextension-browser-action {
+ list-style-image: var(--webextension-toolbar-image-light, inherit);
+ }
+
+ toolbar:not([brighttext]) .webextension-browser-action:-moz-lwtheme {
+ list-style-image: var(--webextension-toolbar-image-dark, inherit);
+ }
+
+ .webextension-browser-action[cui-areatype="menu-panel"] {
+ list-style-image: var(--webextension-menupanel-image, inherit);
+ }
+
+ :root[lwt-popup-brighttext] .webextension-browser-action[cui-areatype="menu-panel"] {
+ list-style-image: var(--webextension-menupanel-image-light, inherit);
+ }
+
+ :root:not([lwt-popup-brighttext]) .webextension-browser-action[cui-areatype="menu-panel"]:-moz-lwtheme {
+ list-style-image: var(--webextension-menupanel-image-dark, inherit);
+ }
+
+ .webextension-menuitem {
+ list-style-image: var(--webextension-menuitem-image, inherit) !important;
+ }
+}
+
+@media (min-resolution: 1.1dppx) {
+ .webextension-browser-action {
+ list-style-image: var(--webextension-toolbar-image-2x, inherit);
+ }
+
+ toolbar[brighttext] .webextension-browser-action {
+ list-style-image: var(--webextension-toolbar-image-2x-light, inherit);
+ }
+
+ toolbar:not([brighttext]) .webextension-browser-action:-moz-lwtheme {
+ list-style-image: var(--webextension-toolbar-image-2x-dark, inherit);
+ }
+
+ .webextension-browser-action[cui-areatype="menu-panel"] {
+ list-style-image: var(--webextension-menupanel-image-2x, inherit);
+ }
+
+ :root[lwt-popup-brighttext] .webextension-browser-action[cui-areatype="menu-panel"] {
+ list-style-image: var(--webextension-menupanel-image-2x-light, inherit);
+ }
+
+ :root:not([lwt-popup-brighttext]) .webextension-browser-action[cui-areatype="menu-panel"]:-moz-lwtheme {
+ list-style-image: var(--webextension-menupanel-image-2x-dark, inherit);
+ }
+
+ .webextension-menuitem {
+ list-style-image: var(--webextension-menuitem-image-2x, inherit) !important;
+ }
+}
+
+toolbarbutton.webextension-menuitem > .toolbarbutton-icon {
+ width: 16px;
+ height: 16px;
+}
+
+toolbarpaletteitem[removable="false"] {
+ opacity: 0.5;
+}
+
+%ifndef XP_MACOSX
+toolbarpaletteitem[place="palette"],
+toolbarpaletteitem[place="menu-panel"],
+toolbarpaletteitem[place="toolbar"] {
+ -moz-user-focus: normal;
+}
+%endif
+
+#bookmarks-toolbar-placeholder,
+#bookmarks-toolbar-button,
+toolbarpaletteitem > #personal-bookmarks > #PlacesToolbar,
+#personal-bookmarks:is([overflowedItem=true], [cui-areatype="menu-panel"]) > #PlacesToolbar {
+ display: none;
+}
+
+toolbarpaletteitem[place="toolbar"] > #personal-bookmarks > #bookmarks-toolbar-placeholder,
+toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-button,
+#personal-bookmarks:is([overflowedItem=true], [cui-areatype="menu-panel"]) > #bookmarks-toolbar-button {
+ display: -moz-box;
+}
+
+#personal-bookmarks {
+ position: relative;
+}
+
+#PlacesToolbarDropIndicatorHolder {
+ display: block;
+ position: absolute;
+}
+
+#nav-bar-customization-target > #personal-bookmarks,
+toolbar:not(#TabsToolbar) > #wrapper-personal-bookmarks,
+toolbar:not(#TabsToolbar) > #personal-bookmarks {
+ -moz-box-flex: 1;
+}
+
+#zoom-controls[cui-areatype="toolbar"]:not([overflowedItem=true]) > #zoom-reset-button > .toolbarbutton-text {
+ display: -moz-box;
+}
+
+#reload-button:not([displaystop]) + #stop-button,
+#reload-button[displaystop] {
+ display: none;
+}
+
+/* The reload-button is only disabled temporarily when it becomes visible
+ to prevent users from accidentally clicking it. We don't however need
+ to show this disabled state, as the flicker that it generates is short
+ enough to be visible but not long enough to explain anything to users. */
+#reload-button[disabled]:not(:-moz-window-inactive) > .toolbarbutton-icon {
+ opacity: 1 !important;
+}
+
+/* Ensure stop-button and reload-button are displayed correctly when in the overflow menu */
+.widget-overflow-list > #stop-reload-button > .toolbarbutton-1 {
+ -moz-box-flex: 1;
+}
+
+%ifdef XP_MACOSX
+:root[inFullscreen="true"] {
+ padding-top: 0; /* override drawintitlebar="true" */
+}
+%endif
+
+/* Hide menu elements intended for keyboard access support */
+#main-menubar[openedwithkey=false] .show-only-for-keyboard {
+ display: none;
+}
+
+/* ::::: location bar & search bar ::::: */
+
+#urlbar,
+#searchbar {
+ /* Setting a width and min-width to let the location & search bars maintain
+ a constant width in case they haven't be resized manually. (bug 965772) */
+ width: 1px;
+ min-width: 1px;
+}
+
+/* Align URLs to the right in RTL mode. */
+#urlbar-input:-moz-locale-dir(rtl) {
+ text-align: right !important;
+}
+
+/* Make sure that the location bar's alignment changes according
+ to the input box direction if the user switches the text direction using
+ cmd_switchTextDirection (which applies a dir attribute to the <input>). */
+#urlbar-input[dir=ltr]:-moz-locale-dir(rtl) {
+ text-align: left !important;
+}
+
+#urlbar-input[dir=rtl]:-moz-locale-dir(ltr) {
+ text-align: right !important;
+}
+
+/*
+ * Display visual cue that browser is under remote control.
+ * This is to help users visually distinguish a user agent session that
+ * is under remote control from those used for normal browsing sessions.
+ *
+ * Attribute is controlled by browser.js:/gRemoteControl.
+ */
+:root[remotecontrol] #urlbar-background {
+ background: repeating-linear-gradient(
+ -45deg,
+ transparent,
+ transparent 25px,
+ rgba(255,255,255,.3) 25px,
+ rgba(255,255,255,.3) 50px);
+ background-color: rgba(255,170,68,.8);
+ color: black;
+}
+
+/* Show the url scheme in a static box when overflowing to the left */
+.urlbar-input-box {
+ position: relative;
+ direction: ltr;
+}
+
+#urlbar-scheme {
+ position: absolute;
+ height: 100%;
+ visibility: hidden;
+ direction: ltr;
+ pointer-events: none;
+}
+
+#urlbar-input {
+ mask-repeat: no-repeat;
+ unicode-bidi: plaintext;
+ text-align: match-parent;
+}
+
+#urlbar:not([focused])[domaindir="ltr"]> #urlbar-input-container > .urlbar-input-box > #urlbar-input {
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+/* The following rules apply overflow masks to the unfocused urlbar
+ This mask may be overriden when a Contextual Feature Recommendation is shown,
+ see browser/themes/shared/urlbar-searchbar.inc.css for details */
+
+#urlbar:not([focused])[textoverflow="both"] > #urlbar-input-container > .urlbar-input-box > #urlbar-input {
+ mask-image: linear-gradient(to right, transparent, black 3ch, black calc(100% - 3ch), transparent);
+}
+#urlbar:not([focused])[textoverflow="right"] > #urlbar-input-container > .urlbar-input-box > #urlbar-input {
+ mask-image: linear-gradient(to left, transparent, black 3ch);
+}
+#urlbar:not([focused])[textoverflow="left"] > #urlbar-input-container > .urlbar-input-box > #urlbar-input {
+ mask-image: linear-gradient(to right, transparent, black 3ch);
+}
+
+/* The protocol is visible if there is an RTL domain and we overflow to the left.
+ Uses the required-valid trick to check if it contains a value */
+#urlbar:not([focused])[textoverflow="left"][domaindir="rtl"] > #urlbar-input-container > .urlbar-input-box > #urlbar-scheme:valid {
+ visibility: visible;
+}
+#urlbar:not([focused])[textoverflow="left"][domaindir="rtl"] > #urlbar-input-container > .urlbar-input-box > #urlbar-input {
+ mask-image: linear-gradient(to right, transparent var(--urlbar-scheme-size), black calc(var(--urlbar-scheme-size) + 3ch));
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ .searchbar-engine-image {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+#urlbar[actiontype="switchtab"][actionoverride] > #urlbar-input-container > #urlbar-label-box,
+#urlbar:not([actiontype="switchtab"], [actiontype="extension"], [searchmode]) > #urlbar-input-container > #urlbar-label-box,
+#urlbar:not([actiontype="switchtab"]) > #urlbar-input-container > #urlbar-label-box > #urlbar-label-switchtab,
+#urlbar:not([actiontype="extension"]) > #urlbar-input-container > #urlbar-label-box > #urlbar-label-extension,
+#urlbar[searchmode][breakout-extend] > #urlbar-input-container > #urlbar-label-box,
+#urlbar:not([searchmode]) > #urlbar-input-container > #urlbar-label-box > #urlbar-label-search-mode,
+#urlbar[breakout-extend] > #urlbar-input-container > #urlbar-label-box > #urlbar-label-search-mode {
+ display: none;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginsFooter"] {
+ -moz-box-pack: center;
+ color: FieldText;
+ min-height: 2.6666em;
+ border-top: 1px solid rgba(38,38,38,.15);
+ background-color: hsla(0,0%,80%,.35); /* match arrowpanel-dimmed */;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginsFooter"]:hover,
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginsFooter"][selected] {
+ background-color: hsla(0,0%,80%,.5); /* match arrowpanel-dimmed-further */
+}
+
+/* Define the minimum width based on the style of result rows.
+ The order of the min-width rules below must be in increasing order. */
+#PopupAutoComplete[resultstyles~="loginsFooter"],
+#PopupAutoComplete[resultstyles~="insecureWarning"] {
+ min-width: 17em;
+}
+
+#PopupAutoComplete[resultstyles~="importableLogins"],
+#PopupAutoComplete[resultstyles~="generatedPassword"] {
+ min-width: 22em;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
+ height: auto;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .ac-site-icon,
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-site-icon {
+ margin-inline-start: 0;
+ display: initial;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > .ac-text-overflow-container > .ac-title-text {
+ text-overflow: initial;
+ white-space: initial;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > label {
+ margin-inline-start: 0;
+}
+
+#urlbar-input-container[pageproxystate=invalid] > #page-action-buttons > .urlbar-page-action,
+#identity-box.chromeUI ~ #page-action-buttons > .urlbar-page-action:not(#star-button-box),
+#urlbar[usertyping] > #urlbar-input-container > #page-action-buttons > #urlbar-zoom-button,
+#urlbar:not([usertyping]) > #urlbar-input-container > #urlbar-go-button,
+#urlbar:not([focused]) > #urlbar-input-container > #urlbar-go-button {
+ display: none;
+}
+
+#nav-bar:not([keyNav=true]) #identity-box,
+#nav-bar:not([keyNav=true]) #tracking-protection-icon-container {
+ -moz-user-focus: normal;
+}
+
+/* We leave 350px plus whatever space the download button will need when it
+ * appears. Normally this should be 16px for the icon, plus 2 * 2px padding
+ * plus the toolbarbutton-inner-padding. We're adding 4px to ensure things
+ * like rounding on hidpi don't accidentally result in the button going
+ * into overflow.
+ */
+ #urlbar-container {
+ min-width: calc(350px + 24px + 2 * var(--toolbarbutton-inner-padding));
+}
+
+#nav-bar[downloadsbuttonshown] #urlbar-container {
+ min-width: 350px;
+}
+
+/* Customize mode is difficult to use at moderate window width if the Urlbar
+ remains 350px wide. */
+:root[customizing] #urlbar-container {
+ min-width: 280px;
+}
+
+#identity-box {
+ max-width: calc(30px + 10em);
+}
+
+@media (max-width: 770px) {
+ #urlbar-container {
+ min-width: calc(280px + 24px + 2 * var(--toolbarbutton-inner-padding));
+ }
+ #nav-bar[downloadsbuttonshown] #urlbar-container {
+ min-width: 280px;
+ }
+ :root[customizing] #urlbar-container {
+ min-width: 245px;
+ }
+ #identity-box {
+ max-width: 80px;
+ }
+ /* Contenxtual identity labels are user-customizable and can be very long,
+ so we only show the colored icon when the window gets small. */
+ #userContext-label {
+ display: none;
+ }
+}
+/* 680px is just below half of popular 1366px wide screens, so when putting two
+ browser windows next to each other on such a screen, they'll be above this
+ threshold. */
+@media (max-width: 680px) {
+ /* Page action buttons are duplicated in the page action menu so we can
+ safely hide them in small windows. */
+ #pageActionSeparator,
+ #pageActionButton ~ .urlbar-page-action {
+ display: none;
+ }
+}
+@media (max-width: 550px) {
+ #urlbar-container {
+ min-width: calc(225px + 24px + 2 * var(--toolbarbutton-inner-padding));
+ }
+ #nav-bar[downloadsbuttonshown] #urlbar-container {
+ min-width: 225px;
+ }
+ #identity-box {
+ max-width: 70px;
+ }
+ #urlbar-zoom-button {
+ display: none;
+ }
+}
+
+/* Flexible spacer sizing (gets overridden in the navbar) */
+toolbarpaletteitem[place=toolbar][id^=wrapper-customizableui-special-spring],
+toolbarspring {
+ -moz-box-flex: 1;
+ min-width: 28px;
+ max-width: 112px;
+}
+
+#nav-bar toolbarpaletteitem[id^=wrapper-customizableui-special-spring],
+#nav-bar toolbarspring {
+ -moz-box-flex: 80;
+ /* We shrink the flexible spacers, but not to nothing so they can be
+ * manipulated in customize mode; the next rule shrinks them further
+ * outside customize mode. */
+ min-width: 10px;
+}
+
+#nav-bar:not([customizing]) toolbarspring {
+ min-width: 1px;
+}
+
+#widget-overflow-list > toolbarspring {
+ display: none;
+}
+
+/* ::::: Unified Back-/Forward Button ::::: */
+.unified-nav-current {
+ font-weight: bold;
+}
+
+.bookmark-item > label {
+ /* ensure we use the direction of the bookmarks label instead of the
+ * browser locale */
+ unicode-bidi: plaintext;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ .menuitem-with-favicon > .menu-iconic-left > .menu-iconic-icon {
+ image-rendering: -moz-crisp-edges;
+ }
+
+ .bookmark-item > .toolbarbutton-icon,
+ .bookmark-item > .menu-iconic-left > .menu-iconic-icon {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+menupopup[emptyplacesresult="true"] > .hide-if-empty-places-result {
+ display: none;
+}
+
+/* Hide extension toolbars that neglected to set the proper class */
+:root[chromehidden~="location"][chromehidden~="toolbar"] toolbar:not(.chromeclass-menubar) {
+ display: none;
+}
+
+#navigator-toolbox ,
+#mainPopupSet {
+ min-width: 1px;
+}
+
+/* History Swipe Animation */
+
+#historySwipeAnimationContainer {
+ overflow: hidden;
+ pointer-events: none;
+}
+
+#historySwipeAnimationPreviousArrow {
+ background: url("chrome://browser/content/history-swipe-arrow.svg")
+ center left / 64px 128px no-repeat transparent;
+}
+#historySwipeAnimationNextArrow {
+ background: url("chrome://browser/content/history-swipe-arrow.svg")
+ center left / 64px 128px no-repeat transparent;
+ transform: rotate(180deg);
+}
+
+/* Pocket */
+:root[pocketdisabled=true] #context-pocket,
+:root[pocketdisabled=true] #context-savelinktopocket,
+:root[pocketdisabled=true] #appMenu-library-pocket-button {
+ display: none;
+}
+
+/* Full Screen UI */
+
+#fullscr-toggler {
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ position: fixed;
+ z-index: 2147483647;
+}
+
+#fullscreen-and-pointerlock-wrapper {
+ position: fixed;
+ width: 100vw;
+ height: 100vh;
+ top: 0;
+ pointer-events: none;
+}
+
+.pointerlockfswarning {
+ position: fixed;
+ z-index: 2147483647 !important;
+ visibility: visible;
+ transition: transform 300ms ease-in;
+ /* To center the warning box horizontally,
+ we use left: 50% with translateX(-50%). */
+ top: 0; left: 50%;
+ transform: translate(-50%, -100%);
+ box-sizing: border-box;
+ width: max-content;
+ max-width: 95%;
+ pointer-events: none;
+}
+.pointerlockfswarning:not([hidden]) {
+ display: flex;
+ will-change: transform;
+}
+.pointerlockfswarning[onscreen] {
+ transform: translate(-50%, 50px);
+}
+.pointerlockfswarning[ontop] {
+ /* Use -10px to hide the border and border-radius on the top */
+ transform: translate(-50%, -10px);
+}
+:root[OSXLionFullscreen] .pointerlockfswarning[ontop] {
+ transform: translate(-50%, 80px);
+}
+
+.pointerlockfswarning-domain-text,
+.pointerlockfswarning-generic-text {
+ word-wrap: break-word;
+ /* We must specify a min-width, otherwise word-wrap:break-word doesn't work. Bug 630864. */
+ min-width: 1px
+}
+.pointerlockfswarning-domain-text:not([hidden]) + .pointerlockfswarning-generic-text {
+ display: none;
+}
+
+#fullscreen-exit-button {
+ pointer-events: auto;
+}
+
+/* notification anchors should only be visible when their associated
+ notifications are */
+#nav-bar:not([keyNav=true]) .notification-anchor-icon {
+ -moz-user-focus: normal;
+}
+
+#blocked-permissions-container > .blocked-permission-icon:not([showing]),
+.notification-anchor-icon:not([showing]) {
+ display: none;
+}
+
+#invalid-form-popup > description {
+ max-width: 280px;
+}
+
+.popup-anchor {
+ /* should occupy space but not be visible */
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+}
+
+browser[tabmodalPromptShowing], browser[tabDialogShowing] {
+ -moz-user-focus: none !important;
+}
+
+/* Status panel */
+
+#statuspanel {
+ display: block;
+ position: fixed;
+ margin-top: -3em;
+ max-width: calc(100% - 5px);
+ pointer-events: none;
+}
+
+#statuspanel[mirror] {
+ inset-inline-start: auto;
+ inset-inline-end: 0;
+}
+
+#statuspanel[sizelimit] {
+ max-width: 50%;
+}
+
+#statuspanel[type=status] {
+ min-width: 23em;
+}
+
+@media all and (max-width: 800px) {
+ #statuspanel[type=status] {
+ min-width: 33%;
+ }
+}
+
+#statuspanel[type=overLink] {
+ transition: opacity 120ms ease-out, visibility 120ms;
+}
+
+#statuspanel:is([type=overLink], [inactive][previoustype=overLink]) > #statuspanel-inner {
+ direction: ltr;
+}
+
+#statuspanel[inactive] {
+ transition: none;
+ opacity: 0;
+ visibility: hidden;
+}
+
+#statuspanel[inactive][previoustype=overLink] {
+ transition: opacity 200ms ease-out, visibility 200ms;
+}
+
+#statuspanel-inner {
+ height: 3em;
+ width: 100%;
+ -moz-box-align: end;
+}
+
+/*** Visibility of downloads indicator controls ***/
+
+/* Bug 924050: If we've loaded the indicator, for now we hide it in the menu panel,
+ and just show the icon. This is a hack to side-step very weird layout bugs that
+ seem to be caused by the indicator stack interacting with the menu panel. */
+#downloads-button[indicator]:not([cui-areatype="menu-panel"]) > .toolbarbutton-badge-stack > image.toolbarbutton-icon,
+#downloads-button[indicator][cui-areatype="menu-panel"] > .toolbarbutton-badge-stack > #downloads-indicator-anchor {
+ display: none;
+}
+
+toolbarpaletteitem[place="palette"] > #downloads-button[indicator] > .toolbarbutton-badge-stack > image.toolbarbutton-icon {
+ display: -moz-box;
+}
+
+toolbarpaletteitem[place="palette"] > #downloads-button[indicator] > .toolbarbutton-badge-stack > #downloads-indicator-anchor {
+ display: none;
+}
+
+/* Combobox dropdown renderer */
+#ContentSelectDropdown > menupopup {
+ /* The menupopup itself should always be rendered LTR to ensure the scrollbar aligns with
+ * the dropdown arrow on the dropdown widget. If a menuitem is RTL, its style will be set accordingly */
+ direction: ltr;
+}
+
+#ContentSelectDropdown > menupopup::part(arrowscrollbox-scrollbox) {
+ scrollbar-width: var(--content-select-scrollbar-width, auto);
+}
+
+/* Indent options in optgroups */
+.contentSelectDropdown-ingroup .menu-iconic-text {
+ padding-inline-start: 2em;
+}
+
+/* Give this menupopup an arrow panel styling */
+#BMB_bookmarksPopup {
+ appearance: none;
+ background: transparent;
+ border: none;
+ /* The popup inherits -moz-image-region from the button, must reset it */
+ -moz-image-region: auto;
+}
+
+@supports -moz-bool-pref("xul.panel-animations.enabled") {
+@media (prefers-reduced-motion: no-preference) {
+%ifdef MOZ_WIDGET_COCOA
+ /* On Mac, use the properties "-moz-window-transform" and "-moz-window-opacity"
+ instead of "transform" and "opacity" for these animations.
+ The -moz-window* properties apply to the whole window including the window's
+ shadow, and they don't affect the window's "shape", so the system doesn't
+ have to recompute the shadow shape during the animation. This makes them a
+ lot faster. In fact, Gecko no longer triggers shadow shape recomputations
+ for repaints.
+ These properties are not implemented on other platforms. */
+ #BMB_bookmarksPopup:not([animate="false"]) {
+ transition-property: -moz-window-transform, -moz-window-opacity;
+ transition-duration: 0.18s, 0.18s;
+ transition-timing-function:
+ var(--animation-easing-function), ease-out;
+ }
+
+ /* Only do the fade-in animation on pre-Big Sur to avoid missing shadows on
+ * Big Sur, see bug 1672091. */
+ @media (-moz-mac-big-sur-theme: 0) {
+ #BMB_bookmarksPopup:not([animate="false"]) {
+ -moz-window-opacity: 0;
+ -moz-window-transform: translateY(-70px);
+ }
+
+ #BMB_bookmarksPopup[side="bottom"]:not([animate="false"]) {
+ -moz-window-transform: translateY(70px);
+ }
+ }
+
+ /* [animate] is here only so that this rule has greater specificity than the
+ * rule right above */
+ #BMB_bookmarksPopup[animate][animate="open"] {
+ -moz-window-opacity: 1.0;
+ transition-duration: 0.18s, 0.18s;
+ -moz-window-transform: none;
+ transition-timing-function:
+ var(--animation-easing-function), ease-in-out;
+ }
+
+ #BMB_bookmarksPopup[animate][animate="cancel"] {
+ -moz-window-opacity: 0;
+ -moz-window-transform: none;
+ }
+%else
+ #BMB_bookmarksPopup:not([animate="false"]) {
+ opacity: 0;
+ transform: translateY(-70px);
+ transition-property: transform, opacity;
+ transition-duration: 0.18s, 0.18s;
+ transition-timing-function:
+ var(--animation-easing-function), ease-out;
+ }
+
+ #BMB_bookmarksPopup[side="bottom"]:not([animate="false"]) {
+ transform: translateY(70px);
+ }
+
+ /* [animate] is here only so that this rule has greater specificity than the
+ * rule right above */
+ #BMB_bookmarksPopup[animate][animate="open"] {
+ opacity: 1.0;
+ transition-duration: 0.18s, 0.18s;
+ transform: none;
+ transition-timing-function:
+ var(--animation-easing-function), ease-in-out;
+ }
+
+ #BMB_bookmarksPopup[animate][animate="cancel"] {
+ transform: none;
+ }
+%endif
+}
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ .PanelUI-remotetabs-clientcontainer > toolbarbutton > .toolbarbutton-icon,
+ #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
+ #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
+ #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+#customization-container {
+ -moz-box-orient: horizontal;
+ flex-direction: column;
+}
+
+#customization-container:not([hidden]) {
+ /* In a separate rule to avoid 'display:flex' causing the node to be
+ * displayed while the container is still hidden. */
+ display: flex;
+}
+
+#customization-content-container {
+ display: flex;
+ flex-grow: 1; /* Grow so there isn't empty space below the footer */
+ min-height: 0; /* Allow this to shrink so the footer doesn't get pushed out. */
+}
+
+#customization-panelHolder {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+#customization-panelHolder > #widget-overflow-fixed-list {
+ flex: 1 1 auto; /* Grow within the available space, and allow ourselves to shrink */
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+#customization-panelWrapper,
+#customization-panelWrapper > .panel-arrowcontent,
+#customization-panelHolder {
+ flex-direction: column;
+ display: flex;
+ min-height: calc(174px + 9em);
+}
+
+#customization-panelWrapper {
+ flex: 1 1 auto;
+ height: 0; /* Don't let my contents affect ancestors' content-based sizing */
+ align-items: end; /* align to the end on the cross-axis (affects arrow) */
+}
+
+#customization-panelWrapper,
+#customization-panelWrapper > .panel-arrowcontent {
+ -moz-box-flex: 1;
+}
+
+#customization-panel-container {
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ flex: none;
+}
+
+toolbarpaletteitem[dragover] {
+ border-inline-color: transparent;
+}
+
+#customization-palette-container {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+}
+
+#customization-palette:not([hidden]) {
+ display: block;
+ flex: 1 1 auto;
+ overflow: auto;
+ min-height: 3em;
+}
+
+#customization-footer-spacer,
+#customization-spacer {
+ flex: 1 1 auto;
+}
+
+#customization-footer {
+ display: flex;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+#customization-toolbar-visibility-button > .box-inherit > .button-menu-dropmarker {
+ display: -moz-box;
+}
+
+toolbarpaletteitem[place="palette"] {
+ -moz-box-orient: vertical;
+ width: 7em;
+ max-width: 7em;
+ /* icon (16) + margin (9 + 12) + 3 lines of text: */
+ height: calc(39px + 3em);
+ margin-bottom: 5px;
+ margin-inline-end: 24px;
+ overflow: visible;
+ display: inline-block;
+ vertical-align: top;
+}
+
+toolbarpaletteitem[place="palette"][hidden] {
+ display: none;
+}
+
+toolbarpaletteitem > toolbarbutton,
+toolbarpaletteitem > toolbaritem {
+ /* Prevent children from getting events */
+ pointer-events: none;
+ -moz-box-pack: center;
+}
+
+:root[customizing=true] .addon-banner-item,
+:root[customizing=true] .panel-banner-item {
+ display: none;
+}
+
+/* UI Tour */
+
+@keyframes uitour-wobble {
+ from {
+ transform: rotate(0deg) translateX(3px) rotate(0deg);
+ }
+ 50% {
+ transform: rotate(360deg) translateX(3px) rotate(-360deg);
+ }
+ to {
+ transform: rotate(720deg) translateX(0px) rotate(-720deg);
+ }
+}
+
+@keyframes uitour-zoom {
+ from {
+ transform: scale(0.8);
+ }
+ 50% {
+ transform: scale(1.0);
+ }
+ to {
+ transform: scale(0.8);
+ }
+}
+
+@keyframes uitour-color {
+ from {
+ border-color: #5B9CD9;
+ }
+ 50% {
+ border-color: #FF0000;
+ }
+ to {
+ border-color: #5B9CD9;
+ }
+}
+
+#UITourHighlightContainer,
+#UITourHighlight {
+ pointer-events: none;
+}
+
+#UITourHighlight[active] {
+ animation-delay: 2s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+}
+
+#UITourHighlight[active="wobble"] {
+ animation-name: uitour-wobble;
+ animation-delay: 0s;
+ animation-duration: 1.5s;
+ animation-iteration-count: 1;
+}
+#UITourHighlight[active="zoom"] {
+ animation-name: uitour-zoom;
+ animation-duration: 1s;
+}
+#UITourHighlight[active="color"] {
+ animation-name: uitour-color;
+ animation-duration: 2s;
+}
+
+/* Combined context-menu items */
+#context-navigation > .menuitem-iconic > .menu-iconic-text,
+#context-navigation > .menuitem-iconic > .menu-accel-container {
+ display: none;
+}
+
+.popup-notification-invalid-input {
+ box-shadow: 0 0 1.5px 1px red;
+}
+
+.popup-notification-invalid-input[focused] {
+ box-shadow: 0 0 2px 2px rgba(255,0,0,0.4);
+}
+
+.popup-notification-description[popupid=webauthn-prompt-register-direct] {
+ white-space: pre-line;
+}
+
+.dragfeedback-tab {
+ appearance: none;
+ opacity: 0.65;
+ -moz-window-shadow: none;
+}
+
+/* Page action panel */
+#pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > .pageAction-sendToDevice-notReady,
+#pageAction-urlbar-sendToDevice-subview-body:not([state="notready"]) > .pageAction-sendToDevice-notReady {
+ display: none;
+}
+
+/* Page action buttons */
+.pageAction-panel-button > .toolbarbutton-icon {
+ list-style-image: var(--pageAction-image-16px, inherit);
+}
+.urlbar-page-action {
+ list-style-image: var(--pageAction-image-16px, inherit);
+}
+@media (min-resolution: 1.1dppx) {
+ .pageAction-panel-button > .toolbarbutton-icon {
+ list-style-image: var(--pageAction-image-32px, inherit);
+ }
+ .urlbar-page-action {
+ list-style-image: var(--pageAction-image-32px, inherit);
+ }
+}
+
+/* Page action context menu */
+#pageActionContextMenu > .pageActionContextMenuItem {
+ visibility: collapse;
+}
+#pageActionContextMenu[state=builtInPinned] > .pageActionContextMenuItem.builtInPinned,
+#pageActionContextMenu[state=builtInUnpinned] > .pageActionContextMenuItem.builtInUnpinned,
+#pageActionContextMenu[state=extensionPinned] > .pageActionContextMenuItem.extensionPinned,
+#pageActionContextMenu[state=extensionUnpinned] > .pageActionContextMenuItem.extensionUnpinned {
+ visibility: visible;
+}
+
+/* Print pending */
+.printSettingsBrowser {
+ width: 250px !important;
+}
+
+.previewStack {
+ background-color: #f9f9fa;
+ color: #0c0c0d;
+}
+
+.previewRendering {
+ background-image: url("chrome://browser/skin/tabbrowser/pendingpaint.png");
+ background-repeat: no-repeat;
+ background-size: 60px 60px;
+ background-position: center center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ visibility: hidden;
+}
+
+.previewStack[rendering=true] > .previewRendering {
+ visibility: visible;
+}
+
+.printPreviewBrowser {
+ display: none;
+ opacity: 1;
+ transition: opacity 60ms;
+}
+
+.previewStack[previewtype="primary"] > .printPreviewBrowser[previewtype="primary"],
+.previewStack[previewtype="selection"] > .printPreviewBrowser[previewtype="selection"] {
+ display: block;
+}
+
+.previewStack[rendering=true] > .printPreviewBrowser {
+ opacity: 0;
+ transition: opacity 1ms 250ms;
+}
+
+.print-pending-label {
+ margin-top: 110px;
+ font-size: large;
+}
+
+printpreview-pagination {
+ opacity: 0;
+ transition: opacity 100ms 500ms;
+}
+printpreview-pagination:focus-within,
+.previewStack:hover > printpreview-pagination {
+ opacity: 1;
+ transition: opacity 100ms;
+}
+.previewStack[rendering=true] > printpreview-pagination {
+ opacity: 0;
+}
+
+@media (prefers-color-scheme: dark) {
+ .previewStack {
+ background-color: #2A2A2E;
+ color: rgb(249, 249, 250);
+ }
+}
+
+/* WebExtension Sidebars */
+#sidebar-box[sidebarcommand$="-sidebar-action"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon {
+ list-style-image: var(--webextension-menuitem-image, inherit);
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 16px;
+ height: 16px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ #sidebar-box[sidebarcommand$="-sidebar-action"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon {
+ list-style-image: var(--webextension-menuitem-image-2x, inherit);
+ }
+}
+
+toolbar[keyNav=true]:not([collapsed=true], [customizing=true]) toolbartabstop {
+ -moz-user-focus: normal;
+}
+
+/* Frame used for rendering the DevTools inspector highlighters */
+:root > iframe.devtools-highlighter-renderer {
+ border: none;
+ pointer-events: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 2;
+}
+
+/**
+ * Tab Dialogs
+ */
+
+.dialogStack {
+ /* Should outrank the z-index values of other UI elements, particularly the devtools
+ splitter element. */
+ z-index: 2;
+}
+
+.dialogStack.temporarilyHidden {
+ /* For some printing use cases we need to visually hide the dialog before
+ * actually closing it / make it disappear from the frame tree. */
+ visibility: hidden;
+}
+
+.dialogOverlay {
+ visibility: hidden;
+}
+
+.dialogOverlay[topmost="true"] {
+ background-color: rgba(0,0,0,0.5);
+ z-index: 1;
+}
+
+.dialogBox {
+ background-clip: content-box;
+ box-shadow: 0 2px 6px 0 rgba(0,0,0,0.3);
+ display: -moz-box;
+ margin: 0;
+ /* Make dialogs overlap with upper chrome UI */
+ margin-top: -5px;
+ padding: 0;
+ overflow-x: auto;
+}
+
+.dialogBox[resizable="true"] {
+ resize: both;
+ overflow: hidden;
+ min-height: 20em;
+ min-width: 66ch;
+}
+
+.dialogBox[sizeto="available"] {
+ --box-inline-margin: 4px;
+ --box-block-margin: 4px;
+ --box-ideal-width: 1000;
+ --box-ideal-height: 650;
+ --box-max-width-margin: calc(100vw - 2 * var(--box-inline-margin));
+ --box-max-height-margin: calc(100vh - var(--box-top-px) - var(--box-block-margin));
+ --box-max-width-ratio: 70vw;
+ --box-max-height-ratio: calc(var(--box-ideal-height) / var(--box-ideal-width) * var(--box-max-width-ratio));
+ max-width: min(max(var(--box-ideal-width) * 1px, var(--box-max-width-ratio)), var(--box-max-width-margin));
+ max-height: min(max(var(--box-ideal-height) * 1px, var(--box-max-height-ratio)), var(--box-max-height-margin));
+ width: 100vw;
+ height: 100vh;
+}
+
+@media (min-width: 550px) {
+ .dialogBox[sizeto="available"] {
+ --box-inline-margin: min(calc(4px + (100vw - 550px) / 4), 16px);
+ }
+}
+
+@media (min-width: 800px) {
+ .dialogBox[sizeto="available"] {
+ --box-inline-margin: min(calc(16px + (100vw - 800px) / 4), 32px);
+ }
+}
+
+@media (min-height: 350px) {
+ .dialogBox[sizeto="available"] {
+ --box-block-margin: min(calc(4px + (100vh - 350px) / 4), 16px);
+ }
+}
+
+@media (min-height: 550px) {
+ .dialogBox[sizeto="available"] {
+ --box-block-margin: min(calc(16px + (100vh - 550px) / 4), 32px);
+ }
+}
+
+.dialogFrame {
+ margin: 0;
+ -moz-box-flex: 1;
+ /* Default dialog dimensions */
+ width: 66ch;
+}
+
+.content-prompt-dialog > .dialogOverlay {
+ display: grid;
+ place-content: center;
+}
+
+/**
+ * End Tab Dialogs
+ */
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
new file mode 100644
index 0000000000..869f05b387
--- /dev/null
+++ b/browser/base/content/browser.js
@@ -0,0 +1,9405 @@
+/* -*- 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/. */
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+ChromeUtils.import("resource://gre/modules/NotificationDB.jsm");
+
+// lazy module getters
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ AboutReaderParent: "resource:///actors/AboutReaderParent.jsm",
+ AddonManager: "resource://gre/modules/AddonManager.jsm",
+ AMTelemetry: "resource://gre/modules/AddonManager.jsm",
+ NewTabPagePreloading: "resource:///modules/NewTabPagePreloading.jsm",
+ BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
+ BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ CFRPageActions: "resource://activity-stream/lib/CFRPageActions.jsm",
+ CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
+ Color: "resource://gre/modules/Color.jsm",
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.jsm",
+ CustomizableUI: "resource:///modules/CustomizableUI.jsm",
+ Deprecated: "resource://gre/modules/Deprecated.jsm",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
+ E10SUtils: "resource://gre/modules/E10SUtils.jsm",
+ ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
+ HomePage: "resource:///modules/HomePage.jsm",
+ LightweightThemeConsumer:
+ "resource://gre/modules/LightweightThemeConsumer.jsm",
+ Log: "resource://gre/modules/Log.jsm",
+ LoginHelper: "resource://gre/modules/LoginHelper.jsm",
+ LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
+ MigrationUtils: "resource:///modules/MigrationUtils.jsm",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
+ OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm",
+ PageActions: "resource:///modules/PageActions.jsm",
+ PageThumbs: "resource://gre/modules/PageThumbs.jsm",
+ PanelMultiView: "resource:///modules/PanelMultiView.jsm",
+ PanelView: "resource:///modules/PanelMultiView.jsm",
+ PictureInPicture: "resource://gre/modules/PictureInPicture.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
+ PluralForm: "resource://gre/modules/PluralForm.jsm",
+ Pocket: "chrome://pocket/content/Pocket.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
+ // TODO (Bug 1529552): Remove once old urlbar code goes away.
+ ReaderMode: "resource://gre/modules/ReaderMode.jsm",
+ RFPHelper: "resource://gre/modules/RFPHelper.jsm",
+ SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
+ Sanitizer: "resource:///modules/Sanitizer.jsm",
+ SessionStartup: "resource:///modules/sessionstore/SessionStartup.jsm",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
+ ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
+ SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
+ SiteDataManager: "resource:///modules/SiteDataManager.jsm",
+ SitePermissions: "resource:///modules/SitePermissions.jsm",
+ SubDialogManager: "resource://gre/modules/SubDialog.jsm",
+ TabModalPrompt: "chrome://global/content/tabprompts.jsm",
+ TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
+ Translation: "resource:///modules/translation/TranslationParent.jsm",
+ UITour: "resource:///modules/UITour.jsm",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
+ UrlbarInput: "resource:///modules/UrlbarInput.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProviderSearchTips: "resource:///modules/UrlbarProviderSearchTips.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.jsm",
+ Weave: "resource://services-sync/main.js",
+ WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
+ fxAccounts: "resource://gre/modules/FxAccounts.jsm",
+ webrtcUI: "resource:///modules/webrtcUI.jsm",
+ WebsiteFilter: "resource:///modules/policies/WebsiteFilter.jsm",
+ ZoomUI: "resource:///modules/ZoomUI.jsm",
+});
+
+if (AppConstants.MOZ_CRASHREPORTER) {
+ ChromeUtils.defineModuleGetter(
+ this,
+ "PluginCrashReporter",
+ "resource:///modules/ContentCrashHandlers.jsm"
+ );
+}
+
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "PlacesTreeView",
+ "chrome://browser/content/places/treeView.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
+ "chrome://browser/content/places/controller.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "PrintUtils",
+ "chrome://global/content/printUtils.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "ZoomManager",
+ "chrome://global/content/viewZoomOverlay.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "FullZoom",
+ "chrome://browser/content/browser-fullZoom.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "PanelUI",
+ "chrome://browser/content/customizableui/panelUI.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gViewSourceUtils",
+ "chrome://global/content/viewSourceUtils.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gTabsPanel",
+ "chrome://browser/content/browser-allTabsMenu.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["BrowserAddonUI", "gExtensionsNotifications", "gXPInstallObserver"],
+ "chrome://browser/content/browser-addons.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "ctrlTab",
+ "chrome://browser/content/browser-ctrlTab.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["CustomizationHandler", "AutoHideMenubar"],
+ "chrome://browser/content/browser-customization.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["PointerLock", "FullScreen"],
+ "chrome://browser/content/browser-fullScreenAndPointerLock.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gIdentityHandler",
+ "chrome://browser/content/browser-siteIdentity.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gProtectionsHandler",
+ "chrome://browser/content/browser-siteProtections.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["gGestureSupport", "gHistorySwipeAnimation"],
+ "chrome://browser/content/browser-gestureSupport.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gSafeBrowsing",
+ "chrome://browser/content/browser-safebrowsing.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gSync",
+ "chrome://browser/content/browser-sync.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gBrowserThumbnails",
+ "chrome://browser/content/browser-thumbnails.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["openContextMenu", "nsContextMenu"],
+ "chrome://browser/content/nsContextMenu.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ [
+ "DownloadsPanel",
+ "DownloadsOverlayLoader",
+ "DownloadsSubview",
+ "DownloadsView",
+ "DownloadsViewUI",
+ "DownloadsViewController",
+ "DownloadsSummary",
+ "DownloadsFooter",
+ "DownloadsBlockedSubview",
+ ],
+ "chrome://browser/content/downloads/downloads.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["DownloadsButton", "DownloadsIndicatorView"],
+ "chrome://browser/content/downloads/indicator.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gEditItemOverlay",
+ "chrome://browser/content/places/editBookmark.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gGfxUtils",
+ "chrome://browser/content/browser-graphics-utils.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "pktUI",
+ "chrome://pocket/content/main.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "ToolbarKeyboardNavigator",
+ "chrome://browser/content/browser-toolbarKeyNav.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "A11yUtils",
+ "chrome://browser/content/browser-a11yUtils.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "gSharedTabWarning",
+ "chrome://browser/content/browser-webrtc.js"
+);
+
+// lazy service getters
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ ContentPrefService2: [
+ "@mozilla.org/content-pref/service;1",
+ "nsIContentPrefService2",
+ ],
+ classifierService: [
+ "@mozilla.org/url-classifier/dbservice;1",
+ "nsIURIClassifier",
+ ],
+ Favicons: ["@mozilla.org/browser/favicon-service;1", "nsIFaviconService"],
+ gDNSService: ["@mozilla.org/network/dns-service;1", "nsIDNSService"],
+ gSerializationHelper: [
+ "@mozilla.org/network/serialization-helper;1",
+ "nsISerializationHelper",
+ ],
+ Marionette: ["@mozilla.org/remote/marionette;1", "nsIMarionette"],
+ WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
+ BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
+});
+
+if (AppConstants.MOZ_CRASHREPORTER) {
+ XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gCrashReporter",
+ "@mozilla.org/xre/app-info;1",
+ "nsICrashReporter"
+ );
+}
+
+if (AppConstants.ENABLE_REMOTE_AGENT) {
+ XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "RemoteAgent",
+ "@mozilla.org/remote/agent;1",
+ "nsIRemoteAgent"
+ );
+} else {
+ this.RemoteAgent = { listening: false };
+}
+
+XPCOMUtils.defineLazyGetter(this, "RTL_UI", () => {
+ return Services.locale.isAppLocaleRTL;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gBrandBundle", () => {
+ return Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", () => {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTabBrowserBundle", () => {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/tabbrowser.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(this, "gCustomizeMode", () => {
+ let { CustomizeMode } = ChromeUtils.import(
+ "resource:///modules/CustomizeMode.jsm"
+ );
+ return new CustomizeMode(window);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gNavToolbox", () => {
+ return document.getElementById("navigator-toolbox");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gURLBar", () => {
+ let urlbar = new UrlbarInput({
+ textbox: document.getElementById("urlbar"),
+ eventTelemetryCategory: "urlbar",
+ });
+
+ let beforeFocusOrSelect = event => {
+ // In customize mode, the url bar is disabled. If a new tab is opened or the
+ // user switches to a different tab, this function gets called before we've
+ // finished leaving customize mode, and the url bar will still be disabled.
+ // We can't focus it when it's disabled, so we need to re-run ourselves when
+ // we've finished leaving customize mode.
+ if (
+ CustomizationHandler.isCustomizing() ||
+ CustomizationHandler.isExitingCustomizeMode
+ ) {
+ gNavToolbox.addEventListener(
+ "aftercustomization",
+ () => {
+ if (event.type == "beforeselect") {
+ gURLBar.select();
+ } else {
+ gURLBar.focus();
+ }
+ },
+ {
+ once: true,
+ }
+ );
+ event.preventDefault();
+ return;
+ }
+
+ if (window.fullScreen) {
+ FullScreen.showNavToolbox();
+ }
+ };
+ urlbar.addEventListener("beforefocus", beforeFocusOrSelect);
+ urlbar.addEventListener("beforeselect", beforeFocusOrSelect);
+
+ return urlbar;
+});
+
+XPCOMUtils.defineLazyGetter(this, "ReferrerInfo", () =>
+ Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ )
+);
+
+// High priority notification bars shown at the top of the window.
+XPCOMUtils.defineLazyGetter(this, "gHighPriorityNotificationBox", () => {
+ return new MozElements.NotificationBox(element => {
+ element.classList.add("global-notificationbox");
+ element.setAttribute("notificationside", "top");
+ document.getElementById("appcontent").prepend(element);
+ });
+});
+
+// Regular notification bars shown at the bottom of the window.
+XPCOMUtils.defineLazyGetter(this, "gNotificationBox", () => {
+ return new MozElements.NotificationBox(element => {
+ element.classList.add("global-notificationbox");
+ element.setAttribute("notificationside", "bottom");
+ document.getElementById("browser-bottombox").appendChild(element);
+ });
+});
+
+XPCOMUtils.defineLazyGetter(this, "InlineSpellCheckerUI", () => {
+ let { InlineSpellChecker } = ChromeUtils.import(
+ "resource://gre/modules/InlineSpellChecker.jsm"
+ );
+ return new InlineSpellChecker();
+});
+
+XPCOMUtils.defineLazyGetter(this, "PageMenuParent", () => {
+ // eslint-disable-next-line no-shadow
+ let { PageMenuParent } = ChromeUtils.import(
+ "resource://gre/modules/PageMenu.jsm"
+ );
+ return new PageMenuParent();
+});
+
+XPCOMUtils.defineLazyGetter(this, "PopupNotifications", () => {
+ // eslint-disable-next-line no-shadow
+ let { PopupNotifications } = ChromeUtils.import(
+ "resource://gre/modules/PopupNotifications.jsm"
+ );
+ try {
+ // Hide all notifications while the URL is being edited and the address bar
+ // has focus, including the virtual focus in the results popup.
+ // We also have to hide notifications explicitly when the window is
+ // minimized because of the effects of the "noautohide" attribute on Linux.
+ // This can be removed once bug 545265 and bug 1320361 are fixed.
+ // Hide popup notifications when system tab prompts are shown so they
+ // don't cover up the prompt.
+ let shouldSuppress = () => {
+ return (
+ window.windowState == window.STATE_MINIMIZED ||
+ (gURLBar.getAttribute("pageproxystate") != "valid" &&
+ gURLBar.focused) ||
+ gBrowser?.selectedBrowser.hasAttribute("tabmodalChromePromptShowing") ||
+ gBrowser?.selectedBrowser.hasAttribute("tabDialogShowing")
+ );
+ };
+ return new PopupNotifications(
+ gBrowser,
+ document.getElementById("notification-popup"),
+ document.getElementById("notification-popup-box"),
+ { shouldSuppress }
+ );
+ } catch (ex) {
+ Cu.reportError(ex);
+ return null;
+ }
+});
+
+XPCOMUtils.defineLazyGetter(this, "Win7Features", () => {
+ if (AppConstants.platform != "win") {
+ return null;
+ }
+
+ const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
+ if (
+ WINTASKBAR_CONTRACTID in Cc &&
+ Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available
+ ) {
+ let { AeroPeek } = ChromeUtils.import(
+ "resource:///modules/WindowsPreviewPerTab.jsm"
+ );
+ return {
+ onOpenWindow() {
+ AeroPeek.onOpenWindow(window);
+ this.handledOpening = true;
+ },
+ onCloseWindow() {
+ if (this.handledOpening) {
+ AeroPeek.onCloseWindow(window);
+ }
+ },
+ handledOpening: false,
+ };
+ }
+ return null;
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gToolbarKeyNavEnabled",
+ "browser.toolbars.keyboard_navigation",
+ false,
+ (aPref, aOldVal, aNewVal) => {
+ if (aNewVal) {
+ ToolbarKeyboardNavigator.init();
+ } else {
+ ToolbarKeyboardNavigator.uninit();
+ }
+ }
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gBookmarksToolbar2h2020",
+ "browser.toolbars.bookmarks.2h2020",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gBookmarksToolbarVisibility",
+ "browser.toolbars.bookmarks.visibility",
+ "newtab"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gFxaToolbarEnabled",
+ "identity.fxaccounts.toolbar.enabled",
+ false,
+ (aPref, aOldVal, aNewVal) => {
+ updateFxaToolbarMenu(aNewVal);
+ }
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gFxaToolbarAccessed",
+ "identity.fxaccounts.toolbar.accessed",
+ false,
+ (aPref, aOldVal, aNewVal) => {
+ updateFxaToolbarMenu(gFxaToolbarEnabled);
+ }
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gFxaSendLoginUrl",
+ "identity.fxaccounts.service.sendLoginUrl",
+ false,
+ (aPref, aOldVal, aNewVal) => {
+ updateFxaToolbarMenu(gFxaToolbarEnabled);
+ }
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gFxaMonitorLoginUrl",
+ "identity.fxaccounts.service.monitorLoginUrl",
+ false,
+ (aPref, aOldVal, aNewVal) => {
+ updateFxaToolbarMenu(gFxaToolbarEnabled);
+ }
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gFxaDeviceName",
+ "identity.fxaccounts.account.device.name",
+ false,
+ (aPref, aOldVal, aNewVal) => {
+ updateFxaToolbarMenu(gFxaToolbarEnabled);
+ }
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gAddonAbuseReportEnabled",
+ "extensions.abuseReport.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gProton",
+ "browser.proton.enabled",
+ false,
+ (pref, oldValue, newValue) => {
+ document.documentElement.toggleAttribute("proton", newValue);
+ }
+);
+
+/* Temporary pref while we settle some questions around new tab design.
+ This will eventually be removed and browser.proton.enabled will be used instead. */
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gProtonTabs",
+ "browser.proton.tabs.enabled",
+ false,
+ (pref, oldValue, newValue) => {
+ document.documentElement.toggleAttribute("proton", newValue);
+ }
+);
+
+customElements.setElementCreationCallback("translation-notification", () => {
+ Services.scriptloader.loadSubScript(
+ "chrome://browser/content/translation-notification.js",
+ window
+ );
+});
+
+var gBrowser;
+var gInPrintPreviewMode = false;
+var gContextMenu = null; // nsContextMenu instance
+var gMultiProcessBrowser = window.docShell.QueryInterface(Ci.nsILoadContext)
+ .useRemoteTabs;
+var gFissionBrowser = window.docShell.QueryInterface(Ci.nsILoadContext)
+ .useRemoteSubframes;
+
+var gBrowserAllowScriptsToCloseInitialTabs = false;
+
+if (AppConstants.platform != "macosx") {
+ var gEditUIVisible = true;
+}
+
+Object.defineProperty(this, "gReduceMotion", {
+ enumerable: true,
+ get() {
+ return typeof gReduceMotionOverride == "boolean"
+ ? gReduceMotionOverride
+ : gReduceMotionSetting;
+ },
+});
+// Reduce motion during startup. The setting will be reset later.
+let gReduceMotionSetting = true;
+// This is for tests to set.
+var gReduceMotionOverride;
+
+// Smart getter for the findbar. If you don't wish to force the creation of
+// the findbar, check gFindBarInitialized first.
+
+Object.defineProperty(this, "gFindBar", {
+ enumerable: true,
+ get() {
+ return gBrowser.getCachedFindBar();
+ },
+});
+
+Object.defineProperty(this, "gFindBarInitialized", {
+ enumerable: true,
+ get() {
+ return gBrowser.isFindBarInitialized();
+ },
+});
+
+Object.defineProperty(this, "gFindBarPromise", {
+ enumerable: true,
+ get() {
+ return gBrowser.getFindBar();
+ },
+});
+
+async function gLazyFindCommand(cmd, ...args) {
+ let fb = await gFindBarPromise;
+ // We could be closed by now, or the tab with XBL binding could have gone away:
+ if (fb && fb[cmd]) {
+ fb[cmd].apply(fb, args);
+ }
+}
+
+var gPageIcons = {
+ "about:home": "chrome://branding/content/icon32.png",
+ "about:newtab": "chrome://branding/content/icon32.png",
+ "about:welcome": "chrome://branding/content/icon32.png",
+ "about:newinstall": "chrome://branding/content/icon32.png",
+ "about:privatebrowsing": "chrome://browser/skin/privatebrowsing/favicon.svg",
+};
+
+var gInitialPages = [
+ "about:blank",
+ "about:newtab",
+ "about:home",
+ "about:privatebrowsing",
+ "about:welcomeback",
+ "about:sessionrestore",
+ "about:welcome",
+ "about:newinstall",
+];
+
+function isInitialPage(url) {
+ if (!(url instanceof Ci.nsIURI)) {
+ try {
+ url = Services.io.newURI(url);
+ } catch (ex) {
+ return false;
+ }
+ }
+
+ let nonQuery = url.prePath + url.filePath;
+ return gInitialPages.includes(nonQuery) || nonQuery == BROWSER_NEW_TAB_URL;
+}
+
+function browserWindows() {
+ return Services.wm.getEnumerator("navigator:browser");
+}
+
+// This is a stringbundle-like interface to gBrowserBundle, formerly a getter for
+// the "bundle_browser" element.
+var gNavigatorBundle = {
+ getString(key) {
+ return gBrowserBundle.GetStringFromName(key);
+ },
+ getFormattedString(key, array) {
+ return gBrowserBundle.formatStringFromName(key, array);
+ },
+};
+
+function updateFxaToolbarMenu(enable, isInitialUpdate = false) {
+ // We only show the Firefox Account toolbar menu if the feature is enabled and
+ // if sync is enabled.
+ const syncEnabled = Services.prefs.getBoolPref(
+ "identity.fxaccounts.enabled",
+ false
+ );
+ const mainWindowEl = document.documentElement;
+ const fxaPanelEl = PanelMultiView.getViewNode(document, "PanelUI-fxa");
+
+ mainWindowEl.setAttribute("fxastatus", "not_configured");
+ fxaPanelEl.addEventListener("ViewShowing", gSync.updateSendToDeviceTitle);
+
+ Services.telemetry.setEventRecordingEnabled("fxa_app_menu", true);
+
+ if (enable && syncEnabled) {
+ mainWindowEl.setAttribute("fxatoolbarmenu", "visible");
+
+ // We have to manually update the sync state UI when toggling the FxA toolbar
+ // because it could show an invalid icon if the user is logged in and no sync
+ // event was performed yet.
+ if (!isInitialUpdate) {
+ gSync.maybeUpdateUIState();
+ }
+
+ Services.telemetry.setEventRecordingEnabled("fxa_avatar_menu", true);
+
+ // When the pref for a FxA service is removed, we remove it from
+ // the FxA toolbar menu as well. This is useful when the service
+ // might not be available that browser.
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-send-button"
+ ).hidden = !gFxaSendLoginUrl;
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-monitor-button"
+ ).hidden = !gFxaMonitorLoginUrl;
+ // If there are no services left, remove the label and sep.
+ let hideSvcs = !gFxaSendLoginUrl && !gFxaMonitorLoginUrl;
+ PanelMultiView.getViewNode(
+ document,
+ "fxa-menu-service-separator"
+ ).hidden = hideSvcs;
+ PanelMultiView.getViewNode(
+ document,
+ "fxa-menu-service-label"
+ ).hidden = hideSvcs;
+ } else {
+ mainWindowEl.removeAttribute("fxatoolbarmenu");
+ }
+}
+
+function UpdateBackForwardCommands(aWebNavigation) {
+ var backCommand = document.getElementById("Browser:Back");
+ var forwardCommand = document.getElementById("Browser:Forward");
+
+ // Avoid setting attributes on commands if the value hasn't changed!
+ // Remember, guys, setting attributes on elements is expensive! They
+ // get inherited into anonymous content, broadcast to other widgets, etc.!
+ // Don't do it if the value hasn't changed! - dwh
+
+ var backDisabled = backCommand.hasAttribute("disabled");
+ var forwardDisabled = forwardCommand.hasAttribute("disabled");
+ if (backDisabled == aWebNavigation.canGoBack) {
+ if (backDisabled) {
+ backCommand.removeAttribute("disabled");
+ } else {
+ backCommand.setAttribute("disabled", true);
+ }
+ }
+
+ if (forwardDisabled == aWebNavigation.canGoForward) {
+ if (forwardDisabled) {
+ forwardCommand.removeAttribute("disabled");
+ } else {
+ forwardCommand.setAttribute("disabled", true);
+ }
+ }
+}
+
+/**
+ * Click-and-Hold implementation for the Back and Forward buttons
+ * XXXmano: should this live in toolbarbutton.js?
+ */
+function SetClickAndHoldHandlers() {
+ // Bug 414797: Clone the back/forward buttons' context menu into both buttons.
+ let popup = document.getElementById("backForwardMenu").cloneNode(true);
+ popup.removeAttribute("id");
+ // Prevent the back/forward buttons' context attributes from being inherited.
+ popup.setAttribute("context", "");
+
+ let backButton = document.getElementById("back-button");
+ backButton.setAttribute("type", "menu");
+ backButton.prepend(popup);
+ gClickAndHoldListenersOnElement.add(backButton);
+
+ let forwardButton = document.getElementById("forward-button");
+ popup = popup.cloneNode(true);
+ forwardButton.setAttribute("type", "menu");
+ forwardButton.prepend(popup);
+ gClickAndHoldListenersOnElement.add(forwardButton);
+}
+
+const gClickAndHoldListenersOnElement = {
+ _timers: new Map(),
+
+ _mousedownHandler(aEvent) {
+ if (
+ aEvent.button != 0 ||
+ aEvent.currentTarget.open ||
+ aEvent.currentTarget.disabled
+ ) {
+ return;
+ }
+
+ // Prevent the menupopup from opening immediately
+ aEvent.currentTarget.menupopup.hidden = true;
+
+ aEvent.currentTarget.addEventListener("mouseout", this);
+ aEvent.currentTarget.addEventListener("mouseup", this);
+ this._timers.set(
+ aEvent.currentTarget,
+ setTimeout(b => this._openMenu(b), 500, aEvent.currentTarget)
+ );
+ },
+
+ _clickHandler(aEvent) {
+ if (
+ aEvent.button == 0 &&
+ aEvent.target == aEvent.currentTarget &&
+ !aEvent.currentTarget.open &&
+ !aEvent.currentTarget.disabled
+ ) {
+ let cmdEvent = document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ window,
+ 0,
+ aEvent.ctrlKey,
+ aEvent.altKey,
+ aEvent.shiftKey,
+ aEvent.metaKey,
+ null,
+ aEvent.mozInputSource
+ );
+ aEvent.currentTarget.dispatchEvent(cmdEvent);
+
+ // This is here to cancel the XUL default event
+ // dom.click() triggers a command even if there is a click handler
+ // however this can now be prevented with preventDefault().
+ aEvent.preventDefault();
+ }
+ },
+
+ _openMenu(aButton) {
+ this._cancelHold(aButton);
+ aButton.firstElementChild.hidden = false;
+ aButton.open = true;
+ },
+
+ _mouseoutHandler(aEvent) {
+ let buttonRect = aEvent.currentTarget.getBoundingClientRect();
+ if (
+ aEvent.clientX >= buttonRect.left &&
+ aEvent.clientX <= buttonRect.right &&
+ aEvent.clientY >= buttonRect.bottom
+ ) {
+ this._openMenu(aEvent.currentTarget);
+ } else {
+ this._cancelHold(aEvent.currentTarget);
+ }
+ },
+
+ _mouseupHandler(aEvent) {
+ this._cancelHold(aEvent.currentTarget);
+ },
+
+ _cancelHold(aButton) {
+ clearTimeout(this._timers.get(aButton));
+ aButton.removeEventListener("mouseout", this);
+ aButton.removeEventListener("mouseup", this);
+ },
+
+ _keypressHandler(aEvent) {
+ if (aEvent.key == " " || aEvent.key == "Enter") {
+ // Normally, command events get fired for keyboard activation. However,
+ // we've set type="menu", so that doesn't happen. Handle this the same
+ // way we handle clicks.
+ aEvent.target.click();
+ }
+ },
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "mouseout":
+ this._mouseoutHandler(e);
+ break;
+ case "mousedown":
+ this._mousedownHandler(e);
+ break;
+ case "click":
+ this._clickHandler(e);
+ break;
+ case "mouseup":
+ this._mouseupHandler(e);
+ break;
+ case "keypress":
+ this._keypressHandler(e);
+ break;
+ }
+ },
+
+ remove(aButton) {
+ aButton.removeEventListener("mousedown", this, true);
+ aButton.removeEventListener("click", this, true);
+ aButton.removeEventListener("keypress", this, true);
+ },
+
+ add(aElm) {
+ this._timers.delete(aElm);
+
+ aElm.addEventListener("mousedown", this, true);
+ aElm.addEventListener("click", this, true);
+ aElm.addEventListener("keypress", this, true);
+ },
+};
+
+const gSessionHistoryObserver = {
+ observe(subject, topic, data) {
+ if (topic != "browser:purge-session-history") {
+ return;
+ }
+
+ var backCommand = document.getElementById("Browser:Back");
+ backCommand.setAttribute("disabled", "true");
+ var fwdCommand = document.getElementById("Browser:Forward");
+ fwdCommand.setAttribute("disabled", "true");
+
+ // Clear undo history of the URL bar
+ gURLBar.editor.transactionManager.clear();
+ },
+};
+
+const gStoragePressureObserver = {
+ _lastNotificationTime: -1,
+
+ async observe(subject, topic, data) {
+ if (topic != "QuotaManager::StoragePressure") {
+ return;
+ }
+
+ const NOTIFICATION_VALUE = "storage-pressure-notification";
+ if (
+ gHighPriorityNotificationBox.getNotificationWithValue(NOTIFICATION_VALUE)
+ ) {
+ // Do not display the 2nd notification when there is already one
+ return;
+ }
+
+ // Don't display notification twice within the given interval.
+ // This is because
+ // - not to annoy user
+ // - give user some time to clean space.
+ // Even user sees notification and starts acting, it still takes some time.
+ const MIN_NOTIFICATION_INTERVAL_MS = Services.prefs.getIntPref(
+ "browser.storageManager.pressureNotification.minIntervalMS"
+ );
+ let duration = Date.now() - this._lastNotificationTime;
+ if (duration <= MIN_NOTIFICATION_INTERVAL_MS) {
+ return;
+ }
+ this._lastNotificationTime = Date.now();
+
+ MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+
+ const BYTES_IN_GIGABYTE = 1073741824;
+ const USAGE_THRESHOLD_BYTES =
+ BYTES_IN_GIGABYTE *
+ Services.prefs.getIntPref(
+ "browser.storageManager.pressureNotification.usageThresholdGB"
+ );
+ let msg = "";
+ let buttons = [];
+ let usage = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ buttons.push({
+ "l10n-id": "space-alert-learn-more-button",
+ callback(notificationBar, button) {
+ let learnMoreURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "storage-permissions";
+ // This is a content URL, loaded from trusted UX.
+ openTrustedLinkIn(learnMoreURL, "tab");
+ },
+ });
+ if (usage < USAGE_THRESHOLD_BYTES) {
+ // The firefox-used space < 5GB, then warn user to free some disk space.
+ // This is because this usage is small and not the main cause for space issue.
+ // In order to avoid the bad and wrong impression among users that
+ // firefox eats disk space a lot, indicate users to clean up other disk space.
+ [msg] = await document.l10n.formatValues([
+ { id: "space-alert-under-5gb-message" },
+ ]);
+ buttons.push({
+ "l10n-id": "space-alert-under-5gb-ok-button",
+ callback() {},
+ });
+ } else {
+ // The firefox-used space >= 5GB, then guide users to about:preferences
+ // to clear some data stored on firefox by websites.
+ [msg] = await document.l10n.formatValues([
+ { id: "space-alert-over-5gb-message" },
+ ]);
+ buttons.push({
+ "l10n-id": "space-alert-over-5gb-pref-button",
+ callback(notificationBar, button) {
+ // The advanced subpanes are only supported in the old organization, which will
+ // be removed by bug 1349689.
+ openPreferences("privacy-sitedata");
+ },
+ });
+ }
+
+ gHighPriorityNotificationBox.appendNotification(
+ msg,
+ NOTIFICATION_VALUE,
+ null,
+ gHighPriorityNotificationBox.PRIORITY_WARNING_HIGH,
+ buttons,
+ null
+ );
+
+ // This seems to be necessary to get the buttons to display correctly
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1504216
+ document.l10n.translateFragment(
+ gHighPriorityNotificationBox.currentNotification
+ );
+ },
+};
+
+var gPopupBlockerObserver = {
+ handleEvent(aEvent) {
+ if (aEvent.originalTarget != gBrowser.selectedBrowser) {
+ return;
+ }
+
+ gIdentityHandler.refreshIdentityBlock();
+
+ let popupCount = gBrowser.selectedBrowser.popupBlocker.getBlockedPopupCount();
+
+ if (!popupCount) {
+ // Hide the notification box (if it's visible).
+ let notificationBox = gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue(
+ "popup-blocked"
+ );
+ if (notification) {
+ notificationBox.removeNotification(notification, false);
+ }
+ return;
+ }
+
+ // Only show the notification again if we've not already shown it. Since
+ // notifications are per-browser, we don't need to worry about re-adding
+ // it.
+ if (gBrowser.selectedBrowser.popupBlocker.shouldShowNotification) {
+ if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
+ var brandBundle = document.getElementById("bundle_brand");
+ var brandShortName = brandBundle.getString("brandShortName");
+
+ var stringKey =
+ AppConstants.platform == "win"
+ ? "popupWarningButton"
+ : "popupWarningButtonUnix";
+
+ var popupButtonText = gNavigatorBundle.getString(stringKey);
+ var popupButtonAccesskey = gNavigatorBundle.getString(
+ stringKey + ".accesskey"
+ );
+
+ let messageBase;
+ if (popupCount < this.maxReportedPopups) {
+ messageBase = gNavigatorBundle.getString("popupWarning.message");
+ } else {
+ messageBase = gNavigatorBundle.getString(
+ "popupWarning.exceeded.message"
+ );
+ }
+
+ var message = PluralForm.get(popupCount, messageBase)
+ .replace("#1", brandShortName)
+ .replace("#2", popupCount);
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue(
+ "popup-blocked"
+ );
+ if (notification) {
+ notification.label = message;
+ } else {
+ var buttons = [
+ {
+ label: popupButtonText,
+ accessKey: popupButtonAccesskey,
+ popup: "blockedPopupOptions",
+ callback: null,
+ },
+ ];
+
+ const priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+ notificationBox.appendNotification(
+ message,
+ "popup-blocked",
+ "chrome://browser/skin/notification-icons/popup.svg",
+ priority,
+ buttons
+ );
+ }
+ }
+
+ // Record the fact that we've reported this blocked popup, so we don't
+ // show it again.
+ gBrowser.selectedBrowser.popupBlocker.didShowNotification();
+ }
+ },
+
+ toggleAllowPopupsForSite(aEvent) {
+ var pm = Services.perms;
+ var shouldBlock = aEvent.target.getAttribute("block") == "true";
+ var perm = shouldBlock ? pm.DENY_ACTION : pm.ALLOW_ACTION;
+ pm.addFromPrincipal(gBrowser.contentPrincipal, "popup", perm);
+
+ if (!shouldBlock) {
+ gBrowser.selectedBrowser.popupBlocker.unblockAllPopups();
+ }
+
+ gBrowser.getNotificationBox().removeCurrentNotification();
+ },
+
+ fillPopupList(aEvent) {
+ // XXXben - rather than using |currentURI| here, which breaks down on multi-framed sites
+ // we should really walk the blockedPopups and create a list of "allow for <host>"
+ // menuitems for the common subset of hosts present in the report, this will
+ // make us frame-safe.
+ //
+ // XXXjst - Note that when this is fixed to work with multi-framed sites,
+ // also back out the fix for bug 343772 where
+ // nsGlobalWindow::CheckOpenAllow() was changed to also
+ // check if the top window's location is whitelisted.
+ let browser = gBrowser.selectedBrowser;
+ var uriOrPrincipal = browser.contentPrincipal.isContentPrincipal
+ ? browser.contentPrincipal
+ : browser.currentURI;
+ var blockedPopupAllowSite = document.getElementById(
+ "blockedPopupAllowSite"
+ );
+ try {
+ blockedPopupAllowSite.removeAttribute("hidden");
+ let uriHost = uriOrPrincipal.asciiHost
+ ? uriOrPrincipal.host
+ : uriOrPrincipal.spec;
+ var pm = Services.perms;
+ if (
+ pm.testPermissionFromPrincipal(browser.contentPrincipal, "popup") ==
+ pm.ALLOW_ACTION
+ ) {
+ // Offer an item to block popups for this site, if a whitelist entry exists
+ // already for it.
+ let blockString = gNavigatorBundle.getFormattedString("popupBlock", [
+ uriHost,
+ ]);
+ blockedPopupAllowSite.setAttribute("label", blockString);
+ blockedPopupAllowSite.setAttribute("block", "true");
+ } else {
+ // Offer an item to allow popups for this site
+ let allowString = gNavigatorBundle.getFormattedString("popupAllow", [
+ uriHost,
+ ]);
+ blockedPopupAllowSite.setAttribute("label", allowString);
+ blockedPopupAllowSite.removeAttribute("block");
+ }
+ } catch (e) {
+ blockedPopupAllowSite.setAttribute("hidden", "true");
+ }
+
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ blockedPopupAllowSite.setAttribute("disabled", "true");
+ } else {
+ blockedPopupAllowSite.removeAttribute("disabled");
+ }
+
+ let blockedPopupDontShowMessage = document.getElementById(
+ "blockedPopupDontShowMessage"
+ );
+ let showMessage = Services.prefs.getBoolPref(
+ "privacy.popups.showBrowserMessage"
+ );
+ blockedPopupDontShowMessage.setAttribute("checked", !showMessage);
+ blockedPopupDontShowMessage.setAttribute(
+ "label",
+ gNavigatorBundle.getString("popupWarningDontShowFromMessage")
+ );
+
+ let blockedPopupsSeparator = document.getElementById(
+ "blockedPopupsSeparator"
+ );
+ blockedPopupsSeparator.setAttribute("hidden", true);
+
+ browser.popupBlocker.getBlockedPopups().then(blockedPopups => {
+ let foundUsablePopupURI = false;
+ if (blockedPopups) {
+ for (let i = 0; i < blockedPopups.length; i++) {
+ let blockedPopup = blockedPopups[i];
+
+ // popupWindowURI will be null if the file picker popup is blocked.
+ // xxxdz this should make the option say "Show file picker" and do it (Bug 590306)
+ if (!blockedPopup.popupWindowURISpec) {
+ continue;
+ }
+
+ var popupURIspec = blockedPopup.popupWindowURISpec;
+
+ // Sometimes the popup URI that we get back from the blockedPopup
+ // isn't useful (for instance, netscape.com's popup URI ends up
+ // being "http://www.netscape.com", which isn't really the URI of
+ // the popup they're trying to show). This isn't going to be
+ // useful to the user, so we won't create a menu item for it.
+ if (
+ popupURIspec == "" ||
+ popupURIspec == "about:blank" ||
+ popupURIspec == "<self>" ||
+ popupURIspec == uriOrPrincipal.spec
+ ) {
+ continue;
+ }
+
+ // Because of the short-circuit above, we may end up in a situation
+ // in which we don't have any usable popup addresses to show in
+ // the menu, and therefore we shouldn't show the separator. However,
+ // since we got past the short-circuit, we must've found at least
+ // one usable popup URI and thus we'll turn on the separator later.
+ foundUsablePopupURI = true;
+
+ var menuitem = document.createXULElement("menuitem");
+ var label = gNavigatorBundle.getFormattedString(
+ "popupShowPopupPrefix",
+ [popupURIspec]
+ );
+ menuitem.setAttribute("label", label);
+ menuitem.setAttribute(
+ "oncommand",
+ "gPopupBlockerObserver.showBlockedPopup(event);"
+ );
+ menuitem.setAttribute("popupReportIndex", i);
+ menuitem.setAttribute(
+ "popupInnerWindowId",
+ blockedPopup.innerWindowId
+ );
+ menuitem.browsingContext = blockedPopup.browsingContext;
+ menuitem.popupReportBrowser = browser;
+ aEvent.target.appendChild(menuitem);
+ }
+ }
+
+ // Show the separator if we added any
+ // showable popup addresses to the menu.
+ if (foundUsablePopupURI) {
+ blockedPopupsSeparator.removeAttribute("hidden");
+ }
+ }, null);
+ },
+
+ onPopupHiding(aEvent) {
+ let item = aEvent.target.lastElementChild;
+ while (item && item.id != "blockedPopupsSeparator") {
+ let next = item.previousElementSibling;
+ item.remove();
+ item = next;
+ }
+ },
+
+ showBlockedPopup(aEvent) {
+ let target = aEvent.target;
+ let browsingContext = target.browsingContext;
+ let innerWindowId = target.getAttribute("popupInnerWindowId");
+ let popupReportIndex = target.getAttribute("popupReportIndex");
+ let browser = target.popupReportBrowser;
+ browser.popupBlocker.unblockPopup(
+ browsingContext,
+ innerWindowId,
+ popupReportIndex
+ );
+ },
+
+ editPopupSettings() {
+ openPreferences("privacy-permissions-block-popups");
+ },
+
+ dontShowMessage() {
+ var showMessage = Services.prefs.getBoolPref(
+ "privacy.popups.showBrowserMessage"
+ );
+ Services.prefs.setBoolPref(
+ "privacy.popups.showBrowserMessage",
+ !showMessage
+ );
+ gBrowser.getNotificationBox().removeCurrentNotification();
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ gPopupBlockerObserver,
+ "maxReportedPopups",
+ "privacy.popups.maxReported"
+);
+
+var gKeywordURIFixup = {
+ check(browser, { fixedURI, keywordProviderName, preferredURI }) {
+ // We get called irrespective of whether we did a keyword search, or
+ // whether the original input would be vaguely interpretable as a URL,
+ // so figure that out first.
+ if (
+ !keywordProviderName ||
+ !fixedURI ||
+ !fixedURI.host ||
+ UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") ||
+ UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") == 0
+ ) {
+ return;
+ }
+
+ let contentPrincipal = browser.contentPrincipal;
+
+ // At this point we're still only just about to load this URI.
+ // When the async DNS lookup comes back, we may be in any of these states:
+ // 1) still on the previous URI, waiting for the preferredURI (keyword
+ // search) to respond;
+ // 2) at the keyword search URI (preferredURI)
+ // 3) at some other page because the user stopped navigation.
+ // We keep track of the currentURI to detect case (1) in the DNS lookup
+ // callback.
+ let previousURI = browser.currentURI;
+
+ // now swap for a weak ref so we don't hang on to browser needlessly
+ // even if the DNS query takes forever
+ let weakBrowser = Cu.getWeakReference(browser);
+ browser = null;
+
+ // Additionally, we need the host of the parsed url
+ let hostName = fixedURI.displayHost;
+ // and the ascii-only host for the pref:
+ let asciiHost = fixedURI.asciiHost;
+ // Normalize out a single trailing dot - NB: not using endsWith/lastIndexOf
+ // because we need to be sure this last dot is the *only* dot, too.
+ // More generally, this is used for the pref and should stay in sync with
+ // the code in URIFixup::KeywordURIFixup .
+ if (asciiHost.indexOf(".") == asciiHost.length - 1) {
+ asciiHost = asciiHost.slice(0, -1);
+ }
+
+ let isIPv4Address = host => {
+ let parts = host.split(".");
+ if (parts.length != 4) {
+ return false;
+ }
+ return parts.every(part => {
+ let n = parseInt(part, 10);
+ return n >= 0 && n <= 255;
+ });
+ };
+ // Avoid showing fixup information if we're suggesting an IP. Note that
+ // decimal representations of IPs are normalized to a 'regular'
+ // dot-separated IP address by network code, but that only happens for
+ // numbers that don't overflow. Longer numbers do not get normalized,
+ // but still work to access IP addresses. So for instance,
+ // 1097347366913 (ff7f000001) gets resolved by using the final bytes,
+ // making it the same as 7f000001, which is 127.0.0.1 aka localhost.
+ // While 2130706433 would get normalized by network, 1097347366913
+ // does not, and we have to deal with both cases here:
+ if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost)) {
+ return;
+ }
+
+ let onLookupCompleteListener = {
+ onLookupComplete(request, record, status) {
+ let browserRef = weakBrowser.get();
+ if (!Components.isSuccessCode(status) || !browserRef) {
+ return;
+ }
+
+ let currentURI = browserRef.currentURI;
+ // If we're in case (3) (see above), don't show an info bar.
+ if (
+ !currentURI.equals(previousURI) &&
+ !currentURI.equals(preferredURI)
+ ) {
+ return;
+ }
+
+ // show infobar offering to visit the host
+ let notificationBox = gBrowser.getNotificationBox(browserRef);
+ if (notificationBox.getNotificationWithValue("keyword-uri-fixup")) {
+ return;
+ }
+
+ let displayHostName = "http://" + hostName + "/";
+ let message = gNavigatorBundle.getFormattedString(
+ "keywordURIFixup.message",
+ [displayHostName]
+ );
+ let yesMessage = gNavigatorBundle.getFormattedString(
+ "keywordURIFixup.goTo",
+ [displayHostName]
+ );
+
+ let buttons = [
+ {
+ label: yesMessage,
+ accessKey: gNavigatorBundle.getString(
+ "keywordURIFixup.goTo.accesskey"
+ ),
+ callback() {
+ // Do not set this preference while in private browsing.
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ let pref = "browser.fixup.domainwhitelist." + asciiHost;
+ Services.prefs.setBoolPref(pref, true);
+ }
+ openTrustedLinkIn(fixedURI.spec, "current");
+ },
+ },
+ {
+ label: gNavigatorBundle.getString("keywordURIFixup.dismiss"),
+ accessKey: gNavigatorBundle.getString(
+ "keywordURIFixup.dismiss.accesskey"
+ ),
+ callback() {
+ let notification = notificationBox.getNotificationWithValue(
+ "keyword-uri-fixup"
+ );
+ notificationBox.removeNotification(notification, true);
+ },
+ },
+ ];
+ let notification = notificationBox.appendNotification(
+ message,
+ "keyword-uri-fixup",
+ null,
+ notificationBox.PRIORITY_INFO_HIGH,
+ buttons
+ );
+ notification.persistence = 1;
+ },
+ };
+
+ try {
+ gDNSService.asyncResolve(
+ hostName,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ onLookupCompleteListener,
+ Services.tm.mainThread,
+ contentPrincipal.originAttributes
+ );
+ } catch (ex) {
+ // Do nothing if the URL is invalid (we don't want to show a notification in that case).
+ if (ex.result != Cr.NS_ERROR_UNKNOWN_HOST) {
+ // ... otherwise, report:
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ observe(fixupInfo, topic, data) {
+ fixupInfo.QueryInterface(Ci.nsIURIFixupInfo);
+
+ let browser = fixupInfo.consumer?.top?.embedderElement;
+ if (!browser || browser.ownerGlobal != window) {
+ return;
+ }
+
+ this.check(browser, fixupInfo);
+ },
+};
+
+function serializeInputStream(aStream) {
+ let data = {
+ content: NetUtil.readInputStreamToString(aStream, aStream.available()),
+ };
+
+ if (aStream instanceof Ci.nsIMIMEInputStream) {
+ data.headers = new Map();
+ aStream.visitHeaders((name, value) => {
+ data.headers.set(name, value);
+ });
+ }
+
+ return data;
+}
+
+/**
+ * Handles URIs when we want to deal with them in chrome code rather than pass
+ * them down to a content browser. This can avoid unnecessary process switching
+ * for the browser.
+ * @param aBrowser the browser that is attempting to load the URI
+ * @param aUri the nsIURI that is being loaded
+ * @returns true if the URI is handled, otherwise false
+ */
+function handleUriInChrome(aBrowser, aUri) {
+ if (aUri.scheme == "file") {
+ try {
+ let mimeType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromURI(aUri);
+ if (mimeType == "application/x-xpinstall") {
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ AddonManager.getInstallForURL(aUri.spec, {
+ telemetryInfo: { source: "file-url" },
+ }).then(install => {
+ AddonManager.installAddonFromWebpage(
+ mimeType,
+ aBrowser,
+ systemPrincipal,
+ install
+ );
+ });
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+
+ return false;
+}
+
+/* Creates a null principal using the userContextId
+ from the current selected tab or a passed in tab argument */
+function _createNullPrincipalFromTabUserContextId(tab = gBrowser.selectedTab) {
+ let userContextId;
+ if (tab.hasAttribute("usercontextid")) {
+ userContextId = tab.getAttribute("usercontextid");
+ }
+ return Services.scriptSecurityManager.createNullPrincipal({
+ userContextId,
+ });
+}
+
+// A shared function used by both remote and non-remote browser XBL bindings to
+// load a URI or redirect it to the correct process.
+function _loadURI(browser, uri, params = {}) {
+ if (!uri) {
+ uri = "about:blank";
+ }
+
+ let { triggeringPrincipal, referrerInfo, postData, userContextId, csp } =
+ params || {};
+ let loadFlags =
+ params.loadFlags || params.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let hasValidUserGestureActivation =
+ document.hasValidTransientUserGestureActivation;
+ if (!triggeringPrincipal) {
+ throw new Error("Must load with a triggering Principal");
+ }
+
+ if (userContextId && userContextId != browser.getAttribute("usercontextid")) {
+ throw new Error("Cannot load with mismatched userContextId");
+ }
+
+ // Attempt to perform URI fixup to see if we can handle this URI in chrome.
+ try {
+ let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE;
+ if (loadFlags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
+ fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ }
+ if (loadFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
+ fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ }
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+
+ let uriObject = Services.uriFixup.getFixupURIInfo(uri, fixupFlags)
+ .preferredURI;
+ if (uriObject && handleUriInChrome(browser, uriObject)) {
+ // If we've handled the URI in Chrome, then just return here.
+ return;
+ }
+ } catch (e) {
+ // getFixupURIInfo may throw. Gracefully recover and try to load the URI normally.
+ }
+
+ // XXX(nika): Is `browser.isNavigating` necessary anymore?
+ browser.isNavigating = true;
+ let loadURIOptions = {
+ triggeringPrincipal,
+ csp,
+ loadFlags,
+ referrerInfo,
+ postData,
+ hasValidUserGestureActivation,
+ };
+ try {
+ browser.webNavigation.loadURI(uri, loadURIOptions);
+ } finally {
+ browser.isNavigating = false;
+ }
+}
+
+let _resolveDelayedStartup;
+var delayedStartupPromise = new Promise(resolve => {
+ _resolveDelayedStartup = resolve;
+});
+
+var gBrowserInit = {
+ delayedStartupFinished: false,
+ idleTasksFinishedPromise: null,
+ idleTaskPromiseResolve: null,
+ domContentLoaded: false,
+
+ _tabToAdopt: undefined,
+
+ getTabToAdopt() {
+ if (this._tabToAdopt !== undefined) {
+ return this._tabToAdopt;
+ }
+
+ if (window.arguments && window.arguments[0] instanceof window.XULElement) {
+ this._tabToAdopt = window.arguments[0];
+
+ // Clear the reference of the tab being adopted from the arguments.
+ window.arguments[0] = null;
+ } else {
+ // There was no tab to adopt in the arguments, set _tabToAdopt to null
+ // to avoid checking it again.
+ this._tabToAdopt = null;
+ }
+
+ return this._tabToAdopt;
+ },
+
+ _clearTabToAdopt() {
+ this._tabToAdopt = null;
+ },
+
+ // Used to check if the new window is still adopting an existing tab as its first tab
+ // (e.g. from the WebExtensions internals).
+ isAdoptingTab() {
+ return !!this.getTabToAdopt();
+ },
+
+ onBeforeInitialXULLayout() {
+ BookmarkingUI.updateEmptyToolbarMessage();
+ setToolbarVisibility(
+ BookmarkingUI.toolbar,
+ gBookmarksToolbar2h2020
+ ? gBookmarksToolbarVisibility
+ : gBookmarksToolbarVisibility == "always",
+ false,
+ false
+ );
+
+ // Set a sane starting width/height for all resolutions on new profiles.
+ if (Services.prefs.getBoolPref("privacy.resistFingerprinting")) {
+ // When the fingerprinting resistance is enabled, making sure that we don't
+ // have a maximum window to interfere with generating rounded window dimensions.
+ document.documentElement.setAttribute("sizemode", "normal");
+ } else if (!document.documentElement.hasAttribute("width")) {
+ const TARGET_WIDTH = 1280;
+ const TARGET_HEIGHT = 1040;
+ let width = Math.min(screen.availWidth * 0.9, TARGET_WIDTH);
+ let height = Math.min(screen.availHeight * 0.9, TARGET_HEIGHT);
+
+ document.documentElement.setAttribute("width", width);
+ document.documentElement.setAttribute("height", height);
+
+ if (width < TARGET_WIDTH && height < TARGET_HEIGHT) {
+ document.documentElement.setAttribute("sizemode", "maximized");
+ }
+ }
+
+ // Run menubar initialization first, to avoid TabsInTitlebar code picking
+ // up mutations from it and causing a reflow.
+ AutoHideMenubar.init();
+ // Update the chromemargin attribute so the window can be sized correctly.
+ window.TabBarVisibility.update();
+ TabsInTitlebar.init();
+
+ new LightweightThemeConsumer(document);
+
+ if (AppConstants.platform == "win") {
+ if (
+ window.matchMedia("(-moz-os-version: windows-win8)").matches &&
+ window.matchMedia("(-moz-windows-default-theme)").matches
+ ) {
+ let windowFrameColor = new Color(
+ ...ChromeUtils.import(
+ "resource:///modules/Windows8WindowFrameColor.jsm",
+ {}
+ ).Windows8WindowFrameColor.get()
+ );
+ // Default to black for foreground text.
+ if (!windowFrameColor.isContrastRatioAcceptable(new Color(0, 0, 0))) {
+ document.documentElement.setAttribute("darkwindowframe", "true");
+ }
+ } else if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ TelemetryEnvironment.onInitialized().then(() => {
+ // 17763 is the build number of Windows 10 version 1809
+ if (
+ TelemetryEnvironment.currentEnvironment.system.os
+ .windowsBuildNumber < 17763
+ ) {
+ document.documentElement.setAttribute(
+ "always-use-accent-color-for-window-border",
+ ""
+ );
+ }
+ });
+ }
+ }
+
+ if (
+ Services.prefs.getBoolPref(
+ "toolkit.legacyUserProfileCustomizations.windowIcon",
+ false
+ )
+ ) {
+ document.documentElement.setAttribute("icon", "main-window");
+ }
+
+ document.documentElement.toggleAttribute("proton", gProton);
+
+ // Call this after we set attributes that might change toolbars' computed
+ // text color.
+ ToolbarIconColor.init();
+ },
+
+ onDOMContentLoaded() {
+ // This needs setting up before we create the first remote browser.
+ window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).XULBrowserWindow = window.XULBrowserWindow;
+ window.browserDOMWindow = new nsBrowserAccess();
+
+ gBrowser = window._gBrowser;
+ delete window._gBrowser;
+ gBrowser.init();
+
+ BrowserWindowTracker.track(window);
+
+ gNavToolbox.palette = document.getElementById(
+ "BrowserToolbarPalette"
+ ).content;
+ let areas = CustomizableUI.areas;
+ areas.splice(areas.indexOf(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL), 1);
+ for (let area of areas) {
+ let node = document.getElementById(area);
+ CustomizableUI.registerToolbarNode(node);
+ }
+ BrowserSearch.initPlaceHolder();
+
+ // Hack to ensure that the various initial pages favicon is loaded
+ // instantaneously, to avoid flickering and improve perceived performance.
+ this._callWithURIToLoad(uriToLoad => {
+ let url;
+ try {
+ url = Services.io.newURI(uriToLoad);
+ } catch (e) {
+ return;
+ }
+ let nonQuery = url.prePath + url.filePath;
+ if (nonQuery in gPageIcons) {
+ gBrowser.setIcon(gBrowser.selectedTab, gPageIcons[nonQuery]);
+ }
+ });
+
+ this._setInitialFocus();
+
+ updateFxaToolbarMenu(gFxaToolbarEnabled, true);
+
+ this.domContentLoaded = true;
+ },
+
+ onLoad() {
+ gBrowser.addEventListener("DOMUpdateBlockedPopups", gPopupBlockerObserver);
+
+ window.addEventListener("AppCommand", HandleAppCommandEvent, true);
+
+ // These routines add message listeners. They must run before
+ // loading the frame script to ensure that we don't miss any
+ // message sent between when the frame script is loaded and when
+ // the listener is registered.
+ CaptivePortalWatcher.init();
+ ZoomUI.init(window);
+
+ let mm = window.getGroupMessageManager("browsers");
+ mm.loadFrameScript("chrome://browser/content/tab-content.js", true, true);
+
+ if (!gMultiProcessBrowser) {
+ // There is a Content:Click message manually sent from content.
+ Services.els.addSystemEventListener(
+ gBrowser.tabpanels,
+ "click",
+ contentAreaClick,
+ true
+ );
+ }
+
+ // hook up UI through progress listener
+ gBrowser.addProgressListener(window.XULBrowserWindow);
+ gBrowser.addTabsProgressListener(window.TabsProgressListener);
+
+ SidebarUI.init();
+
+ // We do this in onload because we want to ensure the button's state
+ // doesn't flicker as the window is being shown.
+ DownloadsButton.init();
+
+ // Certain kinds of automigration rely on this notification to complete
+ // their tasks BEFORE the browser window is shown. SessionStore uses it to
+ // restore tabs into windows AFTER important parts like gMultiProcessBrowser
+ // have been initialized.
+ Services.obs.notifyObservers(window, "browser-window-before-show");
+
+ if (!window.toolbar.visible) {
+ // adjust browser UI for popups
+ gURLBar.readOnly = true;
+ }
+
+ // Misc. inits.
+ gUIDensity.init();
+ TabletModeUpdater.init();
+ CombinedStopReload.ensureInitialized();
+ gPrivateBrowsingUI.init();
+ BrowserSearch.init();
+ BrowserPageActions.init();
+ gAccessibilityServiceIndicator.init();
+ if (gToolbarKeyNavEnabled) {
+ ToolbarKeyboardNavigator.init();
+ }
+
+ // Update UI if browser is under remote control.
+ gRemoteControl.updateVisualCue();
+
+ // If we are given a tab to swap in, take care of it before first paint to
+ // avoid an about:blank flash.
+ let tabToAdopt = this.getTabToAdopt();
+ if (tabToAdopt) {
+ let evt = new CustomEvent("before-initial-tab-adopted", {
+ bubbles: true,
+ });
+ gBrowser.tabpanels.dispatchEvent(evt);
+
+ // Stop the about:blank load
+ gBrowser.stop();
+ // make sure it has a docshell
+ gBrowser.docShell;
+
+ // Remove the speculative focus from the urlbar to let the url be formatted.
+ gURLBar.removeAttribute("focused");
+
+ try {
+ gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, tabToAdopt);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ // Clear the reference to the tab once its adoption has been completed.
+ this._clearTabToAdopt();
+ }
+
+ // Wait until chrome is painted before executing code not critical to making the window visible
+ this._boundDelayedStartup = this._delayedStartup.bind(this);
+ window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
+
+ if (!PrivateBrowsingUtils.enabled) {
+ document.getElementById("Tools:PrivateBrowsing").hidden = true;
+ // Setting disabled doesn't disable the shortcut, so we just remove
+ // the keybinding.
+ document.getElementById("key_privatebrowsing").remove();
+ }
+
+ this._loadHandled = true;
+ },
+
+ _cancelDelayedStartup() {
+ window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
+ this._boundDelayedStartup = null;
+ },
+
+ _delayedStartup() {
+ let { TelemetryTimestamps } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryTimestamps.jsm"
+ );
+ TelemetryTimestamps.add("delayedStartupStarted");
+
+ this._cancelDelayedStartup();
+
+ // Bug 1531854 - The hidden window is force-created here
+ // until all of its dependencies are handled.
+ Services.appShell.hiddenDOMWindow;
+
+ gBrowser.addEventListener(
+ "PermissionStateChange",
+ function() {
+ gIdentityHandler.refreshIdentityBlock();
+ },
+ true
+ );
+
+ this._handleURIToLoad();
+
+ Services.obs.addObserver(gIdentityHandler, "perm-changed");
+ Services.obs.addObserver(gRemoteControl, "marionette-listening");
+ Services.obs.addObserver(gRemoteControl, "remote-listening");
+ Services.obs.addObserver(
+ gSessionHistoryObserver,
+ "browser:purge-session-history"
+ );
+ Services.obs.addObserver(
+ gStoragePressureObserver,
+ "QuotaManager::StoragePressure"
+ );
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled");
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-started");
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked");
+ Services.obs.addObserver(
+ gXPInstallObserver,
+ "addon-install-fullscreen-blocked"
+ );
+ Services.obs.addObserver(
+ gXPInstallObserver,
+ "addon-install-origin-blocked"
+ );
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-failed");
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation");
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-complete");
+ Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup");
+
+ BrowserOffline.init();
+ IndexedDBPromptHelper.init();
+ CanvasPermissionPromptHelper.init();
+ WebAuthnPromptHelper.init();
+
+ // Initialize the full zoom setting.
+ // We do this before the session restore service gets initialized so we can
+ // apply full zoom settings to tabs restored by the session restore service.
+ FullZoom.init();
+ PanelUI.init();
+
+ UpdateUrlbarSearchSplitterState();
+
+ BookmarkingUI.init();
+ BrowserSearch.delayedStartupInit();
+ gProtectionsHandler.init();
+ HomePage.delayedStartup().catch(Cu.reportError);
+
+ let safeMode = document.getElementById("helpSafeMode");
+ if (Services.appinfo.inSafeMode) {
+ document.l10n.setAttributes(safeMode, "menu-help-safe-mode-with-addons");
+ safeMode.setAttribute(
+ "appmenu-data-l10n-id",
+ "appmenu-help-safe-mode-with-addons"
+ );
+ }
+
+ // BiDi UI
+ gBidiUI = isBidiEnabled();
+ if (gBidiUI) {
+ document.getElementById("documentDirection-separator").hidden = false;
+ document.getElementById("documentDirection-swap").hidden = false;
+ document.getElementById("textfieldDirection-separator").hidden = false;
+ document.getElementById("textfieldDirection-swap").hidden = false;
+ }
+
+ // Setup click-and-hold gestures access to the session history
+ // menus if global click-and-hold isn't turned on
+ if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) {
+ SetClickAndHoldHandlers();
+ }
+
+ PlacesToolbarHelper.init();
+
+ ctrlTab.readPref();
+ Services.prefs.addObserver(ctrlTab.prefName, ctrlTab);
+
+ // The object handling the downloads indicator is initialized here in the
+ // delayed startup function, but the actual indicator element is not loaded
+ // unless there are downloads to be displayed.
+ DownloadsButton.initializeIndicator();
+
+ if (AppConstants.platform != "macosx") {
+ updateEditUIVisibility();
+ let placesContext = document.getElementById("placesContext");
+ placesContext.addEventListener("popupshowing", updateEditUIVisibility);
+ placesContext.addEventListener("popuphiding", updateEditUIVisibility);
+ }
+
+ FullScreen.init();
+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ MenuTouchModeObserver.init();
+ }
+
+ if (AppConstants.MOZ_DATA_REPORTING) {
+ gDataNotificationInfoBar.init();
+ }
+
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ DevelopmentHelpers.init();
+ }
+
+ gExtensionsNotifications.init();
+
+ let wasMinimized = window.windowState == window.STATE_MINIMIZED;
+ window.addEventListener("sizemodechange", () => {
+ let isMinimized = window.windowState == window.STATE_MINIMIZED;
+ if (wasMinimized != isMinimized) {
+ wasMinimized = isMinimized;
+ UpdatePopupNotificationsVisibility();
+ }
+ });
+
+ window.addEventListener("mousemove", MousePosTracker);
+ window.addEventListener("dragover", MousePosTracker);
+
+ gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
+ gNavToolbox.addEventListener("aftercustomization", CustomizationHandler);
+
+ SessionStore.promiseInitialized.then(() => {
+ // Bail out if the window has been closed in the meantime.
+ if (window.closed) {
+ return;
+ }
+
+ // Enable the Restore Last Session command if needed
+ RestoreLastSessionObserver.init();
+
+ SidebarUI.startDelayedLoad();
+
+ PanicButtonNotifier.init();
+ });
+
+ gBrowser.tabContainer.addEventListener("TabSelect", function() {
+ for (let panel of document.querySelectorAll(
+ "panel[tabspecific='true']"
+ )) {
+ if (panel.state == "open") {
+ panel.hidePopup();
+ }
+ }
+ });
+
+ if (BrowserHandler.kiosk) {
+ // We don't modify popup windows for kiosk mode
+ if (!gURLBar.readOnly) {
+ window.fullScreen = true;
+ }
+ }
+
+ if (Services.policies.status === Services.policies.ACTIVE) {
+ if (!Services.policies.isAllowed("hideShowMenuBar")) {
+ document
+ .getElementById("toolbar-menubar")
+ .removeAttribute("toolbarname");
+ }
+ let policies = Services.policies.getActivePolicies();
+ if ("ManagedBookmarks" in policies) {
+ let managedBookmarks = policies.ManagedBookmarks;
+ let children = managedBookmarks.filter(
+ child => !("toplevel_name" in child)
+ );
+ if (children.length) {
+ let managedBookmarksButton = document.createXULElement(
+ "toolbarbutton"
+ );
+ managedBookmarksButton.setAttribute("id", "managed-bookmarks");
+ managedBookmarksButton.setAttribute("class", "bookmark-item");
+ let toplevel = managedBookmarks.find(
+ element => "toplevel_name" in element
+ );
+ if (toplevel) {
+ managedBookmarksButton.setAttribute(
+ "label",
+ toplevel.toplevel_name
+ );
+ } else {
+ managedBookmarksButton.setAttribute(
+ "data-l10n-id",
+ "managed-bookmarks"
+ );
+ }
+ managedBookmarksButton.setAttribute("context", "placesContext");
+ managedBookmarksButton.setAttribute("container", "true");
+ managedBookmarksButton.setAttribute("removable", "false");
+ managedBookmarksButton.setAttribute("type", "menu");
+
+ let managedBookmarksPopup = document.createXULElement("menupopup");
+ managedBookmarksPopup.setAttribute("id", "managed-bookmarks-popup");
+ managedBookmarksPopup.setAttribute(
+ "oncommand",
+ "PlacesToolbarHelper.openManagedBookmark(event);"
+ );
+ managedBookmarksPopup.setAttribute(
+ "onclick",
+ "checkForMiddleClick(this, event);"
+ );
+ managedBookmarksPopup.setAttribute(
+ "ondragover",
+ "event.dataTransfer.effectAllowed='none';"
+ );
+ managedBookmarksPopup.setAttribute(
+ "ondragstart",
+ "PlacesToolbarHelper.onDragStartManaged(event);"
+ );
+ managedBookmarksPopup.setAttribute(
+ "onpopupshowing",
+ "PlacesToolbarHelper.populateManagedBookmarks(this);"
+ );
+ managedBookmarksPopup.setAttribute("placespopup", "true");
+ managedBookmarksPopup.setAttribute("is", "places-popup");
+ managedBookmarksButton.appendChild(managedBookmarksPopup);
+
+ gNavToolbox.palette.appendChild(managedBookmarksButton);
+
+ CustomizableUI.ensureWidgetPlacedInWindow(
+ "managed-bookmarks",
+ window
+ );
+
+ // Add button if it doesn't exist
+ if (!CustomizableUI.getPlacementOfWidget("managed-bookmarks")) {
+ CustomizableUI.addWidgetToArea(
+ "managed-bookmarks",
+ CustomizableUI.AREA_BOOKMARKS,
+ 0
+ );
+ }
+ }
+ }
+ }
+
+ CaptivePortalWatcher.delayedStartup();
+
+ SessionStore.promiseAllWindowsRestored.then(() => {
+ this._schedulePerWindowIdleTasks();
+ document.documentElement.setAttribute("sessionrestored", "true");
+ });
+
+ this.delayedStartupFinished = true;
+ _resolveDelayedStartup();
+ Services.obs.notifyObservers(window, "browser-delayed-startup-finished");
+ TelemetryTimestamps.add("delayedStartupFinished");
+ // We've announced that delayed startup has finished. Do not add code past this point.
+ },
+
+ /**
+ * Resolved on the first MozAfterPaint in the first content window.
+ */
+ get firstContentWindowPaintPromise() {
+ return this._firstContentWindowPaintDeferred.promise;
+ },
+
+ _setInitialFocus() {
+ let initiallyFocusedElement = document.commandDispatcher.focusedElement;
+
+ // To prevent startup flicker, the urlbar has the 'focused' attribute set
+ // by default. If we are not sure the urlbar will be focused in this
+ // window, we need to remove the attribute before first paint.
+ // TODO (bug 1629956): The urlbar having the 'focused' attribute by default
+ // isn't a useful optimization anymore since UrlbarInput needs layout
+ // information to focus the urlbar properly.
+ let shouldRemoveFocusedAttribute = true;
+
+ this._callWithURIToLoad(uriToLoad => {
+ if (isBlankPageURL(uriToLoad) || uriToLoad == "about:privatebrowsing") {
+ gURLBar.select();
+ shouldRemoveFocusedAttribute = false;
+ return;
+ }
+
+ if (gBrowser.selectedBrowser.isRemoteBrowser) {
+ // If the initial browser is remote, in order to optimize for first paint,
+ // we'll defer switching focus to that browser until it has painted.
+ this._firstContentWindowPaintDeferred.promise.then(() => {
+ // If focus didn't move while we were waiting for first paint, we're okay
+ // to move to the browser.
+ if (
+ document.commandDispatcher.focusedElement == initiallyFocusedElement
+ ) {
+ gBrowser.selectedBrowser.focus();
+ }
+ });
+ } else {
+ // If the initial browser is not remote, we can focus the browser
+ // immediately with no paint performance impact.
+ gBrowser.selectedBrowser.focus();
+ }
+ });
+
+ // Delay removing the attribute using requestAnimationFrame to avoid
+ // invalidating styles multiple times in a row if uriToLoadPromise
+ // resolves before first paint.
+ if (shouldRemoveFocusedAttribute) {
+ window.requestAnimationFrame(() => {
+ if (shouldRemoveFocusedAttribute) {
+ gURLBar.removeAttribute("focused");
+ }
+ });
+ }
+ },
+
+ _handleURIToLoad() {
+ this._callWithURIToLoad(uriToLoad => {
+ if (!uriToLoad) {
+ // We don't check whether window.arguments[5] (userContextId) is set
+ // because tabbrowser.js takes care of that for the initial tab.
+ return;
+ }
+
+ // We don't check if uriToLoad is a XULElement because this case has
+ // already been handled before first paint, and the argument cleared.
+ if (Array.isArray(uriToLoad)) {
+ // This function throws for certain malformed URIs, so use exception handling
+ // so that we don't disrupt startup
+ try {
+ gBrowser.loadTabs(uriToLoad, {
+ inBackground: false,
+ replace: true,
+ // See below for the semantics of window.arguments. Only the minimum is supported.
+ userContextId: window.arguments[5],
+ triggeringPrincipal:
+ window.arguments[8] ||
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ allowInheritPrincipal: window.arguments[9],
+ csp: window.arguments[10],
+ fromExternal: true,
+ });
+ } catch (e) {}
+ } else if (window.arguments.length >= 3) {
+ // window.arguments[1]: unused (bug 871161)
+ // [2]: referrerInfo (nsIReferrerInfo)
+ // [3]: postData (nsIInputStream)
+ // [4]: allowThirdPartyFixup (bool)
+ // [5]: userContextId (int)
+ // [6]: originPrincipal (nsIPrincipal)
+ // [7]: originStoragePrincipal (nsIPrincipal)
+ // [8]: triggeringPrincipal (nsIPrincipal)
+ // [9]: allowInheritPrincipal (bool)
+ // [10]: csp (nsIContentSecurityPolicy)
+ // [11]: nsOpenWindowInfo
+ let userContextId =
+ window.arguments[5] != undefined
+ ? window.arguments[5]
+ : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+ loadURI(
+ uriToLoad,
+ window.arguments[2] || null,
+ window.arguments[3] || null,
+ window.arguments[4] || false,
+ userContextId,
+ // pass the origin principal (if any) and force its use to create
+ // an initial about:blank viewer if present:
+ window.arguments[6],
+ window.arguments[7],
+ !!window.arguments[6],
+ window.arguments[8],
+ // TODO fix allowInheritPrincipal to default to false.
+ // Default to true unless explicitly set to false because of bug 1475201.
+ window.arguments[9] !== false,
+ window.arguments[10]
+ );
+ window.focus();
+ } else {
+ // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3.
+ // Such callers expect that window.arguments[0] is handled as a single URI.
+ loadOneOrMoreURIs(
+ uriToLoad,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+ }
+ });
+ },
+
+ /**
+ * Use this function as an entry point to schedule tasks that
+ * need to run once per window after startup, and can be scheduled
+ * by using an idle callback.
+ *
+ * The functions scheduled here will fire from idle callbacks
+ * once every window has finished being restored by session
+ * restore, and after the equivalent only-once tasks
+ * have run (from _scheduleStartupIdleTasks in BrowserGlue.jsm).
+ */
+ _schedulePerWindowIdleTasks() {
+ // Bail out if the window has been closed in the meantime.
+ if (window.closed) {
+ return;
+ }
+
+ function scheduleIdleTask(func, options) {
+ requestIdleCallback(function idleTaskRunner() {
+ if (!window.closed) {
+ func();
+ }
+ }, options);
+ }
+
+ scheduleIdleTask(() => {
+ // Initialize the Sync UI
+ gSync.init();
+ });
+
+ scheduleIdleTask(() => {
+ // Read prefers-reduced-motion setting
+ let reduceMotionQuery = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ );
+ function readSetting() {
+ gReduceMotionSetting = reduceMotionQuery.matches;
+ }
+ reduceMotionQuery.addListener(readSetting);
+ readSetting();
+ });
+
+ scheduleIdleTask(() => {
+ // setup simple gestures support
+ gGestureSupport.init(true);
+
+ // setup history swipe animation
+ gHistorySwipeAnimation.init();
+ });
+
+ scheduleIdleTask(() => {
+ gBrowserThumbnails.init();
+ });
+
+ scheduleIdleTask(
+ () => {
+ // Initialize the download manager some time after the app starts so that
+ // auto-resume downloads begin (such as after crashing or quitting with
+ // active downloads) and speeds up the first-load of the download manager UI.
+ // If the user manually opens the download manager before the timeout, the
+ // downloads will start right away, and initializing again won't hurt.
+ try {
+ DownloadsCommon.initializeAllDataLinks();
+ ChromeUtils.import(
+ "resource:///modules/DownloadsTaskbar.jsm",
+ {}
+ ).DownloadsTaskbar.registerIndicator(window);
+ if (AppConstants.platform == "macosx") {
+ ChromeUtils.import(
+ "resource:///modules/DownloadsMacFinderProgress.jsm"
+ ).DownloadsMacFinderProgress.register();
+ }
+ Services.telemetry.setEventRecordingEnabled("downloads", true);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+ { timeout: 10000 }
+ );
+
+ if (Win7Features) {
+ scheduleIdleTask(() => Win7Features.onOpenWindow());
+ }
+
+ scheduleIdleTask(async () => {
+ NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
+ });
+
+ scheduleIdleTask(reportRemoteSubframesEnabledTelemetry);
+
+ if (AppConstants.NIGHTLY_BUILD) {
+ scheduleIdleTask(() => {
+ FissionTestingUI.init();
+ });
+ }
+
+ scheduleIdleTask(() => {
+ gGfxUtils.init();
+ });
+
+ // This should always go last, since the idle tasks (except for the ones with
+ // timeouts) should execute in order. Note that this observer notification is
+ // not guaranteed to fire, since the window could close before we get here.
+ scheduleIdleTask(() => {
+ this.idleTaskPromiseResolve();
+ Services.obs.notifyObservers(
+ window,
+ "browser-idle-startup-tasks-finished"
+ );
+ });
+ },
+
+ // Returns the URI(s) to load at startup if it is immediately known, or a
+ // promise resolving to the URI to load.
+ get uriToLoadPromise() {
+ delete this.uriToLoadPromise;
+ return (this.uriToLoadPromise = (function() {
+ // window.arguments[0]: URI to load (string), or an nsIArray of
+ // nsISupportsStrings to load, or a xul:tab of
+ // a tabbrowser, which will be replaced by this
+ // window (for this case, all other arguments are
+ // ignored).
+ let uri = window.arguments?.[0];
+ if (!uri || uri instanceof window.XULElement) {
+ return null;
+ }
+
+ let defaultArgs = BrowserHandler.defaultArgs;
+
+ // If the given URI is different from the homepage, we want to load it.
+ if (uri != defaultArgs) {
+ AboutNewTab.noteNonDefaultStartup();
+
+ if (uri instanceof Ci.nsIArray) {
+ // Transform the nsIArray of nsISupportsString's into a JS Array of
+ // JS strings.
+ return Array.from(
+ uri.enumerate(Ci.nsISupportsString),
+ supportStr => supportStr.data
+ );
+ } else if (uri instanceof Ci.nsISupportsString) {
+ return uri.data;
+ }
+ return uri;
+ }
+
+ // The URI appears to be the the homepage. We want to load it only if
+ // session restore isn't about to override the homepage.
+ let willOverride = SessionStartup.willOverrideHomepage;
+ if (typeof willOverride == "boolean") {
+ return willOverride ? null : uri;
+ }
+ return willOverride.then(willOverrideHomepage =>
+ willOverrideHomepage ? null : uri
+ );
+ })());
+ },
+
+ // Calls the given callback with the URI to load at startup.
+ // Synchronously if possible, or after uriToLoadPromise resolves otherwise.
+ _callWithURIToLoad(callback) {
+ let uriToLoad = this.uriToLoadPromise;
+ if (uriToLoad && uriToLoad.then) {
+ uriToLoad.then(callback);
+ } else {
+ callback(uriToLoad);
+ }
+ },
+
+ onUnload() {
+ gUIDensity.uninit();
+
+ TabsInTitlebar.uninit();
+
+ ToolbarIconColor.uninit();
+
+ // In certain scenarios it's possible for unload to be fired before onload,
+ // (e.g. if the window is being closed after browser.js loads but before the
+ // load completes). In that case, there's nothing to do here.
+ if (!this._loadHandled) {
+ return;
+ }
+
+ // First clean up services initialized in gBrowserInit.onLoad (or those whose
+ // uninit methods don't depend on the services having been initialized).
+
+ CombinedStopReload.uninit();
+
+ gGestureSupport.init(false);
+
+ gHistorySwipeAnimation.uninit();
+
+ FullScreen.uninit();
+
+ gSync.uninit();
+
+ gExtensionsNotifications.uninit();
+
+ try {
+ gBrowser.removeProgressListener(window.XULBrowserWindow);
+ gBrowser.removeTabsProgressListener(window.TabsProgressListener);
+ } catch (ex) {}
+
+ PlacesToolbarHelper.uninit();
+
+ BookmarkingUI.uninit();
+
+ TabletModeUpdater.uninit();
+
+ gTabletModePageCounter.finish();
+
+ CaptivePortalWatcher.uninit();
+
+ SidebarUI.uninit();
+
+ DownloadsButton.uninit();
+
+ gAccessibilityServiceIndicator.uninit();
+
+ if (gToolbarKeyNavEnabled) {
+ ToolbarKeyboardNavigator.uninit();
+ }
+
+ BrowserSearch.uninit();
+
+ NewTabPagePreloading.removePreloadedBrowser(window);
+
+ // Now either cancel delayedStartup, or clean up the services initialized from
+ // it.
+ if (this._boundDelayedStartup) {
+ this._cancelDelayedStartup();
+ } else {
+ if (Win7Features) {
+ Win7Features.onCloseWindow();
+ }
+ Services.prefs.removeObserver(ctrlTab.prefName, ctrlTab);
+ ctrlTab.uninit();
+ gBrowserThumbnails.uninit();
+ gProtectionsHandler.uninit();
+ FullZoom.destroy();
+
+ Services.obs.removeObserver(gIdentityHandler, "perm-changed");
+ Services.obs.removeObserver(gRemoteControl, "marionette-listening");
+ Services.obs.removeObserver(gRemoteControl, "remote-listening");
+ Services.obs.removeObserver(
+ gSessionHistoryObserver,
+ "browser:purge-session-history"
+ );
+ Services.obs.removeObserver(
+ gStoragePressureObserver,
+ "QuotaManager::StoragePressure"
+ );
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-fullscreen-blocked"
+ );
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-origin-blocked"
+ );
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-confirmation"
+ );
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
+ Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ MenuTouchModeObserver.uninit();
+ }
+ BrowserOffline.uninit();
+ IndexedDBPromptHelper.uninit();
+ CanvasPermissionPromptHelper.uninit();
+ WebAuthnPromptHelper.uninit();
+ PanelUI.uninit();
+ }
+
+ // Final window teardown, do this last.
+ gBrowser.destroy();
+ window.XULBrowserWindow = null;
+ window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).XULBrowserWindow = null;
+ window.browserDOMWindow = null;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(
+ gBrowserInit,
+ "_firstContentWindowPaintDeferred",
+ () => PromiseUtils.defer()
+);
+
+gBrowserInit.idleTasksFinishedPromise = new Promise(resolve => {
+ gBrowserInit.idleTaskPromiseResolve = resolve;
+});
+
+function HandleAppCommandEvent(evt) {
+ switch (evt.command) {
+ case "Back":
+ BrowserBack();
+ break;
+ case "Forward":
+ BrowserForward();
+ break;
+ case "Reload":
+ BrowserReloadSkipCache();
+ break;
+ case "Stop":
+ if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") {
+ BrowserStop();
+ }
+ break;
+ case "Search":
+ BrowserSearch.webSearch();
+ break;
+ case "Bookmarks":
+ SidebarUI.toggle("viewBookmarksSidebar");
+ break;
+ case "Home":
+ BrowserHome();
+ break;
+ case "New":
+ BrowserOpenTab();
+ break;
+ case "Close":
+ BrowserCloseTabOrWindow();
+ break;
+ case "Find":
+ gLazyFindCommand("onFindCommand");
+ break;
+ case "Help":
+ openHelpLink("firefox-help");
+ break;
+ case "Open":
+ BrowserOpenFileWindow();
+ break;
+ case "Print":
+ PrintUtils.startPrintWindow(
+ "app_command",
+ gBrowser.selectedBrowser.browsingContext
+ );
+ break;
+ case "Save":
+ saveBrowser(gBrowser.selectedBrowser);
+ break;
+ case "SendMail":
+ MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
+ break;
+ default:
+ return;
+ }
+ evt.stopPropagation();
+ evt.preventDefault();
+}
+
+function gotoHistoryIndex(aEvent) {
+ aEvent = getRootEvent(aEvent);
+
+ let index = aEvent.target.getAttribute("index");
+ if (!index) {
+ return false;
+ }
+
+ let where = whereToOpenLink(aEvent);
+
+ if (where == "current") {
+ // Normal click. Go there in the current tab and update session history.
+
+ try {
+ gBrowser.gotoIndex(index);
+ } catch (ex) {
+ return false;
+ }
+ return true;
+ }
+ // Modified click. Go there in a new tab/window.
+
+ let historyindex = aEvent.target.getAttribute("historyindex");
+ duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex));
+ return true;
+}
+
+function BrowserForward(aEvent) {
+ let where = whereToOpenLink(aEvent, false, true);
+
+ if (where == "current") {
+ try {
+ gBrowser.goForward();
+ } catch (ex) {}
+ } else {
+ duplicateTabIn(gBrowser.selectedTab, where, 1);
+ }
+}
+
+function BrowserBack(aEvent) {
+ let where = whereToOpenLink(aEvent, false, true);
+
+ if (where == "current") {
+ try {
+ gBrowser.goBack();
+ } catch (ex) {}
+ } else {
+ duplicateTabIn(gBrowser.selectedTab, where, -1);
+ }
+}
+
+function BrowserHandleBackspace() {
+ switch (Services.prefs.getIntPref("browser.backspace_action")) {
+ case 0:
+ BrowserBack();
+ break;
+ case 1:
+ goDoCommand("cmd_scrollPageUp");
+ break;
+ }
+}
+
+function BrowserHandleShiftBackspace() {
+ switch (Services.prefs.getIntPref("browser.backspace_action")) {
+ case 0:
+ BrowserForward();
+ break;
+ case 1:
+ goDoCommand("cmd_scrollPageDown");
+ break;
+ }
+}
+
+function BrowserStop() {
+ gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
+}
+
+function BrowserReloadOrDuplicate(aEvent) {
+ aEvent = getRootEvent(aEvent);
+ let accelKeyPressed =
+ AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
+ var backgroundTabModifier = aEvent.button == 1 || accelKeyPressed;
+
+ if (aEvent.shiftKey && !backgroundTabModifier) {
+ BrowserReloadSkipCache();
+ return;
+ }
+
+ let where = whereToOpenLink(aEvent, false, true);
+ if (where == "current") {
+ BrowserReload();
+ } else {
+ duplicateTabIn(gBrowser.selectedTab, where);
+ }
+}
+
+function BrowserReload() {
+ if (gBrowser.currentURI.schemeIs("view-source")) {
+ // Bug 1167797: For view source, we always skip the cache
+ return BrowserReloadSkipCache();
+ }
+ const reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ BrowserReloadWithFlags(reloadFlags);
+}
+
+const kSkipCacheFlags =
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+function BrowserReloadSkipCache() {
+ // Bypass proxy and cache.
+ BrowserReloadWithFlags(kSkipCacheFlags);
+}
+
+function BrowserHome(aEvent) {
+ if (aEvent && "button" in aEvent && aEvent.button == 2) {
+ // right-click: do nothing
+ return;
+ }
+
+ var homePage = HomePage.get(window);
+ var where = whereToOpenLink(aEvent, false, true);
+ var urls;
+ var notifyObservers;
+
+ // Home page should open in a new tab when current tab is an app tab
+ if (where == "current" && gBrowser && gBrowser.selectedTab.pinned) {
+ where = "tab";
+ }
+
+ // openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages
+ switch (where) {
+ case "current":
+ // If we're going to load an initial page in the current tab as the
+ // home page, we set initialPageLoadedFromURLBar so that the URL
+ // bar is cleared properly (even during a remoteness flip).
+ if (isInitialPage(homePage)) {
+ gBrowser.selectedBrowser.initialPageLoadedFromUserAction = homePage;
+ }
+ loadOneOrMoreURIs(
+ homePage,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+ if (isBlankPageURL(homePage)) {
+ gURLBar.select();
+ } else {
+ gBrowser.selectedBrowser.focus();
+ }
+ notifyObservers = true;
+ break;
+ case "tabshifted":
+ case "tab":
+ urls = homePage.split("|");
+ var loadInBackground = Services.prefs.getBoolPref(
+ "browser.tabs.loadBookmarksInBackground",
+ false
+ );
+ // The homepage observer event should only be triggered when the homepage opens
+ // in the foreground. This is mostly to support the homepage changed by extension
+ // doorhanger which doesn't currently support background pages. This may change in
+ // bug 1438396.
+ notifyObservers = !loadInBackground;
+ gBrowser.loadTabs(urls, {
+ inBackground: loadInBackground,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ csp: null,
+ });
+ break;
+ case "window":
+ // OpenBrowserWindow will trigger the observer event, so no need to do so here.
+ notifyObservers = false;
+ OpenBrowserWindow();
+ break;
+ }
+ if (notifyObservers) {
+ // A notification for when a user has triggered their homepage. This is used
+ // to display a doorhanger explaining that an extension has modified the
+ // homepage, if necessary. Observers are only notified if the homepage
+ // becomes the active page.
+ Services.obs.notifyObservers(null, "browser-open-homepage-start");
+ }
+}
+
+function loadOneOrMoreURIs(aURIString, aTriggeringPrincipal, aCsp) {
+ // we're not a browser window, pass the URI string to a new browser window
+ if (window.location.href != AppConstants.BROWSER_CHROME_URL) {
+ window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "all,dialog=no",
+ aURIString
+ );
+ return;
+ }
+
+ // This function throws for certain malformed URIs, so use exception handling
+ // so that we don't disrupt startup
+ try {
+ gBrowser.loadTabs(aURIString.split("|"), {
+ inBackground: false,
+ replace: true,
+ triggeringPrincipal: aTriggeringPrincipal,
+ csp: aCsp,
+ });
+ } catch (e) {}
+}
+
+function openLocation(event) {
+ if (window.location.href == AppConstants.BROWSER_CHROME_URL) {
+ gURLBar.select();
+ gURLBar.view.autoOpen({ event });
+ return;
+ }
+
+ // If there's an open browser window, redirect the command there.
+ let win = getTopWin();
+ if (win) {
+ win.focus();
+ win.openLocation();
+ return;
+ }
+
+ // There are no open browser windows; open a new one.
+ window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no",
+ BROWSER_NEW_TAB_URL
+ );
+}
+
+function BrowserOpenTab(event) {
+ let where = "tab";
+ let relatedToCurrent = false;
+
+ if (event) {
+ where = whereToOpenLink(event, false, true);
+
+ switch (where) {
+ case "tab":
+ case "tabshifted":
+ // When accel-click or middle-click are used, open the new tab as
+ // related to the current tab.
+ relatedToCurrent = true;
+ break;
+ case "current":
+ where = "tab";
+ break;
+ }
+ }
+
+ // A notification intended to be useful for modular peformance tracking
+ // starting as close as is reasonably possible to the time when the user
+ // expressed the intent to open a new tab. Since there are a lot of
+ // entry points, this won't catch every single tab created, but most
+ // initiated by the user should go through here.
+ //
+ // Note 1: This notification gets notified with a promise that resolves
+ // with the linked browser when the tab gets created
+ // Note 2: This is also used to notify a user that an extension has changed
+ // the New Tab page.
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: new Promise(resolve => {
+ openTrustedLinkIn(BROWSER_NEW_TAB_URL, where, {
+ relatedToCurrent,
+ resolveOnNewTabCreated: resolve,
+ });
+ }),
+ },
+ "browser-open-newtab-start"
+ );
+}
+
+var gLastOpenDirectory = {
+ _lastDir: null,
+ get path() {
+ if (!this._lastDir || !this._lastDir.exists()) {
+ try {
+ this._lastDir = Services.prefs.getComplexValue(
+ "browser.open.lastDir",
+ Ci.nsIFile
+ );
+ if (!this._lastDir.exists()) {
+ this._lastDir = null;
+ }
+ } catch (e) {}
+ }
+ return this._lastDir;
+ },
+ set path(val) {
+ try {
+ if (!val || !val.isDirectory()) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+ this._lastDir = val.clone();
+
+ // Don't save the last open directory pref inside the Private Browsing mode
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ Services.prefs.setComplexValue(
+ "browser.open.lastDir",
+ Ci.nsIFile,
+ this._lastDir
+ );
+ }
+ },
+ reset() {
+ this._lastDir = null;
+ },
+};
+
+function BrowserOpenFileWindow() {
+ // Get filepicker component.
+ try {
+ const nsIFilePicker = Ci.nsIFilePicker;
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult == nsIFilePicker.returnOK) {
+ try {
+ if (fp.file) {
+ gLastOpenDirectory.path = fp.file.parent.QueryInterface(Ci.nsIFile);
+ }
+ } catch (ex) {}
+ openTrustedLinkIn(fp.fileURL.spec, "current");
+ }
+ };
+
+ fp.init(
+ window,
+ gNavigatorBundle.getString("openFile"),
+ nsIFilePicker.modeOpen
+ );
+ fp.appendFilters(
+ nsIFilePicker.filterAll |
+ nsIFilePicker.filterText |
+ nsIFilePicker.filterImages |
+ nsIFilePicker.filterXML |
+ nsIFilePicker.filterHTML
+ );
+ fp.displayDirectory = gLastOpenDirectory.path;
+ fp.open(fpCallback);
+ } catch (ex) {}
+}
+
+function BrowserCloseTabOrWindow(event) {
+ // If we're not a browser window, just close the window.
+ if (window.location.href != AppConstants.BROWSER_CHROME_URL) {
+ closeWindow(true);
+ return;
+ }
+
+ // In a multi-select context, close all selected tabs
+ if (gBrowser.multiSelectedTabsCount) {
+ gBrowser.removeMultiSelectedTabs();
+ return;
+ }
+
+ // Keyboard shortcuts that would close a tab that is pinned select the first
+ // unpinned tab instead.
+ if (
+ event &&
+ (event.ctrlKey || event.metaKey || event.altKey) &&
+ gBrowser.selectedTab.pinned
+ ) {
+ if (gBrowser.visibleTabs.length > gBrowser._numPinnedTabs) {
+ gBrowser.tabContainer.selectedIndex = gBrowser._numPinnedTabs;
+ }
+ return;
+ }
+
+ // If the current tab is the last one, this will close the window.
+ gBrowser.removeCurrentTab({ animate: true });
+}
+
+function BrowserTryToCloseWindow() {
+ if (WindowIsClosing()) {
+ window.close();
+ } // WindowIsClosing does all the necessary checks
+}
+
+function loadURI(
+ uri,
+ referrerInfo,
+ postData,
+ allowThirdPartyFixup,
+ userContextId,
+ originPrincipal,
+ originStoragePrincipal,
+ forceAboutBlankViewerInCurrent,
+ triggeringPrincipal,
+ allowInheritPrincipal = false,
+ csp = null
+) {
+ if (!triggeringPrincipal) {
+ throw new Error("Must load with a triggering Principal");
+ }
+
+ try {
+ openLinkIn(uri, "current", {
+ referrerInfo,
+ postData,
+ allowThirdPartyFixup,
+ userContextId,
+ originPrincipal,
+ originStoragePrincipal,
+ triggeringPrincipal,
+ csp,
+ forceAboutBlankViewerInCurrent,
+ allowInheritPrincipal,
+ });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+}
+
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function readFromClipboard() {
+ var url;
+
+ try {
+ // Create transferable that will transfer the text.
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+
+ trans.addDataFlavor("text/unicode");
+
+ // If available, use selection clipboard, otherwise global one
+ if (Services.clipboard.supportsSelectionClipboard()) {
+ Services.clipboard.getData(trans, Services.clipboard.kSelectionClipboard);
+ } else {
+ Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
+ }
+
+ var data = {};
+ trans.getTransferData("text/unicode", data);
+
+ if (data) {
+ data = data.value.QueryInterface(Ci.nsISupportsString);
+ url = data.data;
+ }
+ } catch (ex) {}
+
+ return url;
+}
+
+/**
+ * Open the View Source dialog.
+ *
+ * @param args
+ * An object with the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. You only need to provide this if you
+ * want to attempt to retrieve the document source from the network
+ * cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ */
+async function BrowserViewSourceOfDocument(args) {
+ // Check if external view source is enabled. If so, try it. If it fails,
+ // fallback to internal view source.
+ if (Services.prefs.getBoolPref("view_source.editor.external")) {
+ try {
+ await top.gViewSourceUtils.openInExternalEditor(args);
+ return;
+ } catch (data) {}
+ }
+
+ let tabBrowser = gBrowser;
+ let preferredRemoteType;
+ let initialBrowsingContextGroupId;
+ if (args.browser) {
+ preferredRemoteType = args.browser.remoteType;
+ initialBrowsingContextGroupId = args.browser.browsingContext.group.id;
+ } else {
+ if (!tabBrowser) {
+ throw new Error(
+ "BrowserViewSourceOfDocument should be passed the " +
+ "subject browser if called from a window without " +
+ "gBrowser defined."
+ );
+ }
+ // Some internal URLs (such as specific chrome: and about: URLs that are
+ // not yet remote ready) cannot be loaded in a remote browser. View
+ // source in tab expects the new view source browser's remoteness to match
+ // that of the original URL, so disable remoteness if necessary for this
+ // URL.
+ var oa = E10SUtils.predictOriginAttributes({ window });
+ preferredRemoteType = E10SUtils.getRemoteTypeForURI(
+ args.URL,
+ gMultiProcessBrowser,
+ gFissionBrowser,
+ E10SUtils.DEFAULT_REMOTE_TYPE,
+ null,
+ oa
+ );
+ }
+
+ // In the case of popups, we need to find a non-popup browser window.
+ if (!tabBrowser || !window.toolbar.visible) {
+ // This returns only non-popup browser windows by default.
+ let browserWindow = BrowserWindowTracker.getTopWindow();
+ tabBrowser = browserWindow.gBrowser;
+ }
+
+ const inNewWindow = !Services.prefs.getBoolPref("view_source.tab");
+
+ // `viewSourceInBrowser` will load the source content from the page
+ // descriptor for the tab (when possible) or fallback to the network if
+ // that fails. Either way, the view source module will manage the tab's
+ // location, so use "about:blank" here to avoid unnecessary redundant
+ // requests.
+ let tab = tabBrowser.loadOneTab("about:blank", {
+ relatedToCurrent: true,
+ inBackground: inNewWindow,
+ skipAnimation: inNewWindow,
+ preferredRemoteType,
+ initialBrowsingContextGroupId,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
+ top.gViewSourceUtils.viewSourceInBrowser(args);
+
+ if (inNewWindow) {
+ tabBrowser.hideTab(tab);
+ tabBrowser.replaceTabWithWindow(tab);
+ }
+}
+
+/**
+ * Opens the View Source dialog for the source loaded in the root
+ * top-level document of the browser. This is really just a
+ * convenience wrapper around BrowserViewSourceOfDocument.
+ *
+ * @param browser
+ * The browser that we want to load the source of.
+ */
+function BrowserViewSource(browser) {
+ BrowserViewSourceOfDocument({
+ browser,
+ outerWindowID: browser.outerWindowID,
+ URL: browser.currentURI.spec,
+ });
+}
+
+// documentURL - URL of the document to view, or null for this window's document
+// initialTab - name of the initial tab to display, or null for the first tab
+// imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted
+// browsingContext - the browsingContext of the frame that we want to view information about; can be null/omitted
+// browser - the browser containing the document we're interested in inspecting; can be null/omitted
+function BrowserPageInfo(
+ documentURL,
+ initialTab,
+ imageElement,
+ browsingContext,
+ browser
+) {
+ if (documentURL instanceof HTMLDocument) {
+ Deprecated.warning(
+ "Please pass the location URL instead of the document " +
+ "to BrowserPageInfo() as the first argument.",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1238180"
+ );
+ documentURL = documentURL.location;
+ }
+
+ let args = { initialTab, imageElement, browsingContext, browser };
+
+ documentURL = documentURL || window.gBrowser.selectedBrowser.currentURI.spec;
+
+ // Check for windows matching the url
+ for (let currentWindow of Services.wm.getEnumerator("Browser:page-info")) {
+ if (currentWindow.closed) {
+ continue;
+ }
+ if (
+ currentWindow.document.documentElement.getAttribute("relatedUrl") ==
+ documentURL
+ ) {
+ currentWindow.focus();
+ currentWindow.resetPageInfo(args);
+ return currentWindow;
+ }
+ }
+
+ // We didn't find a matching window, so open a new one.
+ return openDialog(
+ "chrome://browser/content/pageinfo/pageInfo.xhtml",
+ "",
+ "chrome,toolbar,dialog=no,resizable",
+ args
+ );
+}
+
+function UpdateUrlbarSearchSplitterState() {
+ var splitter = document.getElementById("urlbar-search-splitter");
+ var urlbar = document.getElementById("urlbar-container");
+ var searchbar = document.getElementById("search-container");
+
+ if (document.documentElement.getAttribute("customizing") == "true") {
+ if (splitter) {
+ splitter.remove();
+ }
+ return;
+ }
+
+ // If the splitter is already in the right place, we don't need to do anything:
+ if (
+ splitter &&
+ ((splitter.nextElementSibling == searchbar &&
+ splitter.previousElementSibling == urlbar) ||
+ (splitter.nextElementSibling == urlbar &&
+ splitter.previousElementSibling == searchbar))
+ ) {
+ return;
+ }
+
+ var ibefore = null;
+ if (urlbar && searchbar) {
+ if (urlbar.nextElementSibling == searchbar) {
+ ibefore = searchbar;
+ } else if (searchbar.nextElementSibling == urlbar) {
+ ibefore = urlbar;
+ }
+ }
+
+ if (ibefore) {
+ if (!splitter) {
+ splitter = document.createXULElement("splitter");
+ splitter.id = "urlbar-search-splitter";
+ splitter.setAttribute("resizebefore", "flex");
+ splitter.setAttribute("resizeafter", "flex");
+ splitter.setAttribute("skipintoolbarset", "true");
+ splitter.setAttribute("overflows", "false");
+ splitter.className = "chromeclass-toolbar-additional";
+ }
+ urlbar.parentNode.insertBefore(splitter, ibefore);
+ } else if (splitter) {
+ splitter.remove();
+ }
+}
+
+function UpdatePopupNotificationsVisibility() {
+ // Only need to do something if the PopupNotifications object for this window
+ // has already been initialized (i.e. its getter no longer exists).
+ if (Object.getOwnPropertyDescriptor(window, "PopupNotifications").get) {
+ return;
+ }
+
+ // Notify PopupNotifications that the visible anchors may have changed. This
+ // also checks the suppression state according to the "shouldSuppress"
+ // function defined earlier in this file.
+ PopupNotifications.anchorVisibilityChange();
+}
+
+function PageProxyClickHandler(aEvent) {
+ if (aEvent.button == 1 && Services.prefs.getBoolPref("middlemouse.paste")) {
+ middleMousePaste(aEvent);
+ }
+}
+
+/**
+ * Handle command events bubbling up from error page content
+ * or from about:newtab or from remote error pages that invoke
+ * us via async messaging.
+ */
+var BrowserOnClick = {
+ ignoreWarningLink(reason, blockedInfo, browsingContext) {
+ let triggeringPrincipal =
+ blockedInfo.triggeringPrincipal ||
+ _createNullPrincipalFromTabUserContextId();
+
+ // Allow users to override and continue through to the site,
+ // but add a notify bar as a reminder, so that they don't lose
+ // track after, e.g., tab switching.
+ browsingContext.loadURI(blockedInfo.uri, {
+ triggeringPrincipal,
+ flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
+ });
+
+ // We can't use browser.contentPrincipal which is principal of about:blocked
+ // Create one from uri with current principal origin attributes
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(blockedInfo.uri),
+ browsingContext.currentWindowGlobal.documentPrincipal.originAttributes
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "safe-browsing",
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ Ci.nsIPermissionManager.EXPIRE_SESSION
+ );
+
+ let buttons = [
+ {
+ label: gNavigatorBundle.getString(
+ "safebrowsing.getMeOutOfHereButton.label"
+ ),
+ accessKey: gNavigatorBundle.getString(
+ "safebrowsing.getMeOutOfHereButton.accessKey"
+ ),
+ callback() {
+ getMeOutOfHere(browsingContext);
+ },
+ },
+ ];
+
+ let title;
+ if (reason === "malware") {
+ let reportUrl = gSafeBrowsing.getReportURL("MalwareMistake", blockedInfo);
+ title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite");
+ // There's no button if we can not get report url, for example if the provider
+ // of blockedInfo is not Google
+ if (reportUrl) {
+ buttons[1] = {
+ label: gNavigatorBundle.getString(
+ "safebrowsing.notAnAttackButton.label"
+ ),
+ accessKey: gNavigatorBundle.getString(
+ "safebrowsing.notAnAttackButton.accessKey"
+ ),
+ callback() {
+ openTrustedLinkIn(reportUrl, "tab");
+ },
+ };
+ }
+ } else if (reason === "phishing") {
+ let reportUrl = gSafeBrowsing.getReportURL("PhishMistake", blockedInfo);
+ title = gNavigatorBundle.getString("safebrowsing.deceptiveSite");
+ // There's no button if we can not get report url, for example if the provider
+ // of blockedInfo is not Google
+ if (reportUrl) {
+ buttons[1] = {
+ label: gNavigatorBundle.getString(
+ "safebrowsing.notADeceptiveSiteButton.label"
+ ),
+ accessKey: gNavigatorBundle.getString(
+ "safebrowsing.notADeceptiveSiteButton.accessKey"
+ ),
+ callback() {
+ openTrustedLinkIn(reportUrl, "tab");
+ },
+ };
+ }
+ } else if (reason === "unwanted") {
+ title = gNavigatorBundle.getString("safebrowsing.reportedUnwantedSite");
+ // There is no button for reporting errors since Google doesn't currently
+ // provide a URL endpoint for these reports.
+ } else if (reason === "harmful") {
+ title = gNavigatorBundle.getString("safebrowsing.reportedHarmfulSite");
+ // There is no button for reporting errors since Google doesn't currently
+ // provide a URL endpoint for these reports.
+ }
+
+ SafeBrowsingNotificationBox.show(title, buttons);
+ },
+};
+
+/**
+ * Re-direct the browser to a known-safe page. This function is
+ * used when, for example, the user browses to a known malware page
+ * and is presented with about:blocked. The "Get me out of here!"
+ * button should take the user to the default start page so that even
+ * when their own homepage is infected, we can get them somewhere safe.
+ */
+function getMeOutOfHere(browsingContext) {
+ browsingContext.top.loadURI(getDefaultHomePage(), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), // Also needs to load homepage
+ });
+}
+
+/**
+ * Return the default start page for the cases when the user's own homepage is
+ * infected, so we can get them somewhere safe.
+ */
+function getDefaultHomePage() {
+ let url = BROWSER_NEW_TAB_URL;
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return url;
+ }
+ url = HomePage.getDefault();
+ // If url is a pipe-delimited set of pages, just take the first one.
+ if (url.includes("|")) {
+ url = url.split("|")[0];
+ }
+ return url;
+}
+
+function BrowserFullScreen() {
+ window.fullScreen = !window.fullScreen || BrowserHandler.kiosk;
+}
+
+function BrowserReloadWithFlags(reloadFlags) {
+ let unchangedRemoteness = [];
+
+ for (let tab of gBrowser.selectedTabs) {
+ let browser = tab.linkedBrowser;
+ let url = browser.currentURI.spec;
+ // We need to cache the content principal here because the browser will be
+ // reconstructed when the remoteness changes and the content prinicpal will
+ // be cleared after reconstruction.
+ let principal = tab.linkedBrowser.contentPrincipal;
+ if (gBrowser.updateBrowserRemotenessByURL(browser, url)) {
+ // If the remoteness has changed, the new browser doesn't have any
+ // information of what was loaded before, so we need to load the previous
+ // URL again.
+ if (tab.linkedPanel) {
+ loadBrowserURI(browser, url, principal);
+ } else {
+ // Shift to fully loaded browser and make
+ // sure load handler is instantiated.
+ tab.addEventListener(
+ "SSTabRestoring",
+ () => loadBrowserURI(browser, url, principal),
+ { once: true }
+ );
+ gBrowser._insertBrowser(tab);
+ }
+ } else {
+ unchangedRemoteness.push(tab);
+ }
+ }
+
+ if (!unchangedRemoteness.length) {
+ return;
+ }
+
+ // Reset temporary permissions on the remaining tabs to reload.
+ // This is done here because we only want to reset
+ // permissions on user reload.
+ for (let tab of unchangedRemoteness) {
+ SitePermissions.clearTemporaryPermissions(tab.linkedBrowser);
+ // Also reset DOS mitigations for the basic auth prompt on reload.
+ delete tab.linkedBrowser.authPromptAbuseCounter;
+ }
+ gIdentityHandler.hidePopup();
+
+ let handlingUserInput = window.windowUtils.isHandlingUserInput;
+
+ for (let tab of unchangedRemoteness) {
+ if (tab.linkedPanel) {
+ sendReloadMessage(tab);
+ } else {
+ // Shift to fully loaded browser and make
+ // sure load handler is instantiated.
+ tab.addEventListener("SSTabRestoring", () => sendReloadMessage(tab), {
+ once: true,
+ });
+ gBrowser._insertBrowser(tab);
+ }
+ }
+
+ function loadBrowserURI(browser, url, principal) {
+ browser.loadURI(url, {
+ flags: reloadFlags,
+ triggeringPrincipal: principal,
+ });
+ }
+
+ function sendReloadMessage(tab) {
+ tab.linkedBrowser.sendMessageToActor(
+ "Browser:Reload",
+ { flags: reloadFlags, handlingUserInput },
+ "BrowserTab"
+ );
+ }
+}
+
+function getSecurityInfo(securityInfoAsString) {
+ if (!securityInfoAsString) {
+ return null;
+ }
+
+ let securityInfo = gSerializationHelper.deserializeObject(
+ securityInfoAsString
+ );
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+
+ return securityInfo;
+}
+
+// TODO: can we pull getPEMString in from pippki.js instead of
+// duplicating them here?
+function getPEMString(cert) {
+ var derb64 = cert.getBase64DERString();
+ // 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"
+ );
+}
+
+var PrintPreviewListener = {
+ _printPreviewTab: null,
+ _simplifiedPrintPreviewTab: null,
+ _tabBeforePrintPreview: null,
+ _simplifyPageTab: null,
+ _lastRequestedPrintPreviewTab: null,
+
+ _createPPBrowser() {
+ let browser = this.getSourceBrowser();
+ let preferredRemoteType = browser.remoteType;
+ let initialBrowsingContextGroupId = browser.browsingContext.group.id;
+ let userContextId = browser.browsingContext.originAttributes.userContextId;
+ return gBrowser.loadOneTab("about:printpreview", {
+ inBackground: true,
+ preferredRemoteType,
+ initialBrowsingContextGroupId,
+ userContextId,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ },
+ getPrintPreviewBrowser() {
+ if (!this._printPreviewTab) {
+ this._printPreviewTab = this._createPPBrowser();
+ }
+ gBrowser._allowTabChange = true;
+ this._lastRequestedPrintPreviewTab = gBrowser.selectedTab = this._printPreviewTab;
+ gBrowser._allowTabChange = false;
+ return gBrowser.getBrowserForTab(this._printPreviewTab);
+ },
+ getSimplifiedPrintPreviewBrowser() {
+ if (!this._simplifiedPrintPreviewTab) {
+ this._simplifiedPrintPreviewTab = this._createPPBrowser();
+ }
+ gBrowser._allowTabChange = true;
+ this._lastRequestedPrintPreviewTab = gBrowser.selectedTab = this._simplifiedPrintPreviewTab;
+ gBrowser._allowTabChange = false;
+ return gBrowser.getBrowserForTab(this._simplifiedPrintPreviewTab);
+ },
+ createSimplifiedBrowser() {
+ let browser = this.getSourceBrowser();
+ let preferredRemoteType = browser.remoteType;
+ let initialBrowsingContextGroupId = browser.browsingContext.group.id;
+ this._simplifyPageTab = gBrowser.loadOneTab("about:printpreview", {
+ inBackground: true,
+ preferredRemoteType,
+ initialBrowsingContextGroupId,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ return this.getSimplifiedSourceBrowser();
+ },
+ getSourceBrowser() {
+ if (!this._tabBeforePrintPreview) {
+ this._tabBeforePrintPreview = gBrowser.selectedTab;
+ }
+ return this._tabBeforePrintPreview.linkedBrowser;
+ },
+ getSimplifiedSourceBrowser() {
+ return this._simplifyPageTab
+ ? gBrowser.getBrowserForTab(this._simplifyPageTab)
+ : null;
+ },
+ getNavToolbox() {
+ return gNavToolbox;
+ },
+ onEnter() {
+ // We might have accidentally switched tabs since the user invoked print
+ // preview
+ if (gBrowser.selectedTab != this._lastRequestedPrintPreviewTab) {
+ gBrowser.selectedTab = this._lastRequestedPrintPreviewTab;
+ }
+ gInPrintPreviewMode = true;
+ this._toggleAffectedChrome();
+ },
+ onExit() {
+ gBrowser._allowTabChange = true;
+ gBrowser.selectedTab = this._tabBeforePrintPreview;
+ gBrowser._allowTabChange = false;
+ this._tabBeforePrintPreview = null;
+ gInPrintPreviewMode = false;
+ this._toggleAffectedChrome();
+ let tabsToRemove = [
+ "_simplifyPageTab",
+ "_printPreviewTab",
+ "_simplifiedPrintPreviewTab",
+ ];
+ for (let tabProp of tabsToRemove) {
+ if (this[tabProp]) {
+ gBrowser.removeTab(this[tabProp]);
+ this[tabProp] = null;
+ }
+ }
+ gBrowser.deactivatePrintPreviewBrowsers();
+ this._lastRequestedPrintPreviewTab = null;
+ },
+ _toggleAffectedChrome() {
+ gNavToolbox.collapsed = gInPrintPreviewMode;
+
+ if (gInPrintPreviewMode) {
+ this._hideChrome();
+ } else {
+ this._showChrome();
+ }
+
+ TabsInTitlebar.allowedBy("print-preview", !gInPrintPreviewMode);
+ },
+ _hideChrome() {
+ this._chromeState = {};
+
+ this._chromeState.sidebarOpen = SidebarUI.isOpen;
+ this._sidebarCommand = SidebarUI.currentID;
+ SidebarUI.hide();
+
+ this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden;
+ if (gFindBarInitialized) {
+ gFindBar.close();
+ }
+
+ gBrowser.getNotificationBox().stack.hidden = true;
+ gNotificationBox.stack.hidden = true;
+ },
+ _showChrome() {
+ gNotificationBox.stack.hidden = false;
+ gBrowser.getNotificationBox().stack.hidden = false;
+
+ if (this._chromeState.findOpen) {
+ gLazyFindCommand("open");
+ }
+
+ if (this._chromeState.sidebarOpen) {
+ SidebarUI.show(this._sidebarCommand);
+ }
+ },
+
+ activateBrowser(browser) {
+ gBrowser.activateBrowserForPrintPreview(browser);
+ },
+};
+
+var browserDragAndDrop = {
+ canDropLink: aEvent => Services.droppedLinkHandler.canDropLink(aEvent, true),
+
+ dragOver(aEvent) {
+ if (this.canDropLink(aEvent)) {
+ aEvent.preventDefault();
+ }
+ },
+
+ getTriggeringPrincipal(aEvent) {
+ return Services.droppedLinkHandler.getTriggeringPrincipal(aEvent);
+ },
+
+ getCSP(aEvent) {
+ return Services.droppedLinkHandler.getCSP(aEvent);
+ },
+
+ validateURIsForDrop(aEvent, aURIs) {
+ return Services.droppedLinkHandler.validateURIsForDrop(aEvent, aURIs);
+ },
+
+ dropLinks(aEvent, aDisallowInherit) {
+ return Services.droppedLinkHandler.dropLinks(aEvent, aDisallowInherit);
+ },
+};
+
+var homeButtonObserver = {
+ onDrop(aEvent) {
+ // disallow setting home pages that inherit the principal
+ let links = browserDragAndDrop.dropLinks(aEvent, true);
+ if (links.length) {
+ let urls = [];
+ for (let link of links) {
+ if (link.url.includes("|")) {
+ urls.push(...link.url.split("|"));
+ } else {
+ urls.push(link.url);
+ }
+ }
+
+ try {
+ browserDragAndDrop.validateURIsForDrop(aEvent, urls);
+ } catch (e) {
+ return;
+ }
+
+ setTimeout(openHomeDialog, 0, urls.join("|"));
+ }
+ },
+
+ onDragOver(aEvent) {
+ if (HomePage.locked) {
+ return;
+ }
+ browserDragAndDrop.dragOver(aEvent);
+ aEvent.dropEffect = "link";
+ },
+ onDragExit(aEvent) {},
+};
+
+function openHomeDialog(aURL) {
+ var promptTitle = gNavigatorBundle.getString("droponhometitle");
+ var promptMsg;
+ if (aURL.includes("|")) {
+ promptMsg = gNavigatorBundle.getString("droponhomemsgMultiple");
+ } else {
+ promptMsg = gNavigatorBundle.getString("droponhomemsg");
+ }
+
+ var pressedVal = Services.prompt.confirmEx(
+ window,
+ promptTitle,
+ promptMsg,
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ { value: 0 }
+ );
+
+ if (pressedVal == 0) {
+ HomePage.set(aURL).catch(Cu.reportError);
+ }
+}
+
+var newTabButtonObserver = {
+ onDragOver(aEvent) {
+ browserDragAndDrop.dragOver(aEvent);
+ },
+ onDragExit(aEvent) {},
+ async onDrop(aEvent) {
+ let links = browserDragAndDrop.dropLinks(aEvent);
+ if (
+ links.length >=
+ Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
+ ) {
+ // Sync dialog cannot be used inside drop event handler.
+ let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
+ links.length,
+ window
+ );
+ if (!answer) {
+ return;
+ }
+ }
+
+ let where = aEvent.shiftKey ? "tabshifted" : "tab";
+ let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent);
+ let csp = browserDragAndDrop.getCSP(aEvent);
+ for (let link of links) {
+ if (link.url) {
+ let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url);
+ // Allow third-party services to fixup this URL.
+ openLinkIn(data.url, where, {
+ postData: data.postData,
+ allowThirdPartyFixup: true,
+ triggeringPrincipal,
+ csp,
+ });
+ }
+ }
+ },
+};
+
+var newWindowButtonObserver = {
+ onDragOver(aEvent) {
+ browserDragAndDrop.dragOver(aEvent);
+ },
+ onDragExit(aEvent) {},
+ async onDrop(aEvent) {
+ let links = browserDragAndDrop.dropLinks(aEvent);
+ if (
+ links.length >=
+ Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
+ ) {
+ // Sync dialog cannot be used inside drop event handler.
+ let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
+ links.length,
+ window
+ );
+ if (!answer) {
+ return;
+ }
+ }
+
+ let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent);
+ let csp = browserDragAndDrop.getCSP(aEvent);
+ for (let link of links) {
+ if (link.url) {
+ let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url);
+ // Allow third-party services to fixup this URL.
+ openLinkIn(data.url, "window", {
+ // TODO fix allowInheritPrincipal
+ // (this is required by javascript: drop to the new window) Bug 1475201
+ allowInheritPrincipal: true,
+ postData: data.postData,
+ allowThirdPartyFixup: true,
+ triggeringPrincipal,
+ csp,
+ });
+ }
+ }
+ },
+};
+
+const BrowserSearch = {
+ _searchInitComplete: false,
+
+ init() {
+ Services.obs.addObserver(this, "browser-search-engine-modified");
+ },
+
+ delayedStartupInit() {
+ // Asynchronously initialize the search service if necessary, to get the
+ // current engine for working out the placeholder.
+ this._updateURLBarPlaceholderFromDefaultEngine(
+ PrivateBrowsingUtils.isWindowPrivate(window),
+ // Delay the update for this until so that we don't change it while
+ // the user is looking at it / isn't expecting it.
+ true
+ ).then(() => {
+ this._searchInitComplete = true;
+ });
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ },
+
+ observe(engine, topic, data) {
+ // There are two kinds of search engine objects, nsISearchEngine objects and
+ // plain { uri, title, icon } objects. `engine` in this method is the
+ // former. The browser.engines and browser.hiddenEngines arrays are the
+ // latter, and they're the engines offered by the the page in the browser.
+ //
+ // The two types of engines are currently related by their titles/names,
+ // although that may change; see bug 335102.
+ let engineName = engine.wrappedJSObject.name;
+ switch (data) {
+ case "engine-removed":
+ // An engine was removed from the search service. If a page is offering
+ // the engine, then the engine needs to be added back to the corresponding
+ // browser's offered engines.
+ this._addMaybeOfferedEngine(engineName);
+ break;
+ case "engine-added":
+ // An engine was added to the search service. If a page is offering the
+ // engine, then the engine needs to be removed from the corresponding
+ // browser's offered engines.
+ this._removeMaybeOfferedEngine(engineName);
+ break;
+ case "engine-default":
+ if (
+ this._searchInitComplete &&
+ !PrivateBrowsingUtils.isWindowPrivate(window)
+ ) {
+ this._updateURLBarPlaceholder(engineName, false);
+ }
+ break;
+ case "engine-default-private":
+ if (
+ this._searchInitComplete &&
+ PrivateBrowsingUtils.isWindowPrivate(window)
+ ) {
+ this._updateURLBarPlaceholder(engineName, true);
+ }
+ break;
+ }
+ },
+
+ _addMaybeOfferedEngine(engineName) {
+ let selectedBrowserOffersEngine = false;
+ for (let browser of gBrowser.browsers) {
+ for (let i = 0; i < (browser.hiddenEngines || []).length; i++) {
+ if (browser.hiddenEngines[i].title == engineName) {
+ if (!browser.engines) {
+ browser.engines = [];
+ }
+ browser.engines.push(browser.hiddenEngines[i]);
+ browser.hiddenEngines.splice(i, 1);
+ if (browser == gBrowser.selectedBrowser) {
+ selectedBrowserOffersEngine = true;
+ }
+ break;
+ }
+ }
+ }
+ if (selectedBrowserOffersEngine) {
+ this.updateOpenSearchBadge();
+ }
+ },
+
+ _removeMaybeOfferedEngine(engineName) {
+ let selectedBrowserOffersEngine = false;
+ for (let browser of gBrowser.browsers) {
+ for (let i = 0; i < (browser.engines || []).length; i++) {
+ if (browser.engines[i].title == engineName) {
+ if (!browser.hiddenEngines) {
+ browser.hiddenEngines = [];
+ }
+ browser.hiddenEngines.push(browser.engines[i]);
+ browser.engines.splice(i, 1);
+ if (browser == gBrowser.selectedBrowser) {
+ selectedBrowserOffersEngine = true;
+ }
+ break;
+ }
+ }
+ }
+ if (selectedBrowserOffersEngine) {
+ this.updateOpenSearchBadge();
+ }
+ },
+
+ /**
+ * Initializes the urlbar placeholder to the pre-saved engine name. We do this
+ * via a preference, to avoid needing to synchronously init the search service.
+ *
+ * This should be called around the time of DOMContentLoaded, so that it is
+ * initialized quickly before the user sees anything.
+ *
+ * Note: If the preference doesn't exist, we don't do anything as the default
+ * placeholder is a string which doesn't have the engine name; however, this
+ * can be overridden using the `force` parameter.
+ *
+ * @param {Boolean} force If true and the preference doesn't exist, the
+ * placeholder will be set to the default version
+ * without an engine name ("Search or enter address").
+ */
+ initPlaceHolder(force = false) {
+ const prefName =
+ "browser.urlbar.placeholderName" +
+ (PrivateBrowsingUtils.isWindowPrivate(window) ? ".private" : "");
+ let engineName = Services.prefs.getStringPref(prefName, "");
+ if (engineName || force) {
+ // We can do this directly, since we know we're at DOMContentLoaded.
+ this._setURLBarPlaceholder(engineName);
+ }
+ },
+
+ /**
+ * This is a wrapper around '_updateURLBarPlaceholder' that uses the
+ * appropriate default engine to get the engine name.
+ *
+ * @param {Boolean} isPrivate Set to true if this is a private window.
+ * @param {Boolean} [delayUpdate] Set to true, to delay update until the
+ * placeholder is not displayed.
+ */
+ async _updateURLBarPlaceholderFromDefaultEngine(
+ isPrivate,
+ delayUpdate = false
+ ) {
+ const getDefault = isPrivate
+ ? Services.search.getDefaultPrivate
+ : Services.search.getDefault;
+ let defaultEngine = await getDefault();
+
+ this._updateURLBarPlaceholder(defaultEngine.name, isPrivate, delayUpdate);
+ },
+
+ /**
+ * Updates the URLBar placeholder for the specified engine, delaying the
+ * update if required. This also saves the current engine name in preferences
+ * for the next restart.
+ *
+ * Note: The engine name will only be displayed for built-in engines, as we
+ * know they should have short names.
+ *
+ * @param {String} engineName The search engine name to use for the update.
+ * @param {Boolean} isPrivate Set to true if this is a private window.
+ * @param {Boolean} [delayUpdate] Set to true, to delay update until the
+ * placeholder is not displayed.
+ */
+ _updateURLBarPlaceholder(engineName, isPrivate, delayUpdate = false) {
+ if (!engineName) {
+ throw new Error("Expected an engineName to be specified");
+ }
+
+ const engine = Services.search.getEngineByName(engineName);
+ const prefName =
+ "browser.urlbar.placeholderName" + (isPrivate ? ".private" : "");
+ if (engine.isAppProvided) {
+ Services.prefs.setStringPref(prefName, engineName);
+ } else {
+ Services.prefs.clearUserPref(prefName);
+ // Set the engine name to an empty string for non-default engines, which'll
+ // make sure we display the default placeholder string.
+ engineName = "";
+ }
+
+ // Only delay if requested, and we're not displaying text in the URL bar
+ // currently.
+ if (delayUpdate && !gURLBar.value) {
+ // Delays changing the URL Bar placeholder until the user is not going to be
+ // seeing it, e.g. when there is a value entered in the bar, or if there is
+ // a tab switch to a tab which has a url loaded. We delay the update until
+ // the user is out of search mode since an alternative placeholder is used
+ // in search mode.
+ let placeholderUpdateListener = () => {
+ if (gURLBar.value && !gURLBar.searchMode) {
+ // By the time the user has switched, they may have changed the engine
+ // again, so we need to call this function again but with the
+ // new engine name.
+ // No need to await for this to finish, we're in a listener here anyway.
+ this._updateURLBarPlaceholderFromDefaultEngine(isPrivate, false);
+ gURLBar.removeEventListener("input", placeholderUpdateListener);
+ gBrowser.tabContainer.removeEventListener(
+ "TabSelect",
+ placeholderUpdateListener
+ );
+ }
+ };
+
+ gURLBar.addEventListener("input", placeholderUpdateListener);
+ gBrowser.tabContainer.addEventListener(
+ "TabSelect",
+ placeholderUpdateListener
+ );
+ } else if (!gURLBar.searchMode) {
+ this._setURLBarPlaceholder(engineName);
+ }
+ },
+
+ /**
+ * Sets the URLBar placeholder to either something based on the engine name,
+ * or the default placeholder.
+ *
+ * @param {String} name The name of the engine to use, an empty string if to
+ * use the default placeholder.
+ */
+ _setURLBarPlaceholder(name) {
+ document.l10n.setAttributes(
+ gURLBar.inputField,
+ name ? "urlbar-placeholder-with-name" : "urlbar-placeholder",
+ name ? { name } : undefined
+ );
+ },
+
+ addEngine(browser, engine, uri) {
+ if (!this._searchInitComplete) {
+ // We haven't finished initialising search yet. This means we can't
+ // call getEngineByName here. Since this is only on start-up and unlikely
+ // to happen in the normal case, we'll just return early rather than
+ // trying to handle it asynchronously.
+ return;
+ }
+ // Check to see whether we've already added an engine with this title
+ if (browser.engines) {
+ if (browser.engines.some(e => e.title == engine.title)) {
+ return;
+ }
+ }
+
+ var hidden = false;
+ // If this engine (identified by title) is already in the list, add it
+ // to the list of hidden engines rather than to the main list.
+ // XXX This will need to be changed when engines are identified by URL;
+ // see bug 335102.
+ if (Services.search.getEngineByName(engine.title)) {
+ hidden = true;
+ }
+
+ var engines = (hidden ? browser.hiddenEngines : browser.engines) || [];
+
+ engines.push({
+ uri: engine.href,
+ title: engine.title,
+ get icon() {
+ return browser.mIconURL;
+ },
+ });
+
+ if (hidden) {
+ browser.hiddenEngines = engines;
+ } else {
+ browser.engines = engines;
+ if (browser == gBrowser.selectedBrowser) {
+ this.updateOpenSearchBadge();
+ }
+ }
+ },
+
+ /**
+ * Update the browser UI to show whether or not additional engines are
+ * available when a page is loaded or the user switches tabs to a page that
+ * has search engines.
+ */
+ updateOpenSearchBadge() {
+ BrowserPageActions.addSearchEngine.updateEngines();
+
+ var searchBar = this.searchBar;
+ if (!searchBar) {
+ return;
+ }
+
+ var engines = gBrowser.selectedBrowser.engines;
+ if (engines && engines.length) {
+ searchBar.setAttribute("addengines", "true");
+ } else {
+ searchBar.removeAttribute("addengines");
+ }
+ },
+
+ /**
+ * Focuses the search bar if present on the toolbar, or the address bar,
+ * putting it in search mode. Will do so in an existing non-popup browser
+ * window or open a new one if necessary.
+ */
+ webSearch: function BrowserSearch_webSearch() {
+ if (
+ window.location.href != AppConstants.BROWSER_CHROME_URL ||
+ gURLBar.readOnly
+ ) {
+ let win = getTopWin(true);
+ if (win) {
+ // If there's an open browser window, it should handle this command
+ win.focus();
+ win.BrowserSearch.webSearch();
+ } else {
+ // If there are no open browser windows, open a new one
+ var observer = function(subject, topic, data) {
+ if (subject == win) {
+ BrowserSearch.webSearch();
+ Services.obs.removeObserver(
+ observer,
+ "browser-delayed-startup-finished"
+ );
+ }
+ };
+ win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no",
+ "about:blank"
+ );
+ Services.obs.addObserver(observer, "browser-delayed-startup-finished");
+ }
+ return;
+ }
+
+ let focusUrlBarIfSearchFieldIsNotActive = function(aSearchBar) {
+ if (!aSearchBar || document.activeElement != aSearchBar.textbox) {
+ // Limit the results to search suggestions, like the search bar.
+ gURLBar.searchModeShortcut();
+ }
+ };
+
+ let searchBar = this.searchBar;
+ let placement = CustomizableUI.getPlacementOfWidget("search-container");
+ let focusSearchBar = () => {
+ searchBar = this.searchBar;
+ searchBar.select();
+ focusUrlBarIfSearchFieldIsNotActive(searchBar);
+ };
+ if (
+ placement &&
+ searchBar &&
+ ((searchBar.parentNode.getAttribute("overflowedItem") == "true" &&
+ placement.area == CustomizableUI.AREA_NAVBAR) ||
+ placement.area == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL)
+ ) {
+ let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+ navBar.overflowable.show().then(focusSearchBar);
+ return;
+ }
+ if (searchBar) {
+ if (window.fullScreen) {
+ FullScreen.showNavToolbox();
+ }
+ searchBar.select();
+ }
+ focusUrlBarIfSearchFieldIsNotActive(searchBar);
+ },
+
+ /**
+ * Loads a search results page, given a set of search terms. Uses the current
+ * engine if the search bar is visible, or the default engine otherwise.
+ *
+ * @param searchText
+ * The search terms to use for the search.
+ * @param where
+ * String indicating where the search should load. Most commonly used
+ * are 'tab' or 'window', defaults to 'current'.
+ * @param usePrivate
+ * Whether to use the Private Browsing mode default search engine.
+ * Defaults to `false`.
+ * @param purpose [optional]
+ * A string meant to indicate the context of the search request. This
+ * allows the search service to provide a different nsISearchSubmission
+ * depending on e.g. where the search is triggered in the UI.
+ * @param triggeringPrincipal
+ * The principal to use for a new window or tab.
+ * @param csp
+ * The content security policy to use for a new window or tab.
+ * @param engine [optional]
+ * The search engine to use for the search.
+ * @param tab [optional]
+ * The tab to show the search result.
+ *
+ * @return engine The search engine used to perform a search, or null if no
+ * search was performed.
+ */
+ async _loadSearch(
+ searchText,
+ where,
+ usePrivate,
+ purpose,
+ triggeringPrincipal,
+ csp,
+ engine = null,
+ tab = null
+ ) {
+ if (!triggeringPrincipal) {
+ throw new Error(
+ "Required argument triggeringPrincipal missing within _loadSearch"
+ );
+ }
+
+ if (!engine) {
+ engine = usePrivate
+ ? await Services.search.getDefaultPrivate()
+ : await Services.search.getDefault();
+ }
+
+ let submission = engine.getSubmission(searchText, null, purpose); // HTML response
+
+ // getSubmission can return null if the engine doesn't have a URL
+ // with a text/html response type. This is unlikely (since
+ // SearchService._addEngineToStore() should fail for such an engine),
+ // but let's be on the safe side.
+ if (!submission) {
+ return null;
+ }
+
+ let inBackground = Services.prefs.getBoolPref(
+ "browser.search.context.loadInBackground"
+ );
+
+ openLinkIn(submission.uri.spec, where || "current", {
+ private: usePrivate && !PrivateBrowsingUtils.isWindowPrivate(window),
+ postData: submission.postData,
+ inBackground,
+ relatedToCurrent: true,
+ triggeringPrincipal,
+ csp,
+ targetBrowser: tab?.linkedBrowser,
+ });
+
+ return { engine, url: submission.uri };
+ },
+
+ /**
+ * Perform a search initiated from the context menu.
+ *
+ * This should only be called from the context menu. See
+ * BrowserSearch.loadSearch for the preferred API.
+ */
+ async loadSearchFromContext(terms, usePrivate, triggeringPrincipal, csp) {
+ let { engine, url } = await BrowserSearch._loadSearch(
+ terms,
+ usePrivate && !PrivateBrowsingUtils.isWindowPrivate(window)
+ ? "window"
+ : "tab",
+ usePrivate,
+ "contextmenu",
+ Services.scriptSecurityManager.createNullPrincipal(
+ triggeringPrincipal.originAttributes
+ ),
+ csp
+ );
+ if (engine) {
+ BrowserSearchTelemetry.recordSearch(
+ gBrowser.selectedBrowser,
+ engine,
+ "contextmenu",
+ { url }
+ );
+ }
+ },
+
+ /**
+ * Perform a search initiated from the command line.
+ */
+ async loadSearchFromCommandLine(terms, usePrivate, triggeringPrincipal, csp) {
+ let { engine, url } = await BrowserSearch._loadSearch(
+ terms,
+ "current",
+ usePrivate,
+ "system",
+ triggeringPrincipal,
+ csp
+ );
+ if (engine) {
+ BrowserSearchTelemetry.recordSearch(
+ gBrowser.selectedBrowser,
+ engine,
+ "system",
+ { url }
+ );
+ }
+ },
+
+ /**
+ * Perform a search initiated from an extension.
+ */
+ async loadSearchFromExtension(terms, engine, tab, triggeringPrincipal) {
+ const result = await BrowserSearch._loadSearch(
+ terms,
+ tab ? "current" : "tab",
+ PrivateBrowsingUtils.isWindowPrivate(window),
+ "webextension",
+ triggeringPrincipal,
+ null,
+ engine,
+ tab
+ );
+
+ BrowserSearchTelemetry.recordSearch(
+ gBrowser.selectedBrowser,
+ result.engine,
+ "webextension",
+ { url: result.url }
+ );
+ },
+
+ pasteAndSearch(event) {
+ BrowserSearch.searchBar.select();
+ goDoCommand("cmd_paste");
+ BrowserSearch.searchBar.handleSearchCommand(event);
+ },
+
+ /**
+ * Returns the search bar element if it is present in the toolbar, null otherwise.
+ */
+ get searchBar() {
+ return document.getElementById("searchbar");
+ },
+
+ get searchEnginesURL() {
+ return formatURL("browser.search.searchEnginesURL", true);
+ },
+
+ loadAddEngines: function BrowserSearch_loadAddEngines() {
+ var newWindowPref = Services.prefs.getIntPref(
+ "browser.link.open_newwindow"
+ );
+ var where = newWindowPref == 3 ? "tab" : "window";
+ openTrustedLinkIn(this.searchEnginesURL, where);
+ },
+};
+
+XPCOMUtils.defineConstant(this, "BrowserSearch", BrowserSearch);
+
+function CreateContainerTabMenu(event) {
+ createUserContextMenu(event, {
+ useAccessKeys: false,
+ showDefaultTab: true,
+ });
+}
+
+function FillHistoryMenu(aParent) {
+ // Lazily add the hover listeners on first showing and never remove them
+ if (!aParent.hasStatusListener) {
+ // Show history item's uri in the status bar when hovering, and clear on exit
+ aParent.addEventListener("DOMMenuItemActive", function(aEvent) {
+ // Only the current page should have the checked attribute, so skip it
+ if (!aEvent.target.hasAttribute("checked")) {
+ XULBrowserWindow.setOverLink(aEvent.target.getAttribute("uri"));
+ }
+ });
+ aParent.addEventListener("DOMMenuItemInactive", function() {
+ XULBrowserWindow.setOverLink("");
+ });
+
+ aParent.hasStatusListener = true;
+ }
+
+ // Remove old entries if any
+ let children = aParent.children;
+ for (var i = children.length - 1; i >= 0; --i) {
+ if (children[i].hasAttribute("index")) {
+ aParent.removeChild(children[i]);
+ }
+ }
+
+ const MAX_HISTORY_MENU_ITEMS = 15;
+
+ const tooltipBack = gNavigatorBundle.getString("tabHistory.goBack");
+ const tooltipCurrent = gNavigatorBundle.getString("tabHistory.current");
+ const tooltipForward = gNavigatorBundle.getString("tabHistory.goForward");
+
+ function updateSessionHistory(sessionHistory, initial) {
+ let count = sessionHistory.entries.length;
+
+ if (!initial) {
+ if (count <= 1) {
+ // if there is only one entry now, close the popup.
+ aParent.hidePopup();
+ return;
+ } else if (aParent.id != "backForwardMenu" && !aParent.parentNode.open) {
+ // if the popup wasn't open before, but now needs to be, reopen the menu.
+ // It should trigger FillHistoryMenu again. This might happen with the
+ // delay from click-and-hold menus but skip this for the context menu
+ // (backForwardMenu) rather than figuring out how the menu should be
+ // positioned and opened as it is an extreme edgecase.
+ aParent.parentNode.open = true;
+ return;
+ }
+ }
+
+ let index = sessionHistory.index;
+ let half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2);
+ let start = Math.max(index - half_length, 0);
+ let end = Math.min(
+ start == 0 ? MAX_HISTORY_MENU_ITEMS : index + half_length + 1,
+ count
+ );
+ if (end == count) {
+ start = Math.max(count - MAX_HISTORY_MENU_ITEMS, 0);
+ }
+
+ let existingIndex = 0;
+
+ for (let j = end - 1; j >= start; j--) {
+ let entry = sessionHistory.entries[j];
+ // Explicitly check for "false" to stay backwards-compatible with session histories
+ // from before the hasUserInteraction was implemented.
+ if (
+ BrowserUtils.navigationRequireUserInteraction &&
+ entry.hasUserInteraction === false &&
+ // Always allow going to the first and last navigation points.
+ j != end - 1 &&
+ j != start
+ ) {
+ continue;
+ }
+ let uri = entry.url;
+
+ let item =
+ existingIndex < children.length
+ ? children[existingIndex]
+ : document.createXULElement("menuitem");
+
+ item.setAttribute("uri", uri);
+ item.setAttribute("label", entry.title || uri);
+ item.setAttribute("index", j);
+
+ // Cache this so that gotoHistoryIndex doesn't need the original index
+ item.setAttribute("historyindex", j - index);
+
+ if (j != index) {
+ // Use list-style-image rather than the image attribute in order to
+ // allow CSS to override this.
+ item.style.listStyleImage = `url(page-icon:${uri})`;
+ }
+
+ if (j < index) {
+ item.className =
+ "unified-nav-back menuitem-iconic menuitem-with-favicon";
+ item.setAttribute("tooltiptext", tooltipBack);
+ } else if (j == index) {
+ item.setAttribute("type", "radio");
+ item.setAttribute("checked", "true");
+ item.className = "unified-nav-current";
+ item.setAttribute("tooltiptext", tooltipCurrent);
+ } else {
+ item.className =
+ "unified-nav-forward menuitem-iconic menuitem-with-favicon";
+ item.setAttribute("tooltiptext", tooltipForward);
+ }
+
+ if (!item.parentNode) {
+ aParent.appendChild(item);
+ }
+
+ existingIndex++;
+ }
+
+ if (!initial) {
+ let existingLength = children.length;
+ while (existingIndex < existingLength) {
+ aParent.removeChild(aParent.lastElementChild);
+ existingIndex++;
+ }
+ }
+ }
+
+ let sessionHistory = SessionStore.getSessionHistory(
+ gBrowser.selectedTab,
+ updateSessionHistory
+ );
+ if (!sessionHistory) {
+ return false;
+ }
+
+ // don't display the popup for a single item
+ if (sessionHistory.entries.length <= 1) {
+ return false;
+ }
+
+ updateSessionHistory(sessionHistory, true);
+ return true;
+}
+
+function BrowserDownloadsUI() {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ openTrustedLinkIn("about:downloads", "tab");
+ } else {
+ PlacesCommandHook.showPlacesOrganizer("Downloads");
+ }
+}
+
+function toOpenWindowByType(inType, uri, features) {
+ var topWindow = Services.wm.getMostRecentWindow(inType);
+
+ if (topWindow) {
+ topWindow.focus();
+ } else if (features) {
+ window.open(uri, "_blank", features);
+ } else {
+ window.open(
+ uri,
+ "_blank",
+ "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"
+ );
+ }
+}
+
+/**
+ * Open a new browser window.
+ *
+ * @param {Object} options
+ * {
+ * private: A boolean indicating if the window should be
+ * private
+ * remote: A boolean indicating if the window should run
+ * remote browser tabs or not. If omitted, the window
+ * will choose the profile default state.
+ * fission: A boolean indicating if the window should run
+ * with fission enabled or not. If omitted, the window
+ * will choose the profile default state.
+ * }
+ * @return a reference to the new window.
+ */
+function OpenBrowserWindow(options) {
+ var telemetryObj = {};
+ TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj);
+
+ var defaultArgs = BrowserHandler.defaultArgs;
+ var wintype = document.documentElement.getAttribute("windowtype");
+
+ var extraFeatures = "";
+ if (options && options.private && PrivateBrowsingUtils.enabled) {
+ extraFeatures = ",private";
+ if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ // Force the new window to load about:privatebrowsing instead of the default home page
+ defaultArgs = "about:privatebrowsing";
+ }
+ } else {
+ extraFeatures = ",non-private";
+ }
+
+ if (options && options.remote) {
+ extraFeatures += ",remote";
+ } else if (options && options.remote === false) {
+ extraFeatures += ",non-remote";
+ }
+
+ if (options && options.fission) {
+ extraFeatures += ",fission";
+ } else if (options && options.fission === false) {
+ extraFeatures += ",non-fission";
+ }
+
+ // If the window is maximized, we want to skip the animation, since we're
+ // going to be taking up most of the screen anyways, and we want to optimize
+ // for showing the user a useful window as soon as possible.
+ if (window.windowState == window.STATE_MAXIMIZED) {
+ extraFeatures += ",suppressanimation";
+ }
+
+ // if and only if the current window is a browser window and it has a document with a character
+ // set, then extract the current charset menu setting from the current document and use it to
+ // initialize the new browser window...
+ var win;
+ if (
+ window &&
+ wintype == "navigator:browser" &&
+ window.content &&
+ window.content.document
+ ) {
+ var DocCharset = window.content.document.characterSet;
+ let charsetArg = "charset=" + DocCharset;
+
+ // we should "inherit" the charset menu setting in a new window
+ win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no" + extraFeatures,
+ defaultArgs,
+ charsetArg
+ );
+ } else {
+ // forget about the charset information.
+ win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no" + extraFeatures,
+ defaultArgs
+ );
+ }
+
+ win.addEventListener(
+ "MozAfterPaint",
+ () => {
+ TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj);
+ if (
+ Services.prefs.getIntPref("browser.startup.page") == 1 &&
+ defaultArgs == HomePage.get()
+ ) {
+ // A notification for when a user has triggered their homepage. This is used
+ // to display a doorhanger explaining that an extension has modified the
+ // homepage, if necessary.
+ Services.obs.notifyObservers(win, "browser-open-homepage-start");
+ }
+ },
+ { once: true }
+ );
+
+ return win;
+}
+
+/**
+ * Update the global flag that tracks whether or not any edit UI (the Edit menu,
+ * edit-related items in the context menu, and edit-related toolbar buttons
+ * is visible, then update the edit commands' enabled state accordingly. We use
+ * this flag to skip updating the edit commands on focus or selection changes
+ * when no UI is visible to improve performance (including pageload performance,
+ * since focus changes when you load a new page).
+ *
+ * If UI is visible, we use goUpdateGlobalEditMenuItems to set the commands'
+ * enabled state so the UI will reflect it appropriately.
+ *
+ * If the UI isn't visible, we enable all edit commands so keyboard shortcuts
+ * still work and just lazily disable them as needed when the user presses a
+ * shortcut.
+ *
+ * This doesn't work on Mac, since Mac menus flash when users press their
+ * keyboard shortcuts, so edit UI is essentially always visible on the Mac,
+ * and we need to always update the edit commands. Thus on Mac this function
+ * is a no op.
+ */
+function updateEditUIVisibility() {
+ if (AppConstants.platform == "macosx") {
+ return;
+ }
+
+ let editMenuPopupState = document.getElementById("menu_EditPopup").state;
+ let contextMenuPopupState = document.getElementById("contentAreaContextMenu")
+ .state;
+ let placesContextMenuPopupState = document.getElementById("placesContext")
+ .state;
+
+ let oldVisible = gEditUIVisible;
+
+ // The UI is visible if the Edit menu is opening or open, if the context menu
+ // is open, or if the toolbar has been customized to include the Cut, Copy,
+ // or Paste toolbar buttons.
+ gEditUIVisible =
+ editMenuPopupState == "showing" ||
+ editMenuPopupState == "open" ||
+ contextMenuPopupState == "showing" ||
+ contextMenuPopupState == "open" ||
+ placesContextMenuPopupState == "showing" ||
+ placesContextMenuPopupState == "open";
+ const kOpenPopupStates = ["showing", "open"];
+ if (!gEditUIVisible) {
+ // Now check the edit-controls toolbar buttons.
+ let placement = CustomizableUI.getPlacementOfWidget("edit-controls");
+ let areaType = placement ? CustomizableUI.getAreaType(placement.area) : "";
+ if (areaType == CustomizableUI.TYPE_MENU_PANEL) {
+ let customizablePanel = PanelUI.overflowPanel;
+ gEditUIVisible = kOpenPopupStates.includes(customizablePanel.state);
+ } else if (
+ areaType == CustomizableUI.TYPE_TOOLBAR &&
+ window.toolbar.visible
+ ) {
+ // The edit controls are on a toolbar, so they are visible,
+ // unless they're in a panel that isn't visible...
+ if (placement.area == "nav-bar") {
+ let editControls = document.getElementById("edit-controls");
+ gEditUIVisible =
+ !editControls.hasAttribute("overflowedItem") ||
+ kOpenPopupStates.includes(
+ document.getElementById("widget-overflow").state
+ );
+ } else {
+ gEditUIVisible = true;
+ }
+ }
+ }
+
+ // Now check the main menu panel
+ if (!gEditUIVisible) {
+ gEditUIVisible = kOpenPopupStates.includes(PanelUI.panel.state);
+ }
+
+ // No need to update commands if the edit UI visibility has not changed.
+ if (gEditUIVisible == oldVisible) {
+ return;
+ }
+
+ // If UI is visible, update the edit commands' enabled state to reflect
+ // whether or not they are actually enabled for the current focus/selection.
+ if (gEditUIVisible) {
+ goUpdateGlobalEditMenuItems();
+ } else {
+ // Otherwise, enable all commands, so that keyboard shortcuts still work,
+ // then lazily determine their actual enabled state when the user presses
+ // a keyboard shortcut.
+ goSetCommandEnabled("cmd_undo", true);
+ goSetCommandEnabled("cmd_redo", true);
+ goSetCommandEnabled("cmd_cut", true);
+ goSetCommandEnabled("cmd_copy", true);
+ goSetCommandEnabled("cmd_paste", true);
+ goSetCommandEnabled("cmd_selectAll", true);
+ goSetCommandEnabled("cmd_delete", true);
+ goSetCommandEnabled("cmd_switchTextDirection", true);
+ }
+}
+
+/**
+ * Opens a new tab with the userContextId specified as an attribute of
+ * sourceEvent. This attribute is propagated to the top level originAttributes
+ * living on the tab's docShell.
+ *
+ * @param event
+ * A click event on a userContext File Menu option
+ */
+function openNewUserContextTab(event) {
+ openTrustedLinkIn(BROWSER_NEW_TAB_URL, "tab", {
+ userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
+ });
+}
+
+/**
+ * Updates User Context Menu Item UI visibility depending on
+ * privacy.userContext.enabled pref state.
+ */
+function updateFileMenuUserContextUIVisibility(id) {
+ let menu = document.getElementById(id);
+ menu.hidden = !Services.prefs.getBoolPref(
+ "privacy.userContext.enabled",
+ false
+ );
+ // Visibility of File menu item shouldn't change frequently.
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ menu.setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Updates the User Context UI indicators if the browser is in a non-default context
+ */
+function updateUserContextUIIndicator() {
+ function replaceContainerClass(classType, element, value) {
+ let prefix = "identity-" + classType + "-";
+ if (value && element.classList.contains(prefix + value)) {
+ return;
+ }
+ for (let className of element.classList) {
+ if (className.startsWith(prefix)) {
+ element.classList.remove(className);
+ }
+ }
+ if (value) {
+ element.classList.add(prefix + value);
+ }
+ }
+
+ let hbox = document.getElementById("userContext-icons");
+
+ let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid");
+ if (!userContextId) {
+ replaceContainerClass("color", hbox, "");
+ hbox.hidden = true;
+ return;
+ }
+
+ let identity = ContextualIdentityService.getPublicIdentityFromId(
+ userContextId
+ );
+ if (!identity) {
+ replaceContainerClass("color", hbox, "");
+ hbox.hidden = true;
+ return;
+ }
+
+ replaceContainerClass("color", hbox, identity.color);
+
+ let label = ContextualIdentityService.getUserContextLabel(userContextId);
+ document.getElementById("userContext-label").setAttribute("value", label);
+ // Also set the container label as the tooltip so we can only show the icon
+ // in small windows.
+ hbox.setAttribute("tooltiptext", label);
+
+ let indicator = document.getElementById("userContext-indicator");
+ replaceContainerClass("icon", indicator, identity.icon);
+
+ hbox.hidden = false;
+}
+
+/**
+ * Makes the Character Encoding menu enabled or disabled as appropriate.
+ * To be called when the View menu or the app menu is opened.
+ */
+function updateCharacterEncodingMenuState() {
+ let charsetMenu = document.getElementById("charsetMenu");
+ // gBrowser is null on Mac when the menubar shows in the context of
+ // non-browser windows. The above elements may be null depending on
+ // what parts of the menubar are present. E.g. no app menu on Mac.
+ if (gBrowser && gBrowser.selectedBrowser.mayEnableCharacterEncodingMenu) {
+ if (charsetMenu) {
+ charsetMenu.removeAttribute("disabled");
+ }
+ } else if (charsetMenu) {
+ charsetMenu.setAttribute("disabled", "true");
+ }
+}
+
+var XULBrowserWindow = {
+ // Stored Status, Link and Loading values
+ status: "",
+ defaultStatus: "",
+ overLink: "",
+ startTime: 0,
+ isBusy: false,
+ busyUI: false,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsIWebProgressListener2",
+ "nsISupportsWeakReference",
+ "nsIXULBrowserWindow",
+ ]),
+
+ get stopCommand() {
+ delete this.stopCommand;
+ return (this.stopCommand = document.getElementById("Browser:Stop"));
+ },
+ get reloadCommand() {
+ delete this.reloadCommand;
+ return (this.reloadCommand = document.getElementById("Browser:Reload"));
+ },
+ get _elementsForTextBasedTypes() {
+ delete this._elementsForTextBasedTypes;
+ return (this._elementsForTextBasedTypes = [
+ document.getElementById("pageStyleMenu"),
+ document.getElementById("context-viewpartialsource-selection"),
+ document.getElementById("context-print-selection"),
+ ]);
+ },
+ get _elementsForFind() {
+ delete this._elementsForFind;
+ return (this._elementsForFind = [
+ document.getElementById("cmd_find"),
+ document.getElementById("cmd_findAgain"),
+ document.getElementById("cmd_findPrevious"),
+ ]);
+ },
+ get _elementsForViewSource() {
+ delete this._elementsForViewSource;
+ return (this._elementsForViewSource = [
+ document.getElementById("context-viewsource"),
+ document.getElementById("View:PageSource"),
+ ]);
+ },
+
+ setDefaultStatus(status) {
+ this.defaultStatus = status;
+ StatusPanel.update();
+ },
+
+ setOverLink(url) {
+ if (url) {
+ url = Services.textToSubURI.unEscapeURIForUI(url);
+
+ // Encode bidirectional formatting characters.
+ // (RFC 3987 sections 3.2 and 4.1 paragraph 6)
+ url = url.replace(
+ /[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g,
+ encodeURIComponent
+ );
+
+ if (UrlbarPrefs.get("trimURLs")) {
+ url = BrowserUtils.trimURL(url);
+ }
+ }
+
+ this.overLink = url;
+ LinkTargetDisplay.update();
+ },
+
+ showTooltip(x, y, tooltip, direction, browser) {
+ if (
+ Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService)
+ .getCurrentSession()
+ ) {
+ return;
+ }
+
+ let elt = document.getElementById("remoteBrowserTooltip");
+ elt.label = tooltip;
+ elt.style.direction = direction;
+ elt.openPopupAtScreen(x, y, false, null);
+ },
+
+ hideTooltip() {
+ let elt = document.getElementById("remoteBrowserTooltip");
+ elt.hidePopup();
+ },
+
+ getTabCount() {
+ return gBrowser.tabs.length;
+ },
+
+ // Called before links are navigated to to allow us to retarget them if needed.
+ onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
+ return BrowserUtils.onBeforeLinkTraversal(
+ originalTarget,
+ linkURI,
+ linkNode,
+ isAppTab
+ );
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ // Do nothing.
+ },
+
+ onProgressChange64(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ return this.onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ );
+ },
+
+ // This function fires only for the currently selected tab.
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ const nsIWebProgressListener = Ci.nsIWebProgressListener;
+
+ let browser = gBrowser.selectedBrowser;
+ gProtectionsHandler.onStateChange(aWebProgress, aStateFlags);
+
+ if (
+ aStateFlags & nsIWebProgressListener.STATE_START &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK
+ ) {
+ if (aRequest && aWebProgress.isTopLevel) {
+ // clear out search-engine data
+ browser.engines = null;
+ }
+
+ this.isBusy = true;
+
+ if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) {
+ this.busyUI = true;
+
+ // XXX: This needs to be based on window activity...
+ this.stopCommand.removeAttribute("disabled");
+ CombinedStopReload.switchToStop(aRequest, aWebProgress);
+ }
+ } else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
+ // This (thanks to the filter) is a network stop or the last
+ // request stop outside of loading the document, stop throbbers
+ // and progress bars and such
+ if (aRequest) {
+ let msg = "";
+ let location;
+ let canViewSource = true;
+ // Get the URI either from a channel or a pseudo-object
+ if (aRequest instanceof Ci.nsIChannel || "URI" in aRequest) {
+ location = aRequest.URI;
+
+ // For keyword URIs clear the user typed value since they will be changed into real URIs
+ if (location.scheme == "keyword" && aWebProgress.isTopLevel) {
+ gBrowser.userTypedValue = null;
+ }
+
+ canViewSource = location.scheme != "view-source";
+
+ if (location.spec != "about:blank") {
+ switch (aStatus) {
+ case Cr.NS_ERROR_NET_TIMEOUT:
+ msg = gNavigatorBundle.getString("nv_timeout");
+ break;
+ }
+ }
+ }
+
+ this.status = "";
+ this.setDefaultStatus(msg);
+
+ // Disable View Source menu entries for images, enable otherwise
+ let isText =
+ browser.documentContentType &&
+ BrowserUtils.mimeTypeIsTextBased(browser.documentContentType);
+ for (let element of this._elementsForViewSource) {
+ if (canViewSource && isText) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+ }
+
+ this._updateElementsForContentType();
+ }
+
+ this.isBusy = false;
+
+ if (this.busyUI) {
+ this.busyUI = false;
+
+ this.stopCommand.setAttribute("disabled", "true");
+ CombinedStopReload.switchToReload(aRequest, aWebProgress);
+ }
+ }
+ },
+
+ /**
+ * An nsIWebProgressListener method called by tabbrowser. The `aIsSimulated`
+ * parameter is extra and not declared in nsIWebProgressListener, however; see
+ * below.
+ *
+ * @param {nsIWebProgress} aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param {nsIRequest} aRequest
+ * The associated nsIRequest. This may be null in some cases.
+ * @param {nsIURI} aLocationURI
+ * The URI of the location that is being loaded.
+ * @param {integer} aFlags
+ * Flags that indicate the reason the location changed. See the
+ * nsIWebProgressListener.LOCATION_CHANGE_* values.
+ * @param {boolean} aIsSimulated
+ * True when this is called by tabbrowser due to switching tabs and
+ * undefined otherwise. This parameter is not declared in
+ * nsIWebProgressListener.onLocationChange; see bug 1478348.
+ */
+ onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags, aIsSimulated) {
+ var location = aLocationURI ? aLocationURI.spec : "";
+
+ this.hideOverLinkImmediately = true;
+ this.setOverLink("");
+ this.hideOverLinkImmediately = false;
+
+ // We should probably not do this if the value has changed since the user
+ // searched
+ // Update urlbar only if a new page was loaded on the primary content area
+ // Do not update urlbar if there was a subframe navigation
+
+ if (aWebProgress.isTopLevel) {
+ let isSameDocument =
+ aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT;
+ if (
+ (location == "about:blank" &&
+ BrowserUtils.checkEmptyPageOrigin(gBrowser.selectedBrowser)) ||
+ location == ""
+ ) {
+ // Second condition is for new tabs, otherwise
+ // reload function is enabled until tab is refreshed.
+ this.reloadCommand.setAttribute("disabled", "true");
+ } else {
+ this.reloadCommand.removeAttribute("disabled");
+ }
+
+ // We want to update the popup visibility if we received this notification
+ // via simulated locationchange events such as switching between tabs, however
+ // if this is a document navigation then PopupNotifications will be updated
+ // via TabsProgressListener.onLocationChange and we do not want it called twice
+ gURLBar.setURI(aLocationURI, aIsSimulated);
+
+ BookmarkingUI.onLocationChange();
+ // If we've actually changed document, update the toolbar visibility.
+ if (gBookmarksToolbar2h2020 && !isSameDocument) {
+ let bookmarksToolbar = gNavToolbox.querySelector("#PersonalToolbar");
+ setToolbarVisibility(
+ bookmarksToolbar,
+ gBookmarksToolbarVisibility,
+ false,
+ false
+ );
+ }
+
+ gIdentityHandler.onLocationChange();
+
+ gProtectionsHandler.onLocationChange();
+
+ BrowserPageActions.onLocationChange();
+
+ SafeBrowsingNotificationBox.onLocationChange(aLocationURI);
+
+ UrlbarProviderSearchTips.onLocationChange(
+ window,
+ aLocationURI,
+ aWebProgress,
+ aFlags
+ );
+
+ gTabletModePageCounter.inc();
+
+ this._updateElementsForContentType();
+
+ // Try not to instantiate gCustomizeMode as much as possible,
+ // so don't use CustomizeMode.jsm to check for URI or customizing.
+ if (
+ location == "about:blank" &&
+ gBrowser.selectedTab.hasAttribute("customizemode")
+ ) {
+ gCustomizeMode.enter();
+ } else if (
+ CustomizationHandler.isEnteringCustomizeMode ||
+ CustomizationHandler.isCustomizing()
+ ) {
+ gCustomizeMode.exit();
+ }
+
+ CFRPageActions.updatePageActions(gBrowser.selectedBrowser);
+ }
+ Services.obs.notifyObservers(null, "touchbar-location-change", location);
+ UpdateBackForwardCommands(gBrowser.webNavigation);
+ AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser);
+
+ if (!gMultiProcessBrowser) {
+ // Bug 1108553 - Cannot rotate images with e10s
+ gGestureSupport.restoreRotationState();
+ }
+
+ // See bug 358202, when tabs are switched during a drag operation,
+ // timers don't fire on windows (bug 203573)
+ if (aRequest) {
+ setTimeout(function() {
+ XULBrowserWindow.asyncUpdateUI();
+ }, 0);
+ } else {
+ this.asyncUpdateUI();
+ }
+
+ if (AppConstants.MOZ_CRASHREPORTER && aLocationURI) {
+ let uri = aLocationURI;
+ try {
+ // If the current URI contains a username/password, remove it.
+ uri = aLocationURI
+ .mutate()
+ .setUserPass("")
+ .finalize();
+ } catch (ex) {
+ /* Ignore failures on about: URIs. */
+ }
+
+ try {
+ gCrashReporter.annotateCrashReport("URL", uri.spec);
+ } catch (ex) {
+ // Don't make noise when the crash reporter is built but not enabled.
+ if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
+ throw ex;
+ }
+ }
+ }
+ },
+
+ _updateElementsForContentType() {
+ let browser = gBrowser.selectedBrowser;
+
+ let isText =
+ browser.documentContentType &&
+ BrowserUtils.mimeTypeIsTextBased(browser.documentContentType);
+ for (let element of this._elementsForTextBasedTypes) {
+ if (isText) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+ }
+
+ // Always enable find commands in PDF documents, otherwise do it only for
+ // text documents whose location is not in the blacklist.
+ let enableFind =
+ browser.contentPrincipal?.spec == "resource://pdf.js/web/viewer.html" ||
+ (isText && BrowserUtils.canFindInPage(gBrowser.currentURI.spec));
+ for (let element of this._elementsForFind) {
+ if (enableFind) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+ }
+ },
+
+ asyncUpdateUI() {
+ BrowserSearch.updateOpenSearchBadge();
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ this.status = aMessage;
+ StatusPanel.update();
+ },
+
+ // Properties used to cache security state used to update the UI
+ _state: null,
+ _lastLocation: null,
+ _event: null,
+ _lastLocationForEvent: null,
+ // _isSecureContext can change without the state/location changing, due to security
+ // error pages that intercept certain loads. For example this happens sometimes
+ // with the the HTTPS-Only Mode error page (more details in bug 1656027)
+ _isSecureContext: null,
+
+ // This is called in multiple ways:
+ // 1. Due to the nsIWebProgressListener.onContentBlockingEvent notification.
+ // 2. Called by tabbrowser.xml when updating the current browser.
+ // 3. Called directly during this object's initializations.
+ // 4. Due to the nsIWebProgressListener.onLocationChange notification.
+ // aRequest will be null always in case 2 and 3, and sometimes in case 1 (for
+ // instance, there won't be a request when STATE_BLOCKED_TRACKING_CONTENT or
+ // other blocking events are observed).
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent, aIsSimulated) {
+ // Don't need to do anything if the data we use to update the UI hasn't
+ // changed
+ let uri = gBrowser.currentURI;
+ let spec = uri.spec;
+ if (this._event == aEvent && this._lastLocationForEvent == spec) {
+ return;
+ }
+ this._lastLocationForEvent = spec;
+
+ if (
+ typeof aIsSimulated != "boolean" &&
+ typeof aIsSimulated != "undefined"
+ ) {
+ throw new Error(
+ "onContentBlockingEvent: aIsSimulated receieved an unexpected type"
+ );
+ }
+
+ gProtectionsHandler.onContentBlockingEvent(
+ aEvent,
+ aWebProgress,
+ aIsSimulated,
+ this._event // previous content blocking event
+ );
+
+ // We need the state of the previous content blocking event, so update
+ // event after onContentBlockingEvent is called.
+ this._event = aEvent;
+ },
+
+ // This is called in multiple ways:
+ // 1. Due to the nsIWebProgressListener.onSecurityChange notification.
+ // 2. Called by tabbrowser.xml when updating the current browser.
+ // 3. Called directly during this object's initializations.
+ // aRequest will be null always in case 2 and 3, and sometimes in case 1.
+ onSecurityChange(aWebProgress, aRequest, aState, aIsSimulated) {
+ // Don't need to do anything if the data we use to update the UI hasn't
+ // changed
+ let uri = gBrowser.currentURI;
+ let spec = uri.spec;
+ let isSecureContext = gBrowser.securityUI.isSecureContext;
+ if (
+ this._state == aState &&
+ this._lastLocation == spec &&
+ this._isSecureContext === isSecureContext
+ ) {
+ // Switching to a tab of the same URL doesn't change most security
+ // information, but tab specific permissions may be different.
+ gIdentityHandler.refreshIdentityBlock();
+ return;
+ }
+ this._state = aState;
+ this._lastLocation = spec;
+ this._isSecureContext = isSecureContext;
+
+ // Make sure the "https" part of the URL is striked out or not,
+ // depending on the current mixed active content blocking state.
+ gURLBar.formatValue();
+
+ try {
+ uri = Services.io.createExposableURI(uri);
+ } catch (e) {}
+ gIdentityHandler.updateIdentity(this._state, uri);
+ },
+
+ // simulate all change notifications after switching tabs
+ onUpdateCurrentBrowser: function XWB_onUpdateCurrentBrowser(
+ aStateFlags,
+ aStatus,
+ aMessage,
+ aTotalProgress
+ ) {
+ if (FullZoom.updateBackgroundTabs) {
+ FullZoom.onLocationChange(gBrowser.currentURI, true);
+ }
+
+ CombinedStopReload.onTabSwitch();
+
+ // Docshell should normally take care of hiding the tooltip, but we need to do it
+ // ourselves for tabswitches.
+ this.hideTooltip();
+
+ // Also hide tooltips for content loaded in the parent process:
+ document.getElementById("aHTMLTooltip").hidePopup();
+
+ var nsIWebProgressListener = Ci.nsIWebProgressListener;
+ var loadingDone = aStateFlags & nsIWebProgressListener.STATE_STOP;
+ // use a pseudo-object instead of a (potentially nonexistent) channel for getting
+ // a correct error message - and make sure that the UI is always either in
+ // loading (STATE_START) or done (STATE_STOP) mode
+ this.onStateChange(
+ gBrowser.webProgress,
+ { URI: gBrowser.currentURI },
+ loadingDone
+ ? nsIWebProgressListener.STATE_STOP
+ : nsIWebProgressListener.STATE_START,
+ aStatus
+ );
+ // status message and progress value are undefined if we're done with loading
+ if (loadingDone) {
+ return;
+ }
+ this.onStatusChange(gBrowser.webProgress, null, 0, aMessage);
+ },
+};
+
+var LinkTargetDisplay = {
+ get DELAY_SHOW() {
+ delete this.DELAY_SHOW;
+ return (this.DELAY_SHOW = Services.prefs.getIntPref(
+ "browser.overlink-delay"
+ ));
+ },
+
+ DELAY_HIDE: 250,
+ _timer: 0,
+
+ get _contextMenu() {
+ delete this._contextMenu;
+ return (this._contextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ ));
+ },
+
+ update() {
+ if (
+ this._contextMenu.state == "open" ||
+ this._contextMenu.state == "showing"
+ ) {
+ this._contextMenu.addEventListener("popuphidden", () => this.update(), {
+ once: true,
+ });
+ return;
+ }
+
+ clearTimeout(this._timer);
+ window.removeEventListener("mousemove", this, true);
+
+ if (!XULBrowserWindow.overLink) {
+ if (XULBrowserWindow.hideOverLinkImmediately) {
+ this._hide();
+ } else {
+ this._timer = setTimeout(this._hide.bind(this), this.DELAY_HIDE);
+ }
+ return;
+ }
+
+ if (StatusPanel.isVisible) {
+ StatusPanel.update();
+ } else {
+ // Let the display appear when the mouse doesn't move within the delay
+ this._showDelayed();
+ window.addEventListener("mousemove", this, true);
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "mousemove":
+ // Restart the delay since the mouse was moved
+ clearTimeout(this._timer);
+ this._showDelayed();
+ break;
+ }
+ },
+
+ _showDelayed() {
+ this._timer = setTimeout(
+ function(self) {
+ StatusPanel.update();
+ window.removeEventListener("mousemove", self, true);
+ },
+ this.DELAY_SHOW,
+ this
+ );
+ },
+
+ _hide() {
+ clearTimeout(this._timer);
+
+ StatusPanel.update();
+ },
+};
+
+var CombinedStopReload = {
+ // Try to initialize. Returns whether initialization was successful, which
+ // may mean we had already initialized.
+ ensureInitialized() {
+ if (this._initialized) {
+ return true;
+ }
+ if (this._destroyed) {
+ return false;
+ }
+
+ let reload = document.getElementById("reload-button");
+ let stop = document.getElementById("stop-button");
+ // It's possible the stop/reload buttons have been moved to the palette.
+ // They may be reinserted later, so we will retry initialization if/when
+ // we get notified of document loads.
+ if (!stop || !reload) {
+ return false;
+ }
+
+ this._initialized = true;
+ if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") {
+ reload.setAttribute("displaystop", "true");
+ }
+ stop.addEventListener("click", this);
+
+ // Removing attributes based on the observed command doesn't happen if the button
+ // is in the palette when the command's attribute is removed (cf. bug 309953)
+ for (let button of [stop, reload]) {
+ if (button.hasAttribute("disabled")) {
+ let command = document.getElementById(button.getAttribute("command"));
+ if (!command.hasAttribute("disabled")) {
+ button.removeAttribute("disabled");
+ }
+ }
+ }
+
+ this.reload = reload;
+ this.stop = stop;
+ this.stopReloadContainer = this.reload.parentNode;
+ this.timeWhenSwitchedToStop = 0;
+
+ this.stopReloadContainer.addEventListener("animationend", this);
+ this.stopReloadContainer.addEventListener("animationcancel", this);
+
+ return true;
+ },
+
+ uninit() {
+ this._destroyed = true;
+
+ if (!this._initialized) {
+ return;
+ }
+
+ this._cancelTransition();
+ this.stop.removeEventListener("click", this);
+ this.stopReloadContainer.removeEventListener("animationend", this);
+ this.stopReloadContainer.removeEventListener("animationcancel", this);
+ this.stopReloadContainer = null;
+ this.reload = null;
+ this.stop = null;
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "click":
+ if (event.button == 0 && !this.stop.disabled) {
+ this._stopClicked = true;
+ }
+ break;
+ case "animationcancel":
+ case "animationend": {
+ if (
+ event.target.classList.contains("toolbarbutton-animatable-image") &&
+ (event.animationName == "reload-to-stop" ||
+ event.animationName == "stop-to-reload" ||
+ event.animationName == "reload-to-stop-rtl" ||
+ event.animationName == "stop-to-reload-rtl")
+ ) {
+ this.stopReloadContainer.removeAttribute("animate");
+ }
+ }
+ }
+ },
+
+ onTabSwitch() {
+ // Reset the time in the event of a tabswitch since the stored time
+ // would have been associated with the previous tab, so the animation will
+ // still run if the page has been loading until long after the tab switch.
+ this.timeWhenSwitchedToStop = window.performance.now();
+ },
+
+ switchToStop(aRequest, aWebProgress) {
+ if (
+ !this.ensureInitialized() ||
+ !this._shouldSwitch(aRequest, aWebProgress)
+ ) {
+ return;
+ }
+
+ // Store the time that we switched to the stop button only if a request
+ // is active. Requests are null if the switch is related to a tabswitch.
+ // This is used to determine if we should show the stop->reload animation.
+ if (aRequest instanceof Ci.nsIRequest) {
+ this.timeWhenSwitchedToStop = window.performance.now();
+ }
+
+ let shouldAnimate =
+ aRequest instanceof Ci.nsIRequest &&
+ aWebProgress.isTopLevel &&
+ aWebProgress.isLoadingDocument &&
+ !gBrowser.tabAnimationsInProgress &&
+ !gReduceMotion &&
+ this.stopReloadContainer.closest("#nav-bar-customization-target");
+
+ this._cancelTransition();
+ if (shouldAnimate) {
+ BrowserUtils.setToolbarButtonHeightProperty(this.stopReloadContainer);
+ this.stopReloadContainer.setAttribute("animate", "true");
+ } else {
+ this.stopReloadContainer.removeAttribute("animate");
+ }
+ this.reload.setAttribute("displaystop", "true");
+ },
+
+ switchToReload(aRequest, aWebProgress) {
+ if (!this.ensureInitialized() || !this.reload.hasAttribute("displaystop")) {
+ return;
+ }
+
+ let shouldAnimate =
+ aRequest instanceof Ci.nsIRequest &&
+ aWebProgress.isTopLevel &&
+ !aWebProgress.isLoadingDocument &&
+ !gBrowser.tabAnimationsInProgress &&
+ !gReduceMotion &&
+ this._loadTimeExceedsMinimumForAnimation() &&
+ this.stopReloadContainer.closest("#nav-bar-customization-target");
+
+ if (shouldAnimate) {
+ BrowserUtils.setToolbarButtonHeightProperty(this.stopReloadContainer);
+ this.stopReloadContainer.setAttribute("animate", "true");
+ } else {
+ this.stopReloadContainer.removeAttribute("animate");
+ }
+
+ this.reload.removeAttribute("displaystop");
+
+ if (!shouldAnimate || this._stopClicked) {
+ this._stopClicked = false;
+ this._cancelTransition();
+ this.reload.disabled =
+ XULBrowserWindow.reloadCommand.getAttribute("disabled") == "true";
+ return;
+ }
+
+ if (this._timer) {
+ return;
+ }
+
+ // Temporarily disable the reload button to prevent the user from
+ // accidentally reloading the page when intending to click the stop button
+ this.reload.disabled = true;
+ this._timer = setTimeout(
+ function(self) {
+ self._timer = 0;
+ self.reload.disabled =
+ XULBrowserWindow.reloadCommand.getAttribute("disabled") == "true";
+ },
+ 650,
+ this
+ );
+ },
+
+ _loadTimeExceedsMinimumForAnimation() {
+ // If the time between switching to the stop button then switching to
+ // the reload button exceeds 150ms, then we will show the animation.
+ // If we don't know when we switched to stop (switchToStop is called
+ // after init but before switchToReload), then we will prevent the
+ // animation from occuring.
+ return (
+ this.timeWhenSwitchedToStop &&
+ window.performance.now() - this.timeWhenSwitchedToStop > 150
+ );
+ },
+
+ _shouldSwitch(aRequest, aWebProgress) {
+ if (
+ aRequest &&
+ aRequest.originalURI &&
+ (aRequest.originalURI.schemeIs("chrome") ||
+ (aRequest.originalURI.schemeIs("about") &&
+ aWebProgress.isTopLevel &&
+ !aRequest.originalURI.spec.startsWith("about:reader")))
+ ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ _cancelTransition() {
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = 0;
+ }
+ },
+};
+
+var TabsProgressListener = {
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Collect telemetry data about tab load times.
+ if (
+ aWebProgress.isTopLevel &&
+ (!aRequest.originalURI || aRequest.originalURI.scheme != "about")
+ ) {
+ let histogram = "FX_PAGE_LOAD_MS_2";
+ let recordLoadTelemetry = true;
+
+ if (aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD) {
+ // loadType is constructed by shifting loadFlags, this is why we need to
+ // do the same shifting here.
+ // https://searchfox.org/mozilla-central/rev/11cfa0462a6b5d8c5e2111b8cfddcf78098f0141/docshell/base/nsDocShellLoadTypes.h#22
+ if (aWebProgress.loadType & (kSkipCacheFlags << 16)) {
+ histogram = "FX_PAGE_RELOAD_SKIP_CACHE_MS";
+ } else if (aWebProgress.loadType == Ci.nsIDocShell.LOAD_CMD_RELOAD) {
+ histogram = "FX_PAGE_RELOAD_NORMAL_MS";
+ } else {
+ recordLoadTelemetry = false;
+ }
+ }
+
+ let stopwatchRunning = TelemetryStopwatch.running(histogram, aBrowser);
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ if (stopwatchRunning) {
+ // Oops, we're seeing another start without having noticed the previous stop.
+ if (recordLoadTelemetry) {
+ TelemetryStopwatch.cancel(histogram, aBrowser);
+ }
+ }
+ if (recordLoadTelemetry) {
+ TelemetryStopwatch.start(histogram, aBrowser);
+ }
+ Services.telemetry.getHistogramById("FX_TOTAL_TOP_VISITS").add(true);
+ } else if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ stopwatchRunning /* we won't see STATE_START events for pre-rendered tabs */
+ ) {
+ if (recordLoadTelemetry) {
+ TelemetryStopwatch.finish(histogram, aBrowser);
+ BrowserUtils.recordSiteOriginTelemetry(browserWindows());
+ }
+ }
+ } else if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStatus == Cr.NS_BINDING_ABORTED &&
+ stopwatchRunning /* we won't see STATE_START events for pre-rendered tabs */
+ ) {
+ if (recordLoadTelemetry) {
+ TelemetryStopwatch.cancel(histogram, aBrowser);
+ }
+ }
+ }
+ },
+
+ onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
+ // Filter out location changes caused by anchor navigation
+ // or history.push/pop/replaceState.
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ // Reader mode cares about history.pushState and friends.
+ // FIXME: The content process should manage this directly (bug 1445351).
+ aBrowser.sendMessageToActor(
+ "Reader:PushState",
+ {
+ isArticle: aBrowser.isArticle,
+ },
+ "AboutReader"
+ );
+ return;
+ }
+
+ // Filter out location changes in sub documents.
+ if (!aWebProgress.isTopLevel) {
+ return;
+ }
+
+ // Only need to call locationChange if the PopupNotifications object
+ // for this window has already been initialized (i.e. its getter no
+ // longer exists)
+ if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get) {
+ PopupNotifications.locationChange(aBrowser);
+ }
+
+ let tab = gBrowser.getTabForBrowser(aBrowser);
+ if (tab && tab._sharingState) {
+ gBrowser.resetBrowserSharing(aBrowser);
+ }
+
+ gBrowser.getNotificationBox(aBrowser).removeTransientNotifications();
+
+ FullZoom.onLocationChange(aLocationURI, false, aBrowser);
+ CaptivePortalWatcher.onLocationChange(aBrowser);
+ },
+
+ onLinkIconAvailable(browser, dataURI, iconURI) {
+ if (!iconURI) {
+ return;
+ }
+ if (browser == gBrowser.selectedBrowser) {
+ // If the "Add Search Engine" page action is in the urlbar, its image
+ // needs to be set to the new icon, so call updateOpenSearchBadge.
+ BrowserSearch.updateOpenSearchBadge();
+ }
+ },
+};
+
+function nsBrowserAccess() {}
+
+nsBrowserAccess.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBrowserDOMWindow"]),
+
+ _openURIInNewTab(
+ aURI,
+ aReferrerInfo,
+ aIsPrivate,
+ aIsExternal,
+ aForceNotRemote = false,
+ aUserContextId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID,
+ aOpenWindowInfo = null,
+ aOpenerBrowser = null,
+ aTriggeringPrincipal = null,
+ aName = "",
+ aCsp = null,
+ aSkipLoad = false
+ ) {
+ let win, needToFocusWin;
+
+ // try the current window. if we're in a popup, fall back on the most recent browser window
+ if (window.toolbar.visible) {
+ win = window;
+ } else {
+ win = BrowserWindowTracker.getTopWindow({ private: aIsPrivate });
+ needToFocusWin = true;
+ }
+
+ if (!win) {
+ // we couldn't find a suitable window, a new one needs to be opened.
+ return null;
+ }
+
+ if (aIsExternal && (!aURI || aURI.spec == "about:blank")) {
+ win.BrowserOpenTab(); // this also focuses the location bar
+ win.focus();
+ return win.gBrowser.selectedBrowser;
+ }
+
+ let loadInBackground = Services.prefs.getBoolPref(
+ "browser.tabs.loadDivertedInBackground"
+ );
+
+ let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", {
+ triggeringPrincipal: aTriggeringPrincipal,
+ referrerInfo: aReferrerInfo,
+ userContextId: aUserContextId,
+ fromExternal: aIsExternal,
+ inBackground: loadInBackground,
+ forceNotRemote: aForceNotRemote,
+ openWindowInfo: aOpenWindowInfo,
+ openerBrowser: aOpenerBrowser,
+ name: aName,
+ csp: aCsp,
+ skipLoad: aSkipLoad,
+ });
+ let browser = win.gBrowser.getBrowserForTab(tab);
+
+ if (needToFocusWin || (!loadInBackground && aIsExternal)) {
+ win.focus();
+ }
+
+ return browser;
+ },
+
+ createContentWindow(
+ aURI,
+ aOpenWindowInfo,
+ aWhere,
+ aFlags,
+ aTriggeringPrincipal,
+ aCsp
+ ) {
+ return this.getContentWindowOrOpenURI(
+ null,
+ aOpenWindowInfo,
+ aWhere,
+ aFlags,
+ aTriggeringPrincipal,
+ aCsp,
+ true
+ );
+ },
+
+ openURI(aURI, aOpenWindowInfo, aWhere, aFlags, aTriggeringPrincipal, aCsp) {
+ if (!aURI) {
+ Cu.reportError("openURI should only be called with a valid URI");
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ return this.getContentWindowOrOpenURI(
+ aURI,
+ aOpenWindowInfo,
+ aWhere,
+ aFlags,
+ aTriggeringPrincipal,
+ aCsp,
+ false
+ );
+ },
+
+ getContentWindowOrOpenURI(
+ aURI,
+ aOpenWindowInfo,
+ aWhere,
+ aFlags,
+ aTriggeringPrincipal,
+ aCsp,
+ aSkipLoad
+ ) {
+ var browsingContext = null;
+ var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+
+ if (aOpenWindowInfo && isExternal) {
+ Cu.reportError(
+ "nsBrowserAccess.openURI did not expect aOpenWindowInfo to be " +
+ "passed if the context is OPEN_EXTERNAL."
+ );
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ if (isExternal && aURI && aURI.schemeIs("chrome")) {
+ dump("use --chrome command-line option to load external chrome urls\n");
+ return null;
+ }
+
+ if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
+ if (
+ isExternal &&
+ Services.prefs.prefHasUserValue(
+ "browser.link.open_newwindow.override.external"
+ )
+ ) {
+ aWhere = Services.prefs.getIntPref(
+ "browser.link.open_newwindow.override.external"
+ );
+ } else {
+ aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
+ }
+ }
+
+ let referrerInfo;
+ if (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_REFERRER) {
+ referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, false, null);
+ } else if (
+ aOpenWindowInfo &&
+ aOpenWindowInfo.parent &&
+ aOpenWindowInfo.parent.window
+ ) {
+ referrerInfo = new ReferrerInfo(
+ aOpenWindowInfo.parent.window.document.referrerInfo.referrerPolicy,
+ true,
+ makeURI(aOpenWindowInfo.parent.window.location.href)
+ );
+ } else {
+ referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, null);
+ }
+
+ let isPrivate = aOpenWindowInfo
+ ? aOpenWindowInfo.originAttributes.privateBrowsingId != 0
+ : PrivateBrowsingUtils.isWindowPrivate(window);
+
+ switch (aWhere) {
+ case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW:
+ // FIXME: Bug 408379. So how come this doesn't send the
+ // referrer like the other loads do?
+ var url = aURI && aURI.spec;
+ let features = "all,dialog=no";
+ if (isPrivate) {
+ features += ",private";
+ }
+ // Pass all params to openDialog to ensure that "url" isn't passed through
+ // loadOneOrMoreURIs, which splits based on "|"
+ try {
+ openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ features,
+ // window.arguments
+ url,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ aTriggeringPrincipal,
+ null,
+ aCsp,
+ aOpenWindowInfo
+ );
+ // At this point, the new browser window is just starting to load, and
+ // hasn't created the content <browser> that we should return.
+ // If the caller of this function is originating in C++, they can pass a
+ // callback in nsOpenWindowInfo and it will be invoked when the browsing
+ // context for a newly opened window is ready.
+ browsingContext = null;
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ break;
+ case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB: {
+ // If we have an opener, that means that the caller is expecting access
+ // to the nsIDOMWindow of the opened tab right away. For e10s windows,
+ // this means forcing the newly opened browser to be non-remote so that
+ // we can hand back the nsIDOMWindow. DocumentLoadListener will do the
+ // job of shuttling off the newly opened browser to run in the right
+ // process once it starts loading a URI.
+ let forceNotRemote = aOpenWindowInfo && !aOpenWindowInfo.isRemote;
+ let userContextId = aOpenWindowInfo
+ ? aOpenWindowInfo.originAttributes.userContextId
+ : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+ let browser = this._openURIInNewTab(
+ aURI,
+ referrerInfo,
+ isPrivate,
+ isExternal,
+ forceNotRemote,
+ userContextId,
+ aOpenWindowInfo,
+ null,
+ aTriggeringPrincipal,
+ "",
+ aCsp,
+ aSkipLoad
+ );
+ if (browser) {
+ browsingContext = browser.browsingContext;
+ }
+ break;
+ }
+ case Ci.nsIBrowserDOMWindow.OPEN_PRINT_BROWSER: {
+ let browser = PrintUtils.startPrintWindow(
+ "window_print",
+ aOpenWindowInfo.parent,
+ { openWindowInfo: aOpenWindowInfo }
+ );
+ if (browser) {
+ browsingContext = browser.browsingContext;
+ }
+ break;
+ }
+ default:
+ // OPEN_CURRENTWINDOW or an illegal value
+ browsingContext = window.gBrowser.selectedBrowser.browsingContext;
+ if (aURI) {
+ let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (isExternal) {
+ loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
+ } else if (!aTriggeringPrincipal.isSystemPrincipal) {
+ // XXX this code must be reviewed and changed when bug 1616353
+ // lands.
+ loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIRST_LOAD;
+ }
+ gBrowser.loadURI(aURI.spec, {
+ triggeringPrincipal: aTriggeringPrincipal,
+ csp: aCsp,
+ loadFlags,
+ referrerInfo,
+ });
+ }
+ if (
+ !Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground")
+ ) {
+ window.focus();
+ }
+ }
+ return browsingContext;
+ },
+
+ createContentWindowInFrame: function browser_createContentWindowInFrame(
+ aURI,
+ aParams,
+ aWhere,
+ aFlags,
+ aName
+ ) {
+ // Passing a null-URI to only create the content window,
+ // and pass true for aSkipLoad to prevent loading of
+ // about:blank
+ return this.getContentWindowOrOpenURIInFrame(
+ null,
+ aParams,
+ aWhere,
+ aFlags,
+ aName,
+ true
+ );
+ },
+
+ openURIInFrame: function browser_openURIInFrame(
+ aURI,
+ aParams,
+ aWhere,
+ aFlags,
+ aName
+ ) {
+ return this.getContentWindowOrOpenURIInFrame(
+ aURI,
+ aParams,
+ aWhere,
+ aFlags,
+ aName,
+ false
+ );
+ },
+
+ getContentWindowOrOpenURIInFrame: function browser_getContentWindowOrOpenURIInFrame(
+ aURI,
+ aParams,
+ aWhere,
+ aFlags,
+ aName,
+ aSkipLoad
+ ) {
+ if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_PRINT_BROWSER) {
+ return PrintUtils.startPrintWindow(
+ "window_print",
+ aParams.openWindowInfo.parent,
+ { openWindowInfo: aParams.openWindowInfo }
+ );
+ }
+
+ if (aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) {
+ dump("Error: openURIInFrame can only open in new tabs or print");
+ return null;
+ }
+
+ var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+
+ var userContextId =
+ aParams.openerOriginAttributes &&
+ "userContextId" in aParams.openerOriginAttributes
+ ? aParams.openerOriginAttributes.userContextId
+ : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+
+ return this._openURIInNewTab(
+ aURI,
+ aParams.referrerInfo,
+ aParams.isPrivate,
+ isExternal,
+ false,
+ userContextId,
+ aParams.openWindowInfo,
+ aParams.openerBrowser,
+ aParams.triggeringPrincipal,
+ aName,
+ aParams.csp,
+ aSkipLoad
+ );
+ },
+
+ canClose() {
+ return CanCloseWindow();
+ },
+
+ get tabCount() {
+ return gBrowser.tabs.length;
+ },
+};
+
+function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
+ var popup = aEvent.target;
+ if (popup != aEvent.currentTarget) {
+ return;
+ }
+
+ // Empty the menu
+ for (var i = popup.children.length - 1; i >= 0; --i) {
+ var deadItem = popup.children[i];
+ if (deadItem.hasAttribute("toolbarId")) {
+ popup.removeChild(deadItem);
+ }
+ }
+
+ MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl");
+ let firstMenuItem = aInsertPoint || popup.firstElementChild;
+ let toolbarNodes = gNavToolbox.querySelectorAll("toolbar");
+ for (let toolbar of toolbarNodes) {
+ if (!toolbar.hasAttribute("toolbarname")) {
+ continue;
+ }
+
+ if (toolbar.id == "PersonalToolbar" && gBookmarksToolbar2h2020) {
+ let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(toolbar);
+ popup.insertBefore(menu, firstMenuItem);
+ } else {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("id", "toggle_" + toolbar.id);
+ menuItem.setAttribute("toolbarId", toolbar.id);
+ menuItem.setAttribute("type", "checkbox");
+ menuItem.setAttribute("label", toolbar.getAttribute("toolbarname"));
+ let hidingAttribute =
+ toolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
+ menuItem.setAttribute(
+ "checked",
+ toolbar.getAttribute(hidingAttribute) != "true"
+ );
+ menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
+ if (popup.id != "toolbar-context-menu") {
+ menuItem.setAttribute("key", toolbar.getAttribute("key"));
+ }
+
+ popup.insertBefore(menuItem, firstMenuItem);
+ menuItem.addEventListener("command", onViewToolbarCommand);
+ }
+ }
+
+ let moveToPanel = popup.querySelector(".customize-context-moveToPanel");
+ let removeFromToolbar = popup.querySelector(
+ ".customize-context-removeFromToolbar"
+ );
+ // View -> Toolbars menu doesn't have the moveToPanel or removeFromToolbar items.
+ if (!moveToPanel || !removeFromToolbar) {
+ return;
+ }
+
+ // triggerNode can be a nested child element of a toolbaritem.
+ let toolbarItem = popup.triggerNode;
+
+ if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") {
+ toolbarItem = toolbarItem.firstElementChild;
+ } else if (toolbarItem && toolbarItem.localName != "toolbar") {
+ while (toolbarItem && toolbarItem.parentElement) {
+ let parent = toolbarItem.parentElement;
+ if (
+ (parent.classList &&
+ parent.classList.contains("customization-target")) ||
+ parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well.
+ parent.localName == "toolbarpaletteitem" ||
+ parent.localName == "toolbar"
+ ) {
+ break;
+ }
+ toolbarItem = parent;
+ }
+ } else {
+ toolbarItem = null;
+ }
+
+ let showTabStripItems = toolbarItem && toolbarItem.id == "tabbrowser-tabs";
+ for (let node of popup.querySelectorAll(
+ 'menuitem[contexttype="toolbaritem"]'
+ )) {
+ node.hidden = showTabStripItems;
+ }
+
+ for (let node of popup.querySelectorAll('menuitem[contexttype="tabbar"]')) {
+ node.hidden = !showTabStripItems;
+ }
+
+ document
+ .getElementById("toolbar-context-menu")
+ .querySelectorAll("[data-lazy-l10n-id]")
+ .forEach(el => {
+ el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
+ el.removeAttribute("data-lazy-l10n-id");
+ });
+
+ if (showTabStripItems) {
+ let multipleTabsSelected = !!gBrowser.multiSelectedTabsCount;
+ document.getElementById(
+ "toolbar-context-bookmarkSelectedTabs"
+ ).hidden = !multipleTabsSelected;
+ document.getElementById(
+ "toolbar-context-bookmarkSelectedTab"
+ ).hidden = multipleTabsSelected;
+ document.getElementById(
+ "toolbar-context-reloadSelectedTabs"
+ ).hidden = !multipleTabsSelected;
+ document.getElementById(
+ "toolbar-context-reloadSelectedTab"
+ ).hidden = multipleTabsSelected;
+ document.getElementById(
+ "toolbar-context-selectAllTabs"
+ ).disabled = gBrowser.allTabsSelected();
+ document.getElementById("toolbar-context-undoCloseTab").disabled =
+ SessionStore.getClosedTabCount(window) == 0;
+ return;
+ }
+
+ let movable =
+ toolbarItem &&
+ toolbarItem.id &&
+ CustomizableUI.isWidgetRemovable(toolbarItem);
+ if (movable) {
+ if (CustomizableUI.isSpecialWidget(toolbarItem.id)) {
+ moveToPanel.setAttribute("disabled", true);
+ } else {
+ moveToPanel.removeAttribute("disabled");
+ }
+ removeFromToolbar.removeAttribute("disabled");
+ } else {
+ moveToPanel.setAttribute("disabled", true);
+ removeFromToolbar.setAttribute("disabled", true);
+ }
+}
+
+function onViewToolbarCommand(aEvent) {
+ let node = aEvent.originalTarget;
+ let menuId;
+ let toolbarId;
+ let isVisible;
+ if (node.dataset.bookmarksToolbarVisibility) {
+ isVisible = node.dataset.visibilityEnum;
+ toolbarId = "PersonalToolbar";
+ menuId = node.parentNode.parentNode.parentNode.id;
+ Services.prefs.setCharPref(
+ "browser.toolbars.bookmarks.visibility",
+ isVisible
+ );
+ } else {
+ menuId = node.parentNode.id;
+ toolbarId = node.getAttribute("toolbarId");
+ isVisible = node.getAttribute("checked") == "true";
+ }
+ CustomizableUI.setToolbarVisibility(toolbarId, isVisible);
+ BrowserUsageTelemetry.recordToolbarVisibility(toolbarId, isVisible, menuId);
+}
+
+function setToolbarVisibility(
+ toolbar,
+ isVisible,
+ persist = true,
+ animated = true
+) {
+ let hidingAttribute;
+ if (toolbar.getAttribute("type") == "menubar") {
+ hidingAttribute = "autohide";
+ if (AppConstants.platform == "linux") {
+ Services.prefs.setBoolPref("ui.key.menuAccessKeyFocuses", !isVisible);
+ }
+ } else {
+ hidingAttribute = "collapsed";
+ }
+
+ // For the bookmarks toolbar, we need to persist state before toggling
+ // the visibility in this window, because the state can be different
+ // (newtab vs never or always) even when that won't change visibility
+ // in this window.
+ if (persist && toolbar.id == "PersonalToolbar") {
+ let prefValue;
+ if (typeof isVisible == "string") {
+ prefValue = isVisible;
+ } else {
+ prefValue = isVisible ? "always" : "never";
+ }
+ Services.prefs.setCharPref(
+ "browser.toolbars.bookmarks.visibility",
+ prefValue
+ );
+ }
+
+ if (typeof isVisible == "string") {
+ switch (isVisible) {
+ case "always":
+ isVisible = true;
+ break;
+ case "never":
+ isVisible = false;
+ break;
+ case "newtab":
+ let currentURI = gBrowser?.currentURI;
+ if (!gBrowserInit.domContentLoaded) {
+ let uriToLoad = gBrowserInit.uriToLoadPromise;
+ if (uriToLoad) {
+ if (Array.isArray(uriToLoad)) {
+ // We only care about the first tab being loaded
+ uriToLoad = uriToLoad[0];
+ }
+ try {
+ currentURI = Services.io.newURI(uriToLoad);
+ } catch (ex) {}
+ }
+ }
+ isVisible =
+ !!currentURI && BookmarkingUI.isOnNewTabPage({ currentURI });
+ break;
+ }
+ }
+
+ if (toolbar.getAttribute(hidingAttribute) == (!isVisible).toString()) {
+ // If this call will not result in a visibility change, return early
+ // since dispatching toolbarvisibilitychange will cause views to get rebuilt.
+ return;
+ }
+
+ toolbar.classList.toggle("instant", !animated);
+ toolbar.setAttribute(hidingAttribute, !isVisible);
+ // For the bookmarks toolbar, we will have saved state above. For other
+ // toolbars, we need to do it after setting the attribute, or we might
+ // save the wrong state.
+ if (persist && toolbar.id != "PersonalToolbar") {
+ Services.xulStore.persist(toolbar, hidingAttribute);
+ }
+
+ let eventParams = {
+ detail: {
+ visible: isVisible,
+ },
+ bubbles: true,
+ };
+ let event = new CustomEvent("toolbarvisibilitychange", eventParams);
+ toolbar.dispatchEvent(event);
+
+ if (
+ toolbar.getAttribute("type") == "menubar" &&
+ CustomizationHandler.isCustomizing()
+ ) {
+ gCustomizeMode._updateDragSpaceCheckbox();
+ }
+}
+
+function updateToggleControlLabel(control) {
+ if (!control.hasAttribute("label-checked")) {
+ return;
+ }
+
+ if (!control.hasAttribute("label-unchecked")) {
+ control.setAttribute("label-unchecked", control.getAttribute("label"));
+ }
+ let prefix = control.getAttribute("checked") == "true" ? "" : "un";
+ control.setAttribute("label", control.getAttribute(`label-${prefix}checked`));
+}
+
+var TabletModeUpdater = {
+ init() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ this.update(WindowsUIUtils.inTabletMode);
+ Services.obs.addObserver(this, "tablet-mode-change");
+ }
+ },
+
+ uninit() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ Services.obs.removeObserver(this, "tablet-mode-change");
+ }
+ },
+
+ observe(subject, topic, data) {
+ this.update(data == "tablet-mode");
+ },
+
+ update(isInTabletMode) {
+ let wasInTabletMode = document.documentElement.hasAttribute("tabletmode");
+ if (isInTabletMode) {
+ document.documentElement.setAttribute("tabletmode", "true");
+ } else {
+ document.documentElement.removeAttribute("tabletmode");
+ }
+ if (wasInTabletMode != isInTabletMode) {
+ gUIDensity.update();
+ }
+ },
+};
+
+var gTabletModePageCounter = {
+ enabled: false,
+ inc() {
+ this.enabled = AppConstants.isPlatformAndVersionAtLeast("win", "10.0");
+ if (!this.enabled) {
+ this.inc = () => {};
+ return;
+ }
+ this.inc = this._realInc;
+ this.inc();
+ },
+
+ _desktopCount: 0,
+ _tabletCount: 0,
+ _realInc() {
+ let inTabletMode = document.documentElement.hasAttribute("tabletmode");
+ this[inTabletMode ? "_tabletCount" : "_desktopCount"]++;
+ },
+
+ finish() {
+ if (this.enabled) {
+ let histogram = Services.telemetry.getKeyedHistogramById(
+ "FX_TABLETMODE_PAGE_LOAD"
+ );
+ histogram.add("tablet", this._tabletCount);
+ histogram.add("desktop", this._desktopCount);
+ }
+ },
+};
+
+function displaySecurityInfo() {
+ BrowserPageInfo(null, "securityTab");
+}
+
+// Updates the UI density (for touch and compact mode) based on the uidensity pref.
+var gUIDensity = {
+ MODE_NORMAL: 0,
+ MODE_COMPACT: 1,
+ MODE_TOUCH: 2,
+ uiDensityPref: "browser.uidensity",
+ autoTouchModePref: "browser.touchmode.auto",
+
+ init() {
+ this.update();
+ Services.prefs.addObserver(this.uiDensityPref, this);
+ Services.prefs.addObserver(this.autoTouchModePref, this);
+ },
+
+ uninit() {
+ Services.prefs.removeObserver(this.uiDensityPref, this);
+ Services.prefs.removeObserver(this.autoTouchModePref, this);
+ },
+
+ observe(aSubject, aTopic, aPrefName) {
+ if (
+ aTopic != "nsPref:changed" ||
+ (aPrefName != this.uiDensityPref && aPrefName != this.autoTouchModePref)
+ ) {
+ return;
+ }
+
+ this.update();
+ },
+
+ getCurrentDensity() {
+ // Automatically override the uidensity to touch in Windows tablet mode.
+ if (
+ AppConstants.isPlatformAndVersionAtLeast("win", "10") &&
+ WindowsUIUtils.inTabletMode &&
+ Services.prefs.getBoolPref(this.autoTouchModePref)
+ ) {
+ return { mode: this.MODE_TOUCH, overridden: true };
+ }
+ return {
+ mode: Services.prefs.getIntPref(this.uiDensityPref),
+ overridden: false,
+ };
+ },
+
+ update(mode) {
+ if (mode == null) {
+ mode = this.getCurrentDensity().mode;
+ }
+
+ let docs = [document.documentElement];
+ let shouldUpdateSidebar = SidebarUI.initialized && SidebarUI.isOpen;
+ if (shouldUpdateSidebar) {
+ docs.push(SidebarUI.browser.contentDocument.documentElement);
+ }
+ for (let doc of docs) {
+ switch (mode) {
+ case this.MODE_COMPACT:
+ doc.setAttribute("uidensity", "compact");
+ break;
+ case this.MODE_TOUCH:
+ doc.setAttribute("uidensity", "touch");
+ break;
+ default:
+ doc.removeAttribute("uidensity");
+ break;
+ }
+ }
+ if (shouldUpdateSidebar) {
+ let tree = SidebarUI.browser.contentDocument.querySelector(
+ ".sidebar-placesTree"
+ );
+ if (tree) {
+ // Tree items don't update their styles without changing some property on the
+ // parent tree element, like background-color or border. See bug 1407399.
+ tree.style.border = "1px";
+ tree.style.border = "";
+ }
+ }
+
+ gBrowser.tabContainer.uiDensityChanged();
+ gURLBar.updateLayoutBreakout();
+ },
+};
+
+const nodeToTooltipMap = {
+ "bookmarks-menu-button": "bookmarksMenuButton.tooltip",
+ "context-reload": "reloadButton.tooltip",
+ "context-stop": "stopButton.tooltip",
+ "downloads-button": "downloads.tooltip",
+ "fullscreen-button": "fullscreenButton.tooltip",
+ "appMenu-fullscreen-button": "fullscreenButton.tooltip",
+ "new-window-button": "newWindowButton.tooltip",
+ "new-tab-button": "newTabButton.tooltip",
+ "tabs-newtab-button": "newTabButton.tooltip",
+ "reload-button": "reloadButton.tooltip",
+ "stop-button": "stopButton.tooltip",
+ "urlbar-zoom-button": "urlbar-zoom-button.tooltip",
+ "appMenu-cut-button": "cut-button.tooltip",
+ "appMenu-copy-button": "copy-button.tooltip",
+ "appMenu-paste-button": "paste-button.tooltip",
+ "appMenu-zoomEnlarge-button": "zoomEnlarge-button.tooltip",
+ "appMenu-zoomReset-button": "zoomReset-button.tooltip",
+ "appMenu-zoomReduce-button": "zoomReduce-button.tooltip",
+ "reader-mode-button": "reader-mode-button.tooltip",
+ "print-button": "printButton.tooltip",
+};
+const nodeToShortcutMap = {
+ "bookmarks-menu-button": "manBookmarkKb",
+ "context-reload": "key_reload",
+ "context-stop": "key_stop",
+ "downloads-button": "key_openDownloads",
+ "fullscreen-button": "key_fullScreen",
+ "appMenu-fullscreen-button": "key_fullScreen",
+ "new-window-button": "key_newNavigator",
+ "new-tab-button": "key_newNavigatorTab",
+ "tabs-newtab-button": "key_newNavigatorTab",
+ "reload-button": "key_reload",
+ "stop-button": "key_stop",
+ "urlbar-zoom-button": "key_fullZoomReset",
+ "appMenu-cut-button": "key_cut",
+ "appMenu-copy-button": "key_copy",
+ "appMenu-paste-button": "key_paste",
+ "appMenu-zoomEnlarge-button": "key_fullZoomEnlarge",
+ "appMenu-zoomReset-button": "key_fullZoomReset",
+ "appMenu-zoomReduce-button": "key_fullZoomReduce",
+ "reader-mode-button": "key_toggleReaderMode",
+ "print-button": "printKb",
+};
+
+const gDynamicTooltipCache = new Map();
+function GetDynamicShortcutTooltipText(nodeId) {
+ if (!gDynamicTooltipCache.has(nodeId) && nodeId in nodeToTooltipMap) {
+ let strId = nodeToTooltipMap[nodeId];
+ let args = [];
+ if (nodeId in nodeToShortcutMap) {
+ let shortcutId = nodeToShortcutMap[nodeId];
+ let shortcut = document.getElementById(shortcutId);
+ if (shortcut) {
+ args.push(ShortcutUtils.prettifyShortcut(shortcut));
+ }
+ }
+ gDynamicTooltipCache.set(
+ nodeId,
+ gNavigatorBundle.getFormattedString(strId, args)
+ );
+ }
+ return gDynamicTooltipCache.get(nodeId);
+}
+
+function UpdateDynamicShortcutTooltipText(aTooltip) {
+ let nodeId =
+ aTooltip.triggerNode.id || aTooltip.triggerNode.getAttribute("anonid");
+ if (
+ nodeId == "print-button" &&
+ !Services.prefs.getBoolPref("print.tab_modal.enabled") &&
+ AppConstants.platform !== "macosx"
+ ) {
+ // If the new print UI pref is turned off, we should display the old title that did not have the shortcut
+ aTooltip.setAttribute(
+ "label",
+ document.getElementById(nodeId).getAttribute("print-button-title")
+ );
+ } else {
+ aTooltip.setAttribute("label", GetDynamicShortcutTooltipText(nodeId));
+ }
+}
+
+/*
+ * - [ Dependencies ] ---------------------------------------------------------
+ * utilityOverlay.js:
+ * - gatherTextUnder
+ */
+
+/**
+ * Extracts linkNode and href for the current click target.
+ *
+ * @param event
+ * The click event.
+ * @return [href, linkNode].
+ *
+ * @note linkNode will be null if the click wasn't on an anchor
+ * element (or XLink).
+ */
+function hrefAndLinkNodeForClickEvent(event) {
+ function isHTMLLink(aNode) {
+ // Be consistent with what nsContextMenu.js does.
+ return (
+ (aNode instanceof HTMLAnchorElement && aNode.href) ||
+ (aNode instanceof HTMLAreaElement && aNode.href) ||
+ aNode instanceof HTMLLinkElement
+ );
+ }
+
+ let node = event.composedTarget;
+ while (node && !isHTMLLink(node)) {
+ node = node.flattenedTreeParentNode;
+ }
+
+ if (node) {
+ return [node.href, node];
+ }
+
+ // If there is no linkNode, try simple XLink.
+ let href, baseURI;
+ node = event.composedTarget;
+ while (node && !href) {
+ if (
+ node.nodeType == Node.ELEMENT_NODE &&
+ (node.localName == "a" ||
+ node.namespaceURI == "http://www.w3.org/1998/Math/MathML")
+ ) {
+ href =
+ node.getAttribute("href") ||
+ node.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+
+ if (href) {
+ baseURI = node.baseURI;
+ break;
+ }
+ }
+ node = node.flattenedTreeParentNode;
+ }
+
+ // In case of XLink, we don't return the node we got href from since
+ // callers expect <a>-like elements.
+ return [href ? makeURLAbsolute(baseURI, href) : null, null];
+}
+
+/**
+ * Called whenever the user clicks in the content area.
+ *
+ * @param event
+ * The click event.
+ * @param isPanelClick
+ * Whether the event comes from an extension panel.
+ * @note default event is prevented if the click is handled.
+ */
+function contentAreaClick(event, isPanelClick) {
+ if (!event.isTrusted || event.defaultPrevented || event.button != 0) {
+ return;
+ }
+
+ let [href, linkNode] = hrefAndLinkNodeForClickEvent(event);
+ if (!href) {
+ // Not a link, handle middle mouse navigation.
+ if (
+ event.button == 1 &&
+ Services.prefs.getBoolPref("middlemouse.contentLoadURL") &&
+ !Services.prefs.getBoolPref("general.autoScroll")
+ ) {
+ middleMousePaste(event);
+ event.preventDefault();
+ }
+ return;
+ }
+
+ // This code only applies if we have a linkNode (i.e. clicks on real anchor
+ // elements, as opposed to XLink).
+ if (
+ linkNode &&
+ event.button == 0 &&
+ !event.ctrlKey &&
+ !event.shiftKey &&
+ !event.altKey &&
+ !event.metaKey
+ ) {
+ // An extension panel's links should target the main content area. Do this
+ // if no modifier keys are down and if there's no target or the target
+ // equals _main (the IE convention) or _content (the Mozilla convention).
+ let target = linkNode.target;
+ let mainTarget = !target || target == "_content" || target == "_main";
+ if (isPanelClick && mainTarget) {
+ // javascript and data links should be executed in the current browser.
+ if (
+ linkNode.getAttribute("onclick") ||
+ href.startsWith("javascript:") ||
+ href.startsWith("data:")
+ ) {
+ return;
+ }
+
+ try {
+ urlSecurityCheck(href, linkNode.ownerDocument.nodePrincipal);
+ } catch (ex) {
+ // Prevent loading unsecure destinations.
+ event.preventDefault();
+ return;
+ }
+
+ loadURI(href, null, null, false);
+ event.preventDefault();
+ return;
+ }
+ }
+
+ handleLinkClick(event, href, linkNode);
+
+ // Mark the page as a user followed link. This is done so that history can
+ // distinguish automatic embed visits from user activated ones. For example
+ // pages loaded in frames are embed visits and lost with the session, while
+ // visits across frames should be preserved.
+ try {
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ PlacesUIUtils.markPageAsFollowedLink(href);
+ }
+ } catch (ex) {
+ /* Skip invalid URIs. */
+ }
+}
+
+/**
+ * Handles clicks on links.
+ *
+ * @return true if the click event was handled, false otherwise.
+ */
+function handleLinkClick(event, href, linkNode) {
+ if (event.button == 2) {
+ // right click
+ return false;
+ }
+
+ var where = whereToOpenLink(event);
+ if (where == "current") {
+ return false;
+ }
+
+ var doc = event.target.ownerDocument;
+ let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
+ Ci.nsIReferrerInfo
+ );
+ if (linkNode) {
+ referrerInfo.initWithElement(linkNode);
+ } else {
+ referrerInfo.initWithDocument(doc);
+ }
+
+ if (where == "save") {
+ saveURL(
+ href,
+ linkNode ? gatherTextUnder(linkNode) : "",
+ null,
+ true,
+ true,
+ referrerInfo,
+ doc.cookieJarSettings,
+ doc
+ );
+ event.preventDefault();
+ return true;
+ }
+
+ // if the mixedContentChannel is present and the referring URI passes
+ // a same origin check with the target URI, we can preserve the users
+ // decision of disabling MCB on a page for it's child tabs.
+ var persistAllowMixedContentInChildTab = false;
+
+ if (where == "tab" && gBrowser.docShell.mixedContentChannel) {
+ const sm = Services.scriptSecurityManager;
+ try {
+ var targetURI = makeURI(href);
+ let isPrivateWin =
+ doc.nodePrincipal.originAttributes.privateBrowsingId > 0;
+ sm.checkSameOriginURI(
+ doc.documentURIObject,
+ targetURI,
+ false,
+ isPrivateWin
+ );
+ persistAllowMixedContentInChildTab = true;
+ } catch (e) {}
+ }
+
+ let frameID = WebNavigationFrames.getFrameId(doc.defaultView);
+
+ urlSecurityCheck(href, doc.nodePrincipal);
+ let params = {
+ charset: doc.characterSet,
+ allowMixedContent: persistAllowMixedContentInChildTab,
+ referrerInfo,
+ originPrincipal: doc.nodePrincipal,
+ originStoragePrincipal: doc.effectiveStoragePrincipal,
+ triggeringPrincipal: doc.nodePrincipal,
+ csp: doc.csp,
+ frameID,
+ };
+
+ // The new tab/window must use the same userContextId
+ if (doc.nodePrincipal.originAttributes.userContextId) {
+ params.userContextId = doc.nodePrincipal.originAttributes.userContextId;
+ }
+
+ openLinkIn(href, where, params);
+ event.preventDefault();
+ return true;
+}
+
+/**
+ * Handles paste on middle mouse clicks.
+ *
+ * @param event {Event | Object} Event or JSON object.
+ */
+function middleMousePaste(event) {
+ let clipboard = readFromClipboard();
+ if (!clipboard) {
+ return;
+ }
+
+ // Strip embedded newlines and surrounding whitespace, to match the URL
+ // bar's behavior (stripsurroundingwhitespace)
+ clipboard = clipboard.replace(/\s*\n\s*/g, "");
+
+ clipboard = UrlbarUtils.stripUnsafeProtocolOnPaste(clipboard);
+
+ // if it's not the current tab, we don't need to do anything because the
+ // browser doesn't exist.
+ let where = whereToOpenLink(event, true, false);
+ let lastLocationChange;
+ if (where == "current") {
+ lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+ }
+
+ UrlbarUtils.getShortcutOrURIAndPostData(clipboard).then(data => {
+ try {
+ makeURI(data.url);
+ } catch (ex) {
+ // Not a valid URI.
+ return;
+ }
+
+ try {
+ UrlbarUtils.addToUrlbarHistory(data.url, window);
+ } catch (ex) {
+ // Things may go wrong when adding url to session history,
+ // but don't let that interfere with the loading of the url.
+ Cu.reportError(ex);
+ }
+
+ if (
+ where != "current" ||
+ lastLocationChange == gBrowser.selectedBrowser.lastLocationChange
+ ) {
+ openUILink(data.url, event, {
+ ignoreButton: true,
+ allowInheritPrincipal: data.mayInheritPrincipal,
+ triggeringPrincipal: gBrowser.selectedBrowser.contentPrincipal,
+ csp: gBrowser.selectedBrowser.csp,
+ });
+ }
+ });
+
+ if (event instanceof Event) {
+ event.stopPropagation();
+ }
+}
+
+// handleDroppedLink has the following 2 overloads:
+// handleDroppedLink(event, url, name, triggeringPrincipal)
+// handleDroppedLink(event, links, triggeringPrincipal)
+function handleDroppedLink(
+ event,
+ urlOrLinks,
+ nameOrTriggeringPrincipal,
+ triggeringPrincipal
+) {
+ let links;
+ if (Array.isArray(urlOrLinks)) {
+ links = urlOrLinks;
+ triggeringPrincipal = nameOrTriggeringPrincipal;
+ } else {
+ links = [{ url: urlOrLinks, nameOrTriggeringPrincipal, type: "" }];
+ }
+
+ let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+
+ let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid");
+
+ // event is null if links are dropped in content process.
+ // inBackground should be false, as it's loading into current browser.
+ let inBackground = false;
+ if (event) {
+ inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+ if (event.shiftKey) {
+ inBackground = !inBackground;
+ }
+ }
+
+ (async function() {
+ if (
+ links.length >=
+ Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
+ ) {
+ // Sync dialog cannot be used inside drop event handler.
+ let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
+ links.length,
+ window
+ );
+ if (!answer) {
+ return;
+ }
+ }
+
+ let urls = [];
+ let postDatas = [];
+ for (let link of links) {
+ let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url);
+ urls.push(data.url);
+ postDatas.push(data.postData);
+ }
+ if (lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) {
+ gBrowser.loadTabs(urls, {
+ inBackground,
+ replace: true,
+ allowThirdPartyFixup: false,
+ postDatas,
+ userContextId,
+ triggeringPrincipal,
+ });
+ }
+ })();
+
+ // If links are dropped in content process, event.preventDefault() should be
+ // called in content process.
+ if (event) {
+ // Keep the event from being handled by the dragDrop listeners
+ // built-in to gecko if they happen to be above us.
+ event.preventDefault();
+ }
+}
+
+function BrowserSetForcedCharacterSet(aCharset) {
+ if (aCharset) {
+ if (aCharset == "Japanese") {
+ aCharset = "Shift_JIS";
+ }
+ gBrowser.selectedBrowser.characterSet = aCharset;
+ // Save the forced character-set
+ PlacesUIUtils.setCharsetForPage(
+ gBrowser.currentURI,
+ aCharset,
+ window
+ ).catch(Cu.reportError);
+ }
+ BrowserCharsetReload();
+}
+
+function BrowserCharsetReload() {
+ BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+}
+
+function UpdateCurrentCharset(target) {
+ let selectedCharset = CharsetMenu.foldCharset(
+ gBrowser.selectedBrowser.characterSet,
+ gBrowser.selectedBrowser.charsetAutodetected
+ );
+ for (let menuItem of target.getElementsByTagName("menuitem")) {
+ let isSelected = menuItem.getAttribute("charset") === selectedCharset;
+ menuItem.setAttribute("checked", isSelected);
+ }
+}
+
+var ToolbarContextMenu = {
+ updateDownloadsAutoHide(popup) {
+ let checkbox = document.getElementById(
+ "toolbar-context-autohide-downloads-button"
+ );
+ let isDownloads =
+ popup.triggerNode &&
+ ["downloads-button", "wrapper-downloads-button"].includes(
+ popup.triggerNode.id
+ );
+ checkbox.hidden = !isDownloads;
+ if (DownloadsButton.autoHideDownloadsButton) {
+ checkbox.setAttribute("checked", "true");
+ } else {
+ checkbox.removeAttribute("checked");
+ }
+ },
+
+ onDownloadsAutoHideChange(event) {
+ let autoHide = event.target.getAttribute("checked") == "true";
+ Services.prefs.setBoolPref("browser.download.autohideButton", autoHide);
+ },
+
+ _getUnwrappedTriggerNode(popup) {
+ // Toolbar buttons are wrapped in customize mode. Unwrap if necessary.
+ let { triggerNode } = popup;
+ if (triggerNode && gCustomizeMode.isWrappedToolbarItem(triggerNode)) {
+ return triggerNode.firstElementChild;
+ }
+ return triggerNode;
+ },
+
+ _getExtensionId(popup) {
+ let node = this._getUnwrappedTriggerNode(popup);
+ return node && node.getAttribute("data-extensionid");
+ },
+
+ async updateExtension(popup) {
+ let removeExtension = popup.querySelector(
+ ".customize-context-removeExtension"
+ );
+ let manageExtension = popup.querySelector(
+ ".customize-context-manageExtension"
+ );
+ let reportExtension = popup.querySelector(
+ ".customize-context-reportExtension"
+ );
+ let separator = reportExtension.nextElementSibling;
+ let id = this._getExtensionId(popup);
+ let addon = id && (await AddonManager.getAddonByID(id));
+
+ for (let element of [removeExtension, manageExtension, separator]) {
+ element.hidden = !addon;
+ }
+
+ reportExtension.hidden = !addon || !gAddonAbuseReportEnabled;
+
+ if (addon) {
+ removeExtension.disabled = !(
+ addon.permissions & AddonManager.PERM_CAN_UNINSTALL
+ );
+ }
+ },
+
+ async removeExtensionForContextAction(popup) {
+ let id = this._getExtensionId(popup);
+
+ await BrowserAddonUI.removeAddon(id, "browserAction");
+ },
+
+ async reportExtensionForContextAction(popup, reportEntryPoint) {
+ let id = this._getExtensionId(popup);
+ let addon = id && (await AddonManager.getAddonByID(id));
+ if (!addon) {
+ return;
+ }
+
+ await BrowserAddonUI.reportAddon(addon.id, reportEntryPoint);
+ },
+
+ openAboutAddonsForContextAction(popup) {
+ let id = this._getExtensionId(popup);
+ if (id) {
+ let viewID = "addons://detail/" + encodeURIComponent(id);
+ BrowserOpenAddonsMgr(viewID);
+ AMTelemetry.recordActionEvent({
+ object: "browserAction",
+ action: "manage",
+ extra: { addonId: id },
+ });
+ }
+ },
+};
+
+var gPageStyleMenu = {
+ // This maps from a <browser> element (or, more specifically, a
+ // browser's permanentKey) to an Object that contains the most recent
+ // information about the browser content's stylesheets. That Object
+ // is populated via the PageStyle:StyleSheets message from the content
+ // process. The Object should have the following structure:
+ //
+ // filteredStyleSheets (Array):
+ // An Array of objects with a filtered list representing all stylesheets
+ // that the current page offers. Each object has the following members:
+ //
+ // title (String):
+ // The title of the stylesheet
+ //
+ // disabled (bool):
+ // Whether or not the stylesheet is currently applied
+ //
+ // href (String):
+ // The URL of the stylesheet. Stylesheets loaded via a data URL will
+ // have this property set to null.
+ //
+ // authorStyleDisabled (bool):
+ // Whether or not the user currently has "No Style" selected for
+ // the current page.
+ //
+ // preferredStyleSheetSet (bool):
+ // Whether or not the user currently has the "Default" style selected
+ // for the current page.
+ //
+ _pageStyleSheets: new WeakMap(),
+
+ /**
+ * Add/append styleSheets to the _pageStyleSheets weakmap.
+ * @param styleSheets
+ * The stylesheets to add, including the preferred
+ * stylesheet set for this document.
+ * @param permanentKey
+ * The permanent key of the browser that
+ * these stylesheets come from.
+ */
+ addBrowserStyleSheets(styleSheets, permanentKey) {
+ let sheetData = this._pageStyleSheets.get(permanentKey);
+ if (!sheetData) {
+ this._pageStyleSheets.set(permanentKey, styleSheets);
+ return;
+ }
+ sheetData.filteredStyleSheets.push(...styleSheets.filteredStyleSheets);
+ sheetData.preferredStyleSheetSet =
+ sheetData.preferredStyleSheetSet || styleSheets.preferredStyleSheetSet;
+ },
+
+ clearBrowserStyleSheets(permanentKey) {
+ this._pageStyleSheets.delete(permanentKey);
+ },
+
+ _getStyleSheetInfo(browser) {
+ let data = this._pageStyleSheets.get(browser.permanentKey);
+ if (!data) {
+ return {
+ filteredStyleSheets: [],
+ authorStyleDisabled: false,
+ preferredStyleSheetSet: true,
+ };
+ }
+
+ return data;
+ },
+
+ fillPopup(menuPopup) {
+ let styleSheetInfo = this._getStyleSheetInfo(gBrowser.selectedBrowser);
+ var noStyle = menuPopup.firstElementChild;
+ var persistentOnly = noStyle.nextElementSibling;
+ var sep = persistentOnly.nextElementSibling;
+ while (sep.nextElementSibling) {
+ menuPopup.removeChild(sep.nextElementSibling);
+ }
+
+ let styleSheets = styleSheetInfo.filteredStyleSheets;
+ var currentStyleSheets = {};
+ var styleDisabled = styleSheetInfo.authorStyleDisabled;
+ var haveAltSheets = false;
+ var altStyleSelected = false;
+
+ for (let currentStyleSheet of styleSheets) {
+ if (!currentStyleSheet.disabled) {
+ altStyleSelected = true;
+ }
+
+ haveAltSheets = true;
+
+ let lastWithSameTitle = null;
+ if (currentStyleSheet.title in currentStyleSheets) {
+ lastWithSameTitle = currentStyleSheets[currentStyleSheet.title];
+ }
+
+ if (!lastWithSameTitle) {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("type", "radio");
+ menuItem.setAttribute("label", currentStyleSheet.title);
+ menuItem.setAttribute("data", currentStyleSheet.title);
+ menuItem.setAttribute(
+ "checked",
+ !currentStyleSheet.disabled && !styleDisabled
+ );
+ menuItem.setAttribute(
+ "oncommand",
+ "gPageStyleMenu.switchStyleSheet(this.getAttribute('data'));"
+ );
+ menuPopup.appendChild(menuItem);
+ currentStyleSheets[currentStyleSheet.title] = menuItem;
+ } else if (currentStyleSheet.disabled) {
+ lastWithSameTitle.removeAttribute("checked");
+ }
+ }
+
+ noStyle.setAttribute("checked", styleDisabled);
+ persistentOnly.setAttribute("checked", !altStyleSelected && !styleDisabled);
+ persistentOnly.hidden = styleSheetInfo.preferredStyleSheetSet
+ ? haveAltSheets
+ : false;
+ sep.hidden = (noStyle.hidden && persistentOnly.hidden) || !haveAltSheets;
+ },
+
+ /**
+ * Send a message to all PageStyleParents by walking the BrowsingContext tree.
+ * @param message
+ * The string message to send to each PageStyleChild.
+ * @param data
+ * The data to send to each PageStyleChild within the message.
+ */
+ _sendMessageToAll(message, data) {
+ let contextsToVisit = [gBrowser.selectedBrowser.browsingContext];
+ while (contextsToVisit.length) {
+ let currentContext = contextsToVisit.pop();
+ let global = currentContext.currentWindowGlobal;
+
+ if (!global) {
+ continue;
+ }
+
+ let actor = global.getActor("PageStyle");
+ actor.sendAsyncMessage(message, data);
+
+ contextsToVisit.push(...currentContext.children);
+ }
+ },
+
+ /**
+ * Switch the stylesheet of all documents in the current browser.
+ * @param title The title of the stylesheet to switch to.
+ */
+ switchStyleSheet(title) {
+ let { permanentKey } = gBrowser.selectedBrowser;
+ let sheetData = this._pageStyleSheets.get(permanentKey);
+ if (sheetData && sheetData.filteredStyleSheets) {
+ sheetData.authorStyleDisabled = false;
+ for (let sheet of sheetData.filteredStyleSheets) {
+ sheet.disabled = sheet.title !== title;
+ }
+ }
+ this._sendMessageToAll("PageStyle:Switch", { title });
+ },
+
+ /**
+ * Disable all stylesheets. Called with View > Page Style > No Style.
+ */
+ disableStyle() {
+ let { permanentKey } = gBrowser.selectedBrowser;
+ let sheetData = this._pageStyleSheets.get(permanentKey);
+ if (sheetData) {
+ sheetData.authorStyleDisabled = true;
+ }
+ this._sendMessageToAll("PageStyle:Disable", {});
+ },
+};
+
+// Note that this is also called from non-browser windows on OSX, which do
+// share menu items but not much else. See nonbrowser-mac.js.
+var BrowserOffline = {
+ _inited: false,
+
+ // BrowserOffline Public Methods
+ init() {
+ if (!this._uiElement) {
+ this._uiElement = document.getElementById("cmd_toggleOfflineStatus");
+ }
+
+ Services.obs.addObserver(this, "network:offline-status-changed");
+
+ this._updateOfflineUI(Services.io.offline);
+
+ this._inited = true;
+ },
+
+ uninit() {
+ if (this._inited) {
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+ }
+ },
+
+ toggleOfflineStatus() {
+ var ioService = Services.io;
+
+ if (!ioService.offline && !this._canGoOffline()) {
+ this._updateOfflineUI(false);
+ return;
+ }
+
+ ioService.offline = !ioService.offline;
+ },
+
+ // nsIObserver
+ observe(aSubject, aTopic, aState) {
+ if (aTopic != "network:offline-status-changed") {
+ return;
+ }
+
+ // This notification is also received because of a loss in connectivity,
+ // which we ignore by updating the UI to the current value of io.offline
+ this._updateOfflineUI(Services.io.offline);
+ },
+
+ // BrowserOffline Implementation Methods
+ _canGoOffline() {
+ try {
+ var cancelGoOffline = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(cancelGoOffline, "offline-requested");
+
+ // Something aborted the quit process.
+ if (cancelGoOffline.data) {
+ return false;
+ }
+ } catch (ex) {}
+
+ return true;
+ },
+
+ _uiElement: null,
+ _updateOfflineUI(aOffline) {
+ var offlineLocked = Services.prefs.prefIsLocked("network.online");
+ if (offlineLocked) {
+ this._uiElement.setAttribute("disabled", "true");
+ }
+
+ this._uiElement.setAttribute("checked", aOffline);
+ },
+};
+
+var IndexedDBPromptHelper = {
+ _permissionsPrompt: "indexedDB-permissions-prompt",
+ _permissionsResponse: "indexedDB-permissions-response",
+
+ _notificationIcon: "indexedDB-notification-icon",
+
+ init: function IndexedDBPromptHelper_init() {
+ Services.obs.addObserver(this, this._permissionsPrompt);
+ },
+
+ uninit: function IndexedDBPromptHelper_uninit() {
+ Services.obs.removeObserver(this, this._permissionsPrompt);
+ },
+
+ observe: function IndexedDBPromptHelper_observe(subject, topic, data) {
+ if (topic != this._permissionsPrompt) {
+ throw new Error("Unexpected topic!");
+ }
+
+ var request = subject.QueryInterface(Ci.nsIIDBPermissionsRequest);
+
+ var browser = request.browserElement;
+ if (browser.ownerGlobal != window) {
+ // Only listen for notifications for browsers in our chrome window.
+ return;
+ }
+
+ // Get the host name if available or the file path otherwise.
+ var host = browser.currentURI.asciiHost || browser.currentURI.pathQueryRef;
+
+ var message;
+ var responseTopic;
+ if (topic == this._permissionsPrompt) {
+ message = gNavigatorBundle.getFormattedString("offlineApps.available2", [
+ host,
+ ]);
+ responseTopic = this._permissionsResponse;
+ }
+
+ var observer = request.responseObserver;
+
+ var mainAction = {
+ label: gNavigatorBundle.getString("offlineApps.allowStoring.label"),
+ accessKey: gNavigatorBundle.getString(
+ "offlineApps.allowStoring.accesskey"
+ ),
+ callback() {
+ observer.observe(
+ null,
+ responseTopic,
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+ },
+ };
+
+ var secondaryActions = [
+ {
+ label: gNavigatorBundle.getString("offlineApps.dontAllow.label"),
+ accessKey: gNavigatorBundle.getString(
+ "offlineApps.dontAllow.accesskey"
+ ),
+ callback() {
+ observer.observe(
+ null,
+ responseTopic,
+ Ci.nsIPermissionManager.DENY_ACTION
+ );
+ },
+ },
+ ];
+
+ PopupNotifications.show(
+ browser,
+ topic,
+ message,
+ this._notificationIcon,
+ mainAction,
+ secondaryActions,
+ {
+ persistent: true,
+ hideClose: true,
+ }
+ );
+ },
+};
+
+var CanvasPermissionPromptHelper = {
+ _permissionsPrompt: "canvas-permissions-prompt",
+ _permissionsPromptHideDoorHanger: "canvas-permissions-prompt-hide-doorhanger",
+ _notificationIcon: "canvas-notification-icon",
+
+ init() {
+ Services.obs.addObserver(this, this._permissionsPrompt);
+ Services.obs.addObserver(this, this._permissionsPromptHideDoorHanger);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, this._permissionsPrompt);
+ Services.obs.removeObserver(this, this._permissionsPromptHideDoorHanger);
+ },
+
+ // aSubject is an nsIBrowser (e10s) or an nsIDOMWindow (non-e10s).
+ // aData is an Origin string.
+ observe(aSubject, aTopic, aData) {
+ if (
+ aTopic != this._permissionsPrompt &&
+ aTopic != this._permissionsPromptHideDoorHanger
+ ) {
+ return;
+ }
+
+ let browser;
+ if (aSubject instanceof Ci.nsIDOMWindow) {
+ browser = aSubject.docShell.chromeEventHandler;
+ } else {
+ browser = aSubject;
+ }
+
+ if (gBrowser.selectedBrowser !== browser) {
+ // Must belong to some other window.
+ return;
+ }
+
+ let message = gNavigatorBundle.getFormattedString(
+ "canvas.siteprompt",
+ ["<>"],
+ 1
+ );
+
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ aData
+ );
+
+ function setCanvasPermission(aPerm, aPersistent) {
+ Services.perms.addFromPrincipal(
+ principal,
+ "canvas",
+ aPerm,
+ aPersistent
+ ? Ci.nsIPermissionManager.EXPIRE_NEVER
+ : Ci.nsIPermissionManager.EXPIRE_SESSION
+ );
+ }
+
+ let mainAction = {
+ label: gNavigatorBundle.getString("canvas.allow"),
+ accessKey: gNavigatorBundle.getString("canvas.allow.accesskey"),
+ callback(state) {
+ setCanvasPermission(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ state && state.checkboxChecked
+ );
+ },
+ };
+
+ let secondaryActions = [
+ {
+ label: gNavigatorBundle.getString("canvas.notAllow"),
+ accessKey: gNavigatorBundle.getString("canvas.notAllow.accesskey"),
+ callback(state) {
+ setCanvasPermission(
+ Ci.nsIPermissionManager.DENY_ACTION,
+ state && state.checkboxChecked
+ );
+ },
+ },
+ ];
+
+ let checkbox = {
+ // In PB mode, we don't want the "always remember" checkbox
+ show: !PrivateBrowsingUtils.isWindowPrivate(window),
+ };
+ if (checkbox.show) {
+ checkbox.checked = true;
+ checkbox.label = gBrowserBundle.GetStringFromName("canvas.remember");
+ }
+
+ let options = {
+ checkbox,
+ name: principal.host,
+ learnMoreURL:
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "fingerprint-permission",
+ dismissed: aTopic == this._permissionsPromptHideDoorHanger,
+ };
+ PopupNotifications.show(
+ browser,
+ this._permissionsPrompt,
+ message,
+ this._notificationIcon,
+ mainAction,
+ secondaryActions,
+ options
+ );
+ },
+};
+
+var WebAuthnPromptHelper = {
+ _icon: "webauthn-notification-icon",
+ _topic: "webauthn-prompt",
+
+ // The current notification, if any. The U2F manager is a singleton, we will
+ // never allow more than one active request. And thus we'll never have more
+ // than one notification either.
+ _current: null,
+
+ // The current transaction ID. Will be checked when we're notified of the
+ // cancellation of an ongoing WebAuthhn request.
+ _tid: 0,
+
+ init() {
+ Services.obs.addObserver(this, this._topic);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, this._topic);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ let mgr = aSubject.QueryInterface(Ci.nsIU2FTokenManager);
+ let data = JSON.parse(aData);
+
+ // If we receive a cancel, it might be a WebAuthn prompt starting in another
+ // window, and the other window's browsing context will send out the
+ // cancellations, so any cancel action we get should prompt us to cancel.
+ if (data.action == "cancel") {
+ this.cancel(data);
+ }
+
+ if (
+ data.browsingContextId !== gBrowser.selectedBrowser.browsingContext.id
+ ) {
+ // Must belong to some other window.
+ return;
+ }
+
+ if (data.action == "register") {
+ this.register(mgr, data);
+ } else if (data.action == "register-direct") {
+ this.registerDirect(mgr, data);
+ } else if (data.action == "sign") {
+ this.sign(mgr, data);
+ }
+ },
+
+ register(mgr, { origin, tid }) {
+ let mainAction = this.buildCancelAction(mgr, tid);
+ this.show(tid, "register", "webauthn.registerPrompt2", origin, mainAction);
+ },
+
+ registerDirect(mgr, { origin, tid }) {
+ let mainAction = this.buildProceedAction(mgr, tid);
+ let secondaryActions = [this.buildCancelAction(mgr, tid)];
+
+ let learnMoreURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "webauthn-direct-attestation";
+
+ let options = {
+ learnMoreURL,
+ checkbox: {
+ label: gNavigatorBundle.getString("webauthn.anonymize"),
+ },
+ };
+
+ this.show(
+ tid,
+ "register-direct",
+ "webauthn.registerDirectPrompt2",
+ origin,
+ mainAction,
+ secondaryActions,
+ options
+ );
+ },
+
+ sign(mgr, { origin, tid }) {
+ let mainAction = this.buildCancelAction(mgr, tid);
+ this.show(tid, "sign", "webauthn.signPrompt2", origin, mainAction);
+ },
+
+ show(
+ tid,
+ id,
+ stringId,
+ origin,
+ mainAction,
+ secondaryActions = [],
+ options = {}
+ ) {
+ this.reset();
+
+ try {
+ origin = Services.io.newURI(origin).asciiHost;
+ } catch (e) {
+ /* Might fail for arbitrary U2F RP IDs. */
+ }
+
+ let brandShortName = document
+ .getElementById("bundle_brand")
+ .getString("brandShortName");
+ let message = gNavigatorBundle.getFormattedString(
+ stringId,
+ ["<>", brandShortName],
+ 1
+ );
+
+ options.name = origin;
+ options.hideClose = true;
+ options.persistent = true;
+ options.eventCallback = event => {
+ if (event == "removed") {
+ this._current = null;
+ this._tid = 0;
+ }
+ };
+
+ this._tid = tid;
+ this._current = PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ `webauthn-prompt-${id}`,
+ message,
+ this._icon,
+ mainAction,
+ secondaryActions,
+ options
+ );
+ },
+
+ cancel({ tid }) {
+ if (this._tid == tid) {
+ this.reset();
+ }
+ },
+
+ reset() {
+ if (this._current) {
+ this._current.remove();
+ }
+ },
+
+ buildProceedAction(mgr, tid) {
+ return {
+ label: gNavigatorBundle.getString("webauthn.proceed"),
+ accessKey: gNavigatorBundle.getString("webauthn.proceed.accesskey"),
+ callback(state) {
+ mgr.resumeRegister(tid, state.checkboxChecked);
+ },
+ };
+ },
+
+ buildCancelAction(mgr, tid) {
+ return {
+ label: gNavigatorBundle.getString("webauthn.cancel"),
+ accessKey: gNavigatorBundle.getString("webauthn.cancel.accesskey"),
+ callback() {
+ mgr.cancel(tid);
+ },
+ };
+ },
+};
+
+function CanCloseWindow() {
+ // Avoid redundant calls to canClose from showing multiple
+ // PermitUnload dialogs.
+ if (Services.startup.shuttingDown || window.skipNextCanClose) {
+ return true;
+ }
+
+ for (let browser of gBrowser.browsers) {
+ // Don't instantiate lazy browsers.
+ if (!browser.isConnected) {
+ continue;
+ }
+
+ let { permitUnload } = browser.permitUnload();
+ if (!permitUnload) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function WindowIsClosing() {
+ if (!closeWindow(false, warnAboutClosingWindow)) {
+ return false;
+ }
+
+ // In theory we should exit here and the Window's internal Close
+ // method should trigger canClose on nsBrowserAccess. However, by
+ // that point it's too late to be able to show a prompt for
+ // PermitUnload. So we do it here, when we still can.
+ if (CanCloseWindow()) {
+ // This flag ensures that the later canClose call does nothing.
+ // It's only needed to make tests pass, since they detect the
+ // prompt even when it's not actually shown.
+ window.skipNextCanClose = true;
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Checks if this is the last full *browser* window around. If it is, this will
+ * be communicated like quitting. Otherwise, we warn about closing multiple tabs.
+ * @returns true if closing can proceed, false if it got cancelled.
+ */
+function warnAboutClosingWindow() {
+ // Popups aren't considered full browser windows; we also ignore private windows.
+ let isPBWindow =
+ PrivateBrowsingUtils.isWindowPrivate(window) &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing;
+
+ let closingTabs = gBrowser.tabs.length - gBrowser._removingTabs.length;
+
+ if (!isPBWindow && !toolbar.visible) {
+ return gBrowser.warnAboutClosingTabs(
+ closingTabs,
+ gBrowser.closingTabsEnum.ALL
+ );
+ }
+
+ // Figure out if there's at least one other browser window around.
+ let otherPBWindowExists = false;
+ let otherWindowExists = false;
+ for (let win of browserWindows()) {
+ if (!win.closed && win != window) {
+ otherWindowExists = true;
+ if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win)) {
+ otherPBWindowExists = true;
+ }
+ // If the current window is not in private browsing mode we don't need to
+ // look for other pb windows, we can leave the loop when finding the
+ // first non-popup window. If however the current window is in private
+ // browsing mode then we need at least one other pb and one non-popup
+ // window to break out early.
+ if (!isPBWindow || otherPBWindowExists) {
+ break;
+ }
+ }
+ }
+
+ if (isPBWindow && !otherPBWindowExists) {
+ let exitingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ exitingCanceled.data = false;
+ Services.obs.notifyObservers(exitingCanceled, "last-pb-context-exiting");
+ if (exitingCanceled.data) {
+ return false;
+ }
+ }
+
+ if (otherWindowExists) {
+ return (
+ isPBWindow ||
+ gBrowser.warnAboutClosingTabs(closingTabs, gBrowser.closingTabsEnum.ALL)
+ );
+ }
+
+ let os = Services.obs;
+
+ let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ os.notifyObservers(closingCanceled, "browser-lastwindow-close-requested");
+ if (closingCanceled.data) {
+ return false;
+ }
+
+ os.notifyObservers(null, "browser-lastwindow-close-granted");
+
+ // OS X doesn't quit the application when the last window is closed, but keeps
+ // the session alive. Hence don't prompt users to save tabs, but warn about
+ // closing multiple tabs.
+ return (
+ AppConstants.platform != "macosx" ||
+ isPBWindow ||
+ gBrowser.warnAboutClosingTabs(closingTabs, gBrowser.closingTabsEnum.ALL)
+ );
+}
+
+var MailIntegration = {
+ sendLinkForBrowser(aBrowser) {
+ this.sendMessage(
+ gURLBar.makeURIReadable(aBrowser.currentURI).displaySpec,
+ aBrowser.contentTitle
+ );
+ },
+
+ sendMessage(aBody, aSubject) {
+ // generate a mailto url based on the url and the url's title
+ var mailtoUrl = "mailto:";
+ if (aBody) {
+ mailtoUrl += "?body=" + encodeURIComponent(aBody);
+ mailtoUrl += "&subject=" + encodeURIComponent(aSubject);
+ }
+
+ var uri = makeURI(mailtoUrl);
+
+ // now pass this uri to the operating system
+ this._launchExternalUrl(uri);
+ },
+
+ // a generic method which can be used to pass arbitrary urls to the operating
+ // system.
+ // aURL --> a nsIURI which represents the url to launch
+ _launchExternalUrl(aURL) {
+ var extProtocolSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ if (extProtocolSvc) {
+ extProtocolSvc.loadURI(
+ aURL,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ }
+ },
+};
+
+function BrowserOpenAddonsMgr(aView) {
+ return new Promise(resolve => {
+ let emWindow;
+ let browserWindow;
+
+ var receivePong = function(aSubject, aTopic, aData) {
+ let browserWin = aSubject.browsingContext.topChromeWindow;
+ if (!emWindow || browserWin == window /* favor the current window */) {
+ emWindow = aSubject;
+ browserWindow = browserWin;
+ }
+ };
+ Services.obs.addObserver(receivePong, "EM-pong");
+ Services.obs.notifyObservers(null, "EM-ping");
+ Services.obs.removeObserver(receivePong, "EM-pong");
+
+ if (emWindow) {
+ if (aView) {
+ emWindow.loadView(aView);
+ }
+ let tab = browserWindow.gBrowser.getTabForBrowser(
+ emWindow.docShell.chromeEventHandler
+ );
+ browserWindow.gBrowser.selectedTab = tab;
+ emWindow.focus();
+ resolve(emWindow);
+ return;
+ }
+
+ // This must be a new load, else the ping/pong would have
+ // found the window above.
+ switchToTabHavingURI("about:addons", true);
+
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+ if (aView) {
+ aSubject.loadView(aView);
+ }
+ aSubject.focus();
+ resolve(aSubject);
+ }, "EM-loaded");
+ });
+}
+
+function AddKeywordForSearchField() {
+ if (!gContextMenu) {
+ throw new Error("Context menu doesn't seem to be open.");
+ }
+
+ gContextMenu.addKeywordForSearchField();
+}
+
+/**
+ * Re-open a closed tab.
+ * @param aIndex
+ * The index of the tab (via SessionStore.getClosedTabData)
+ * @returns a reference to the reopened tab.
+ */
+function undoCloseTab(aIndex) {
+ // wallpaper patch to prevent an unnecessary blank tab (bug 343895)
+ let blankTabToRemove = null;
+ if (gBrowser.tabs.length == 1 && gBrowser.selectedTab.isEmpty) {
+ blankTabToRemove = gBrowser.selectedTab;
+ }
+
+ let tab = null;
+ // aIndex is undefined if the function is called without a specific tab to restore.
+ let tabsToRemove =
+ aIndex !== undefined
+ ? [aIndex]
+ : new Array(SessionStore.getLastClosedTabCount(window)).fill(0);
+ for (let index of tabsToRemove) {
+ if (SessionStore.getClosedTabCount(window) > index) {
+ tab = SessionStore.undoCloseTab(window, index);
+
+ if (blankTabToRemove) {
+ gBrowser.removeTab(blankTabToRemove);
+ }
+ }
+ }
+ SessionStore.setLastClosedTabCount(window, 1);
+
+ return tab;
+}
+
+/**
+ * Re-open a closed window.
+ * @param aIndex
+ * The index of the window (via SessionStore.getClosedWindowData)
+ * @returns a reference to the reopened window.
+ */
+function undoCloseWindow(aIndex) {
+ let window = null;
+ if (SessionStore.getClosedWindowCount() > (aIndex || 0)) {
+ window = SessionStore.undoCloseWindow(aIndex || 0);
+ }
+
+ return window;
+}
+
+function ReportFalseDeceptiveSite() {
+ let contextsToVisit = [gBrowser.selectedBrowser.browsingContext];
+ while (contextsToVisit.length) {
+ let currentContext = contextsToVisit.pop();
+ let global = currentContext.currentWindowGlobal;
+
+ if (!global) {
+ continue;
+ }
+ let docURI = global.documentURI;
+ // Ensure the page is an about:blocked pagae before handling.
+ if (docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked")) {
+ let actor = global.getActor("BlockedSite");
+ actor.sendQuery("DeceptiveBlockedDetails").then(data => {
+ let reportUrl = gSafeBrowsing.getReportURL(
+ "PhishMistake",
+ data.blockedInfo
+ );
+ if (reportUrl) {
+ openTrustedLinkIn(reportUrl, "tab");
+ } else {
+ let bundle = Services.strings.createBundle(
+ "chrome://browser/locale/safebrowsing/safebrowsing.properties"
+ );
+ Services.prompt.alert(
+ window,
+ bundle.GetStringFromName("errorReportFalseDeceptiveTitle"),
+ bundle.formatStringFromName("errorReportFalseDeceptiveMessage", [
+ data.blockedInfo.provider,
+ ])
+ );
+ }
+ });
+ }
+
+ contextsToVisit.push(...currentContext.children);
+ }
+}
+
+/**
+ * Format a URL
+ * eg:
+ * echo formatURL("https://addons.mozilla.org/%LOCALE%/%APP%/%VERSION%/");
+ * > https://addons.mozilla.org/en-US/firefox/3.0a1/
+ *
+ * Currently supported built-ins are LOCALE, APP, and any value from nsIXULAppInfo, uppercased.
+ */
+function formatURL(aFormat, aIsPref) {
+ return aIsPref
+ ? Services.urlFormatter.formatURLPref(aFormat)
+ : Services.urlFormatter.formatURL(aFormat);
+}
+
+/**
+ * When the browser is being controlled from out-of-process,
+ * e.g. when Marionette or the remote debugging protocol is used,
+ * we add a visual hint to the browser UI to indicate to the user
+ * that the browser session is under remote control.
+ *
+ * This is called when the content browser initialises (from gBrowserInit.onLoad())
+ * and when the "remote-listening" system notification fires.
+ */
+const gRemoteControl = {
+ observe(subject, topic, data) {
+ gRemoteControl.updateVisualCue();
+ },
+
+ updateVisualCue() {
+ const mainWindow = document.documentElement;
+ if (Marionette.running || RemoteAgent.listening) {
+ mainWindow.setAttribute("remotecontrol", "true");
+ } else {
+ mainWindow.removeAttribute("remotecontrol");
+ }
+ },
+};
+
+const gAccessibilityServiceIndicator = {
+ init() {
+ // Pref to enable accessibility service indicator.
+ Services.prefs.addObserver("accessibility.indicator.enabled", this);
+ // Accessibility service init/shutdown event.
+ Services.obs.addObserver(this, "a11y-init-or-shutdown");
+ this._update(Services.appinfo.accessibilityEnabled);
+ },
+
+ _update(accessibilityEnabled = false) {
+ if (this.enabled && accessibilityEnabled) {
+ this._active = true;
+ document.documentElement.setAttribute("accessibilitymode", "true");
+ [
+ ...document.querySelectorAll(".accessibility-indicator"),
+ ].forEach(indicator =>
+ ["click", "keypress"].forEach(type =>
+ indicator.addEventListener(type, this)
+ )
+ );
+ } else if (this._active) {
+ this._active = false;
+ document.documentElement.removeAttribute("accessibilitymode");
+ [
+ ...document.querySelectorAll(".accessibility-indicator"),
+ ].forEach(indicator =>
+ ["click", "keypress"].forEach(type =>
+ indicator.removeEventListener(type, this)
+ )
+ );
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (
+ topic == "nsPref:changed" &&
+ data === "accessibility.indicator.enabled"
+ ) {
+ this._update(Services.appinfo.accessibilityEnabled);
+ } else if (topic === "a11y-init-or-shutdown") {
+ // When "a11y-init-or-shutdown" event is fired, "1" indicates that
+ // accessibility service is started and "0" that it is shut down.
+ this._update(data === "1");
+ }
+ },
+
+ get enabled() {
+ return Services.prefs.getBoolPref("accessibility.indicator.enabled");
+ },
+
+ handleEvent({ key, type }) {
+ if (
+ (type === "keypress" && [" ", "Enter"].includes(key)) ||
+ type === "click"
+ ) {
+ let a11yServicesSupportURL = Services.urlFormatter.formatURLPref(
+ "accessibility.support.url"
+ );
+ // This is a known URL coming from trusted UI
+ openTrustedLinkIn(a11yServicesSupportURL, "tab");
+ Services.telemetry.scalarSet("a11y.indicator_acted_on", true);
+ }
+ },
+
+ uninit() {
+ Services.prefs.removeObserver("accessibility.indicator.enabled", this);
+ Services.obs.removeObserver(this, "a11y-init-or-shutdown");
+ },
+};
+
+// Note that this is also called from non-browser windows on OSX, which do
+// share menu items but not much else. See nonbrowser-mac.js.
+var gPrivateBrowsingUI = {
+ init: function PBUI_init() {
+ // Do nothing for normal windows
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+
+ // Disable the Clear Recent History... menu item when in PB mode
+ // temporary fix until bug 463607 is fixed
+ document.getElementById("Tools:Sanitize").setAttribute("disabled", "true");
+
+ if (window.location.href != AppConstants.BROWSER_CHROME_URL) {
+ return;
+ }
+
+ // Adjust the window's title
+ let docElement = document.documentElement;
+ docElement.setAttribute(
+ "privatebrowsingmode",
+ PrivateBrowsingUtils.permanentPrivateBrowsing ? "permanent" : "temporary"
+ );
+ gBrowser.updateTitlebar();
+
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ // Adjust the New Window menu entries
+ let newWindow = document.getElementById("menu_newNavigator");
+ let newPrivateWindow = document.getElementById("menu_newPrivateWindow");
+ if (newWindow && newPrivateWindow) {
+ newPrivateWindow.hidden = true;
+ newWindow.label = newPrivateWindow.label;
+ newWindow.accessKey = newPrivateWindow.accessKey;
+ newWindow.command = newPrivateWindow.command;
+ }
+ }
+ },
+};
+
+/**
+ * Switch to a tab that has a given URI, and focuses its browser window.
+ * If a matching tab is in this window, it will be switched to. Otherwise, other
+ * windows will be searched.
+ *
+ * @param aURI
+ * URI to search for
+ * @param aOpenNew
+ * True to open a new tab and switch to it, if no existing tab is found.
+ * If no suitable window is found, a new one will be opened.
+ * @param aOpenParams
+ * If switching to this URI results in us opening a tab, aOpenParams
+ * will be the parameter object that gets passed to openTrustedLinkIn. Please
+ * see the documentation for openTrustedLinkIn to see what parameters can be
+ * passed via this object.
+ * This object also allows:
+ * - 'ignoreFragment' property to be set to true to exclude fragment-portion
+ * matching when comparing URIs.
+ * If set to "whenComparing", the fragment will be unmodified.
+ * If set to "whenComparingAndReplace", the fragment will be replaced.
+ * - 'ignoreQueryString' boolean property to be set to true to exclude query string
+ * matching when comparing URIs.
+ * - 'replaceQueryString' boolean property to be set to true to exclude query string
+ * matching when comparing URIs and overwrite the initial query string with
+ * the one from the new URI.
+ * - 'adoptIntoActiveWindow' boolean property to be set to true to adopt the tab
+ * into the current window.
+ * @return True if an existing tab was found, false otherwise
+ */
+function switchToTabHavingURI(aURI, aOpenNew, aOpenParams = {}) {
+ // Certain URLs can be switched to irrespective of the source or destination
+ // window being in private browsing mode:
+ const kPrivateBrowsingWhitelist = new Set(["about:addons"]);
+
+ let ignoreFragment = aOpenParams.ignoreFragment;
+ let ignoreQueryString = aOpenParams.ignoreQueryString;
+ let replaceQueryString = aOpenParams.replaceQueryString;
+ let adoptIntoActiveWindow = aOpenParams.adoptIntoActiveWindow;
+
+ // These properties are only used by switchToTabHavingURI and should
+ // not be used as a parameter for the new load.
+ delete aOpenParams.ignoreFragment;
+ delete aOpenParams.ignoreQueryString;
+ delete aOpenParams.replaceQueryString;
+ delete aOpenParams.adoptIntoActiveWindow;
+
+ let isBrowserWindow = !!window.gBrowser;
+
+ // This will switch to the tab in aWindow having aURI, if present.
+ function switchIfURIInWindow(aWindow) {
+ // Only switch to the tab if neither the source nor the destination window
+ // are private and they are not in permanent private browsing mode
+ if (
+ !kPrivateBrowsingWhitelist.has(aURI.spec) &&
+ (PrivateBrowsingUtils.isWindowPrivate(window) ||
+ PrivateBrowsingUtils.isWindowPrivate(aWindow)) &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing
+ ) {
+ return false;
+ }
+
+ // Remove the query string, fragment, both, or neither from a given url.
+ function cleanURL(url, removeQuery, removeFragment) {
+ let ret = url;
+ if (removeFragment) {
+ ret = ret.split("#")[0];
+ if (removeQuery) {
+ // This removes a query, if present before the fragment.
+ ret = ret.split("?")[0];
+ }
+ } else if (removeQuery) {
+ // This is needed in case there is a fragment after the query.
+ let fragment = ret.split("#")[1];
+ ret = ret
+ .split("?")[0]
+ .concat(fragment != undefined ? "#".concat(fragment) : "");
+ }
+ return ret;
+ }
+
+ // Need to handle nsSimpleURIs here too (e.g. about:...), which don't
+ // work correctly with URL objects - so treat them as strings
+ let ignoreFragmentWhenComparing =
+ typeof ignoreFragment == "string" &&
+ ignoreFragment.startsWith("whenComparing");
+ let requestedCompare = cleanURL(
+ aURI.displaySpec,
+ ignoreQueryString || replaceQueryString,
+ ignoreFragmentWhenComparing
+ );
+ let browsers = aWindow.gBrowser.browsers;
+ for (let i = 0; i < browsers.length; i++) {
+ let browser = browsers[i];
+ let browserCompare = cleanURL(
+ browser.currentURI.displaySpec,
+ ignoreQueryString || replaceQueryString,
+ ignoreFragmentWhenComparing
+ );
+ if (requestedCompare == browserCompare) {
+ // If adoptIntoActiveWindow is set, and this is a cross-window switch,
+ // adopt the tab into the current window, after the active tab.
+ let doAdopt =
+ adoptIntoActiveWindow && isBrowserWindow && aWindow != window;
+
+ if (doAdopt) {
+ window.gBrowser.adoptTab(
+ aWindow.gBrowser.getTabForBrowser(browser),
+ window.gBrowser.tabContainer.selectedIndex + 1,
+ /* aSelectTab = */ true
+ );
+ } else {
+ aWindow.focus();
+ }
+
+ if (ignoreFragment == "whenComparingAndReplace" || replaceQueryString) {
+ browser.loadURI(aURI.spec, {
+ triggeringPrincipal:
+ aOpenParams.triggeringPrincipal ||
+ _createNullPrincipalFromTabUserContextId(),
+ });
+ }
+
+ if (!doAdopt) {
+ aWindow.gBrowser.tabContainer.selectedIndex = i;
+ }
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // This can be passed either nsIURI or a string.
+ if (!(aURI instanceof Ci.nsIURI)) {
+ aURI = Services.io.newURI(aURI);
+ }
+
+ // Prioritise this window.
+ if (isBrowserWindow && switchIfURIInWindow(window)) {
+ return true;
+ }
+
+ for (let browserWin of browserWindows()) {
+ // Skip closed (but not yet destroyed) windows,
+ // and the current window (which was checked earlier).
+ if (browserWin.closed || browserWin == window) {
+ continue;
+ }
+ if (switchIfURIInWindow(browserWin)) {
+ return true;
+ }
+ }
+
+ // No opened tab has that url.
+ if (aOpenNew) {
+ if (isBrowserWindow && gBrowser.selectedTab.isEmpty) {
+ openTrustedLinkIn(aURI.spec, "current", aOpenParams);
+ } else {
+ openTrustedLinkIn(aURI.spec, "tab", aOpenParams);
+ }
+ }
+
+ return false;
+}
+
+var RestoreLastSessionObserver = {
+ init() {
+ if (
+ SessionStore.canRestoreLastSession &&
+ !PrivateBrowsingUtils.isWindowPrivate(window)
+ ) {
+ Services.obs.addObserver(this, "sessionstore-last-session-cleared", true);
+ goSetCommandEnabled("Browser:RestoreLastSession", true);
+ } else if (SessionStore.willAutoRestore) {
+ document
+ .getElementById("Browser:RestoreLastSession")
+ .setAttribute("hidden", true);
+ }
+ },
+
+ observe() {
+ // The last session can only be restored once so there's
+ // no way we need to re-enable our menu item.
+ Services.obs.removeObserver(this, "sessionstore-last-session-cleared");
+ goSetCommandEnabled("Browser:RestoreLastSession", false);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+/* Observes menus and adjusts their size for better
+ * usability when opened via a touch screen. */
+var MenuTouchModeObserver = {
+ init() {
+ window.addEventListener("popupshowing", this, true);
+ },
+
+ handleEvent(event) {
+ let target = event.originalTarget;
+ if (event.mozInputSource == MouseEvent.MOZ_SOURCE_TOUCH) {
+ target.setAttribute("touchmode", "true");
+ } else {
+ target.removeAttribute("touchmode");
+ }
+ },
+
+ uninit() {
+ window.removeEventListener("popupshowing", this, true);
+ },
+};
+
+// Prompt user to restart the browser in safe mode
+function safeModeRestart() {
+ if (Services.appinfo.inSafeMode) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+
+ if (cancelQuit.data) {
+ return;
+ }
+
+ Services.startup.quit(
+ Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit
+ );
+ return;
+ }
+
+ Services.obs.notifyObservers(null, "restart-in-safe-mode");
+}
+
+/* duplicateTabIn duplicates tab in a place specified by the parameter |where|.
+ *
+ * |where| can be:
+ * "tab" new tab
+ * "tabshifted" same as "tab" but in background if default is to select new
+ * tabs, and vice versa
+ * "window" new window
+ *
+ * delta is the offset to the history entry that you want to load.
+ */
+function duplicateTabIn(aTab, where, delta) {
+ switch (where) {
+ case "window":
+ let otherWin = OpenBrowserWindow({
+ private: PrivateBrowsingUtils.isBrowserPrivate(aTab.linkedBrowser),
+ });
+ let delayedStartupFinished = (subject, topic) => {
+ if (
+ topic == "browser-delayed-startup-finished" &&
+ subject == otherWin
+ ) {
+ Services.obs.removeObserver(delayedStartupFinished, topic);
+ let otherGBrowser = otherWin.gBrowser;
+ let otherTab = otherGBrowser.selectedTab;
+ SessionStore.duplicateTab(otherWin, aTab, delta);
+ otherGBrowser.removeTab(otherTab, { animate: false });
+ }
+ };
+
+ Services.obs.addObserver(
+ delayedStartupFinished,
+ "browser-delayed-startup-finished"
+ );
+ break;
+ case "tabshifted":
+ SessionStore.duplicateTab(window, aTab, delta);
+ // A background tab has been opened, nothing else to do here.
+ break;
+ case "tab":
+ SessionStore.duplicateTab(window, aTab, delta, true, {
+ inBackground: false,
+ });
+ break;
+ }
+}
+
+var MousePosTracker = {
+ _listeners: new Set(),
+ _x: 0,
+ _y: 0,
+
+ /**
+ * Registers a listener.
+ *
+ * @param listener (object)
+ * A listener is expected to expose the following properties:
+ *
+ * getMouseTargetRect (function)
+ * Returns the rect that the MousePosTracker needs to alert
+ * the listener about if the mouse happens to be within it.
+ *
+ * onMouseEnter (function, optional)
+ * The function to be called if the mouse enters the rect
+ * returned by getMouseTargetRect. MousePosTracker always
+ * runs this inside of a requestAnimationFrame, since it
+ * assumes that the notification is used to update the DOM.
+ *
+ * onMouseLeave (function, optional)
+ * The function to be called if the mouse exits the rect
+ * returned by getMouseTargetRect. MousePosTracker always
+ * runs this inside of a requestAnimationFrame, since it
+ * assumes that the notification is used to update the DOM.
+ */
+ addListener(listener) {
+ if (this._listeners.has(listener)) {
+ return;
+ }
+
+ listener._hover = false;
+ this._listeners.add(listener);
+
+ this._callListener(listener);
+ },
+
+ removeListener(listener) {
+ this._listeners.delete(listener);
+ },
+
+ handleEvent(event) {
+ let fullZoom = window.windowUtils.fullZoom;
+ this._x = event.screenX / fullZoom - window.mozInnerScreenX;
+ this._y = event.screenY / fullZoom - window.mozInnerScreenY;
+
+ this._listeners.forEach(listener => {
+ try {
+ this._callListener(listener);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ });
+ },
+
+ _callListener(listener) {
+ let rect = listener.getMouseTargetRect();
+ let hover =
+ this._x >= rect.left &&
+ this._x <= rect.right &&
+ this._y >= rect.top &&
+ this._y <= rect.bottom;
+
+ if (hover == listener._hover) {
+ return;
+ }
+
+ listener._hover = hover;
+
+ if (hover) {
+ if (listener.onMouseEnter) {
+ listener.onMouseEnter();
+ }
+ } else if (listener.onMouseLeave) {
+ listener.onMouseLeave();
+ }
+ },
+};
+
+var ToolbarIconColor = {
+ _windowState: {
+ active: false,
+ fullscreen: false,
+ tabsintitlebar: false,
+ },
+ init() {
+ this._initialized = true;
+
+ window.addEventListener("activate", this);
+ window.addEventListener("deactivate", this);
+ window.addEventListener("toolbarvisibilitychange", this);
+ window.addEventListener("windowlwthemeupdate", this);
+
+ // If the window isn't active now, we assume that it has never been active
+ // before and will soon become active such that inferFromText will be
+ // called from the initial activate event.
+ if (Services.focus.activeWindow == window) {
+ this.inferFromText("activate");
+ }
+ },
+
+ uninit() {
+ this._initialized = false;
+
+ window.removeEventListener("activate", this);
+ window.removeEventListener("deactivate", this);
+ window.removeEventListener("toolbarvisibilitychange", this);
+ window.removeEventListener("windowlwthemeupdate", this);
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "activate":
+ case "deactivate":
+ case "windowlwthemeupdate":
+ this.inferFromText(event.type);
+ break;
+ case "toolbarvisibilitychange":
+ this.inferFromText(event.type, event.visible);
+ break;
+ }
+ },
+
+ // a cache of luminance values for each toolbar
+ // to avoid unnecessary calls to getComputedStyle
+ _toolbarLuminanceCache: new Map(),
+
+ inferFromText(reason, reasonValue) {
+ if (!this._initialized) {
+ return;
+ }
+ function parseRGB(aColorString) {
+ let rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/);
+ rgb.shift();
+ return rgb.map(x => parseInt(x));
+ }
+
+ switch (reason) {
+ case "activate": // falls through
+ case "deactivate":
+ this._windowState.active = reason === "activate";
+ break;
+ case "fullscreen":
+ this._windowState.fullscreen = reasonValue;
+ break;
+ case "windowlwthemeupdate":
+ // theme change, we'll need to recalculate all color values
+ this._toolbarLuminanceCache.clear();
+ break;
+ case "toolbarvisibilitychange":
+ // toolbar changes dont require reset of the cached color values
+ break;
+ case "tabsintitlebar":
+ this._windowState.tabsintitlebar = reasonValue;
+ break;
+ }
+
+ let toolbarSelector = ".browser-toolbar:not([collapsed=true])";
+ if (AppConstants.platform == "macosx") {
+ toolbarSelector += ":not([type=menubar])";
+ }
+
+ // The getComputedStyle calls and setting the brighttext are separated in
+ // two loops to avoid flushing layout and making it dirty repeatedly.
+ let cachedLuminances = this._toolbarLuminanceCache;
+ let luminances = new Map();
+ for (let toolbar of document.querySelectorAll(toolbarSelector)) {
+ // toolbars *should* all have ids, but guard anyway to avoid blowing up
+ let cacheKey =
+ toolbar.id && toolbar.id + JSON.stringify(this._windowState);
+ // lookup cached luminance value for this toolbar in this window state
+ let luminance = cacheKey && cachedLuminances.get(cacheKey);
+ if (isNaN(luminance)) {
+ let [r, g, b] = parseRGB(getComputedStyle(toolbar).color);
+ luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
+ if (cacheKey) {
+ cachedLuminances.set(cacheKey, luminance);
+ }
+ }
+ luminances.set(toolbar, luminance);
+ }
+
+ for (let [toolbar, luminance] of luminances) {
+ if (luminance <= 110) {
+ toolbar.removeAttribute("brighttext");
+ } else {
+ toolbar.setAttribute("brighttext", "true");
+ }
+ }
+ },
+};
+
+var PanicButtonNotifier = {
+ init() {
+ this._initialized = true;
+ if (window.PanicButtonNotifierShouldNotify) {
+ delete window.PanicButtonNotifierShouldNotify;
+ this.notify();
+ }
+ },
+ createPanelIfNeeded() {
+ // Lazy load the panic-button-success-notification panel the first time we need to display it.
+ if (!document.getElementById("panic-button-success-notification")) {
+ let template = document.getElementById("panicButtonNotificationTemplate");
+ template.replaceWith(template.content);
+ }
+ },
+ notify() {
+ if (!this._initialized) {
+ window.PanicButtonNotifierShouldNotify = true;
+ return;
+ }
+ // Display notification panel here...
+ try {
+ this.createPanelIfNeeded();
+ let popup = document.getElementById("panic-button-success-notification");
+ popup.hidden = false;
+ // To close the popup in 3 seconds after the popup is shown but left uninteracted.
+ let onTimeout = () => {
+ PanicButtonNotifier.close();
+ removeListeners();
+ };
+ popup.addEventListener("popupshown", function() {
+ PanicButtonNotifier.timer = setTimeout(onTimeout, 3000);
+ });
+ // To prevent the popup from closing when user tries to interact with the
+ // popup using mouse or keyboard.
+ let onUserInteractsWithPopup = () => {
+ clearTimeout(PanicButtonNotifier.timer);
+ removeListeners();
+ };
+ popup.addEventListener("mouseover", onUserInteractsWithPopup);
+ window.addEventListener("keydown", onUserInteractsWithPopup);
+ let removeListeners = () => {
+ popup.removeEventListener("mouseover", onUserInteractsWithPopup);
+ window.removeEventListener("keydown", onUserInteractsWithPopup);
+ popup.removeEventListener("popuphidden", removeListeners);
+ };
+ popup.addEventListener("popuphidden", removeListeners);
+
+ let widget = CustomizableUI.getWidget("panic-button").forWindow(window);
+ let anchor = widget.anchor.icon;
+ popup.openPopup(anchor, popup.getAttribute("position"));
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+ close() {
+ let popup = document.getElementById("panic-button-success-notification");
+ popup.hidePopup();
+ },
+};
+
+const SafeBrowsingNotificationBox = {
+ _currentURIBaseDomain: null,
+ show(title, buttons) {
+ let uri = gBrowser.currentURI;
+
+ // start tracking host so that we know when we leave the domain
+ try {
+ this._currentURIBaseDomain = Services.eTLD.getBaseDomain(uri);
+ } catch (e) {
+ // If we can't get the base domain, fallback to use host instead. However,
+ // host is sometimes empty when the scheme is file. In this case, just use
+ // spec.
+ this._currentURIBaseDomain = uri.asciiHost || uri.asciiSpec;
+ }
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let value = "blocked-badware-page";
+
+ let previousNotification = notificationBox.getNotificationWithValue(value);
+ if (previousNotification) {
+ notificationBox.removeNotification(previousNotification);
+ }
+
+ let notification = notificationBox.appendNotification(
+ title,
+ value,
+ "chrome://global/skin/icons/blocklist_favicon.png",
+ notificationBox.PRIORITY_CRITICAL_HIGH,
+ buttons
+ );
+ // Persist the notification until the user removes so it
+ // doesn't get removed on redirects.
+ notification.persistence = -1;
+ },
+ onLocationChange(aLocationURI) {
+ // take this to represent that you haven't visited a bad place
+ if (!this._currentURIBaseDomain) {
+ return;
+ }
+
+ let newURIBaseDomain = Services.eTLD.getBaseDomain(aLocationURI);
+
+ if (newURIBaseDomain !== this._currentURIBaseDomain) {
+ let notificationBox = gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue(
+ "blocked-badware-page"
+ );
+ if (notification) {
+ notificationBox.removeNotification(notification, false);
+ }
+
+ this._currentURIBaseDomain = null;
+ }
+ },
+};
+
+/**
+ * The TabDialogBox supports opening window dialogs as SubDialogs on the tab and content
+ * level. Both tab and content dialogs have their own separate managers.
+ * Dialogs will be queued FIFO and cover the web content.
+ * Dialogs are closed when the user reloads or leaves the page.
+ * While a dialog is open PopupNotifications, such as permission prompts, are
+ * suppressed.
+ */
+class TabDialogBox {
+ constructor(browser) {
+ this._weakBrowserRef = Cu.getWeakReference(browser);
+
+ // Create parent element for tab dialogs
+ let template = document.getElementById("dialogStackTemplate");
+ let dialogStack = template.content.cloneNode(true).firstElementChild;
+ dialogStack.classList.add("tab-prompt-dialog");
+
+ this.browser.parentNode.insertBefore(
+ dialogStack,
+ this.browser.nextElementSibling
+ );
+
+ // Initially the stack only contains the template
+ let dialogTemplate = dialogStack.firstElementChild;
+
+ // Create dialog manager for prompts at the tab level.
+ this._tabDialogManager = new SubDialogManager({
+ dialogStack,
+ dialogTemplate,
+ orderType: SubDialogManager.ORDER_QUEUE,
+ allowDuplicateDialogs: true,
+ dialogOptions: {
+ consumeOutsideClicks: false,
+ },
+ });
+ }
+
+ /**
+ * Open a dialog on tab or content level.
+ * @param {String} aURL - URL of the dialog to load in the tab box.
+ * @param {Object} [aOptions]
+ * @param {String} [aOptions.features] - Comma separated list of window
+ * features.
+ * @param {Boolean} [aOptions.allowDuplicateDialogs] - Whether to allow
+ * showing multiple dialogs with aURL at the same time. If false calls for
+ * duplicate dialogs will be dropped.
+ * @param {String} [aOptions.sizeTo] - Pass "available" to stretch dialog to
+ * roughly content size.
+ * @param {Boolean} [aOptions.keepOpenSameOriginNav] - By default dialogs are
+ * aborted on any navigation.
+ * Set to true to keep the dialog open for same origin navigation.
+ * @param {Number} [aOptions.modalType] - The modal type to create the dialog for.
+ * By default, we show the dialog for tab prompts.
+ * @returns {Promise} - Resolves once the dialog has been closed.
+ */
+ open(
+ aURL,
+ {
+ features = null,
+ allowDuplicateDialogs = true,
+ sizeTo,
+ keepOpenSameOriginNav,
+ modalType = null,
+ } = {},
+ ...aParams
+ ) {
+ return new Promise(resolve => {
+ // Get the dialog manager to open the prompt with.
+ let dialogManager =
+ modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT
+ ? this.getContentDialogManager()
+ : this._tabDialogManager;
+ let hasDialogs =
+ this._tabDialogManager.hasDialogs ||
+ this._contentDialogManager?.hasDialogs;
+
+ if (!hasDialogs) {
+ this._onFirstDialogOpen();
+ }
+
+ let closingCallback = () => {
+ if (!hasDialogs) {
+ this._onLastDialogClose();
+ }
+ };
+
+ // Open dialog and resolve once it has been closed
+ let dialog = dialogManager.open(
+ aURL,
+ {
+ features,
+ allowDuplicateDialogs,
+ sizeTo,
+ closingCallback,
+ closedCallback: resolve,
+ },
+ ...aParams
+ );
+
+ // Marking the dialog externally, instead of passing it as an option.
+ // The SubDialog(Manager) does not care about navigation.
+ // dialog can be null here if allowDuplicateDialogs = false.
+ if (dialog) {
+ dialog._keepOpenSameOriginNav = keepOpenSameOriginNav;
+ }
+ });
+ }
+
+ _onFirstDialogOpen() {
+ // Hide PopupNotifications to prevent them from covering up dialogs.
+ this.browser.setAttribute("tabDialogShowing", true);
+ UpdatePopupNotificationsVisibility();
+
+ // Register listeners
+ this._lastPrincipal = this.browser.contentPrincipal;
+ this.browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+
+ this.tab?.addEventListener("TabClose", this);
+ }
+
+ _onLastDialogClose() {
+ // Show PopupNotifications again.
+ this.browser.removeAttribute("tabDialogShowing");
+ UpdatePopupNotificationsVisibility();
+
+ // Clean up listeners
+ this.browser.removeProgressListener(this);
+ this._lastPrincipal = null;
+
+ this.tab?.removeEventListener("TabClose", this);
+ }
+
+ _buildContentPromptDialog() {
+ let template = document.getElementById("dialogStackTemplate");
+ let contentDialogStack = template.content.cloneNode(true).firstElementChild;
+ contentDialogStack.classList.add("content-prompt-dialog");
+
+ // Create a dialog manager for content prompts.
+ let tabPromptDialog = this.browser.parentNode.querySelector(
+ ".tab-prompt-dialog"
+ );
+ this.browser.parentNode.insertBefore(contentDialogStack, tabPromptDialog);
+
+ let contentDialogTemplate = contentDialogStack.firstElementChild;
+ this._contentDialogManager = new SubDialogManager({
+ dialogStack: contentDialogStack,
+ dialogTemplate: contentDialogTemplate,
+ orderType: SubDialogManager.ORDER_QUEUE,
+ allowDuplicateDialogs: true,
+ dialogOptions: {
+ consumeOutsideClicks: false,
+ },
+ });
+ }
+
+ handleEvent(event) {
+ if (event.type !== "TabClose") {
+ return;
+ }
+ this.abortAllDialogs();
+ }
+
+ abortAllDialogs() {
+ this._tabDialogManager.abortDialogs();
+ this._contentDialogManager?.abortDialogs();
+ }
+
+ focus() {
+ // Prioritize focusing the dialog manager for tab prompts
+ if (this._tabDialogManager._dialogs.length) {
+ this._tabDialogManager.focusTopDialog();
+ return;
+ }
+ this._contentDialogManager?.focusTopDialog();
+ }
+
+ /**
+ * If the user navigates away or refreshes the page, close all dialogs for
+ * the current browser.
+ */
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (
+ !aWebProgress.isTopLevel ||
+ aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
+ ) {
+ return;
+ }
+
+ // Dialogs can be exempt from closing on same origin location change.
+ let filterFn;
+
+ // Test for same origin location change
+ if (
+ this._lastPrincipal?.isSameOrigin(
+ aLocation,
+ this.browser.browsingContext.usePrivateBrowsing
+ )
+ ) {
+ filterFn = dialog => !dialog._keepOpenSameOriginNav;
+ }
+
+ this._lastPrincipal = this.browser.contentPrincipal;
+
+ this._tabDialogManager.abortDialogs(filterFn);
+ this._contentDialogManager?.abortDialogs(filterFn);
+ }
+
+ get tab() {
+ return gBrowser.getTabForBrowser(this.browser);
+ }
+
+ get browser() {
+ let browser = this._weakBrowserRef.get();
+ if (!browser) {
+ throw new Error("Stale dialog box! The associated browser is gone.");
+ }
+ return browser;
+ }
+
+ getTabDialogManager() {
+ return this._tabDialogManager;
+ }
+
+ getContentDialogManager() {
+ if (!this._contentDialogManager) {
+ this._buildContentPromptDialog();
+ }
+ return this._contentDialogManager;
+ }
+}
+
+TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+]);
+
+function TabModalPromptBox(browser) {
+ this._weakBrowserRef = Cu.getWeakReference(browser);
+ /*
+ * These WeakMaps holds the TabModalPrompt instances, key to the <tabmodalprompt> prompt
+ * in the DOM. We don't want to hold the instances directly to avoid leaking.
+ *
+ * WeakMap also prevents us from reading back its insertion order.
+ * Order of the elements in the DOM should be the only order to consider.
+ */
+ this._contentPrompts = new WeakMap();
+ this._tabPrompts = new WeakMap();
+}
+
+TabModalPromptBox.prototype = {
+ _promptCloseCallback(
+ onCloseCallback,
+ principalToAllowFocusFor,
+ allowFocusCheckbox,
+ ...args
+ ) {
+ if (
+ principalToAllowFocusFor &&
+ allowFocusCheckbox &&
+ allowFocusCheckbox.checked
+ ) {
+ Services.perms.addFromPrincipal(
+ principalToAllowFocusFor,
+ "focus-tab-by-prompt",
+ Services.perms.ALLOW_ACTION
+ );
+ }
+ onCloseCallback.apply(this, args);
+ },
+
+ getPrompt(promptEl) {
+ if (promptEl.classList.contains("tab-prompt")) {
+ return this._tabPrompts.get(promptEl);
+ }
+ return this._contentPrompts.get(promptEl);
+ },
+
+ appendPrompt(args, onCloseCallback) {
+ let browser = this.browser;
+ let newPrompt = new TabModalPrompt(browser.ownerGlobal);
+
+ if (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ newPrompt.element.classList.add("tab-prompt");
+ this._tabPrompts.set(newPrompt.element, newPrompt);
+ } else {
+ newPrompt.element.classList.add("content-prompt");
+ this._contentPrompts.set(newPrompt.element, newPrompt);
+ }
+
+ browser.parentNode.insertBefore(
+ newPrompt.element,
+ browser.nextElementSibling
+ );
+ browser.setAttribute("tabmodalPromptShowing", true);
+
+ // Indicate if a tab modal chrome prompt is being shown so that
+ // PopupNotifications are suppressed.
+ if (
+ args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB &&
+ !browser.hasAttribute("tabmodalChromePromptShowing")
+ ) {
+ browser.setAttribute("tabmodalChromePromptShowing", true);
+ // Notify PopupNotifications of the UI change so it hides the notification
+ // panel.
+ PopupNotifications.anchorVisibilityChange();
+ }
+
+ let prompts = this.listPrompts(args.modalType);
+ if (prompts.length > 1) {
+ // Let's hide ourself behind the current prompt.
+ newPrompt.element.hidden = true;
+ }
+
+ let principalToAllowFocusFor = this._allowTabFocusByPromptPrincipal;
+ delete this._allowTabFocusByPromptPrincipal;
+
+ let allowFocusCheckbox; // Define outside the if block so we can bind it into the callback.
+ let hostForAllowFocusCheckbox = "";
+ try {
+ hostForAllowFocusCheckbox = principalToAllowFocusFor.URI.host;
+ } catch (ex) {
+ /* Ignore exceptions for host-less URIs */
+ }
+ if (hostForAllowFocusCheckbox) {
+ let allowFocusRow = document.createElement("div");
+
+ let spacer = document.createElement("div");
+ allowFocusRow.appendChild(spacer);
+
+ allowFocusCheckbox = document.createXULElement("checkbox");
+ let label = gTabBrowserBundle.formatStringFromName(
+ "tabs.allowTabFocusByPromptForSite",
+ [hostForAllowFocusCheckbox]
+ );
+ allowFocusCheckbox.setAttribute("label", label);
+ allowFocusRow.appendChild(allowFocusCheckbox);
+
+ newPrompt.ui.rows.append(allowFocusRow);
+ }
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ let closeCB = this._promptCloseCallback.bind(
+ null,
+ onCloseCallback,
+ principalToAllowFocusFor,
+ allowFocusCheckbox
+ );
+ newPrompt.init(args, tab, closeCB);
+ return newPrompt;
+ },
+
+ removePrompt(aPrompt) {
+ let { modalType } = aPrompt.args;
+ if (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ this._tabPrompts.delete(aPrompt.element);
+ } else {
+ this._contentPrompts.delete(aPrompt.element);
+ }
+
+ let browser = this.browser;
+ aPrompt.element.remove();
+
+ let prompts = this.listPrompts(modalType);
+ if (prompts.length) {
+ let prompt = prompts[prompts.length - 1];
+ prompt.element.hidden = false;
+ // Because we were hidden before, this won't have been possible, so do it now:
+ prompt.Dialog.setDefaultFocus();
+ } else if (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ // If we remove the last tab chrome prompt, also remove the browser
+ // attribute.
+ browser.removeAttribute("tabmodalChromePromptShowing");
+ // Notify PopupNotifications of the UI change so it shows the notification
+ // panel again.
+ PopupNotifications.anchorVisibilityChange();
+ }
+ // Check if all prompts are closed
+ if (!this._hasPrompts()) {
+ browser.removeAttribute("tabmodalPromptShowing");
+ browser.focus();
+ }
+ },
+
+ /**
+ * Checks if the prompt box has prompt elements.
+ * @returns {Boolean} - true if there are prompt elements.
+ */
+ _hasPrompts() {
+ return !!this._getPromptElements().length;
+ },
+
+ /**
+ * Get list of current prompt elements.
+ * @param {Number} [aModalType] - Optionally filter by
+ * Ci.nsIPrompt.MODAL_TYPE_.
+ * @returns {NodeList} - A list of tabmodalprompt elements.
+ */
+ _getPromptElements(aModalType = null) {
+ let selector = "tabmodalprompt";
+
+ if (aModalType != null) {
+ if (aModalType === Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ selector += ".tab-prompt";
+ } else {
+ selector += ".content-prompt";
+ }
+ }
+ return this.browser.parentNode.querySelectorAll(selector);
+ },
+
+ /**
+ * Get a list of all TabModalPrompt objects associated with the prompt box.
+ * @param {Number} [aModalType] - Optionally filter by
+ * Ci.nsIPrompt.MODAL_TYPE_.
+ * @returns {TabModalPrompt[]} - An array of TabModalPrompt objects.
+ */
+ listPrompts(aModalType = null) {
+ // Get the nodelist, then return the TabModalPrompt instances as an array
+ let promptMap;
+
+ if (aModalType) {
+ if (aModalType === Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ promptMap = this._tabPrompts;
+ } else {
+ promptMap = this._contentPrompts;
+ }
+ }
+
+ let elements = this._getPromptElements(aModalType);
+
+ if (promptMap) {
+ return [...elements].map(el => promptMap.get(el));
+ }
+ return [...elements].map(
+ el => this._contentPrompts.get(el) || this._tabPrompts.get(el)
+ );
+ },
+
+ onNextPromptShowAllowFocusCheckboxFor(principal) {
+ this._allowTabFocusByPromptPrincipal = principal;
+ },
+
+ get browser() {
+ let browser = this._weakBrowserRef.get();
+ if (!browser) {
+ throw new Error("Stale promptbox! The associated browser is gone.");
+ }
+ return browser;
+ },
+};
+
+var ConfirmationHint = {
+ _timerID: null,
+
+ /**
+ * Shows a transient, non-interactive confirmation hint anchored to an
+ * element, usually used in response to a user action to reaffirm that it was
+ * successful and potentially provide extra context. Examples for such hints:
+ * - "Saved to Library!" after bookmarking a page
+ * - "Sent!" after sending a tab to another device
+ * - "Queued (offline)" when attempting to send a tab to another device
+ * while offline
+ *
+ * @param anchor (DOM node, required)
+ * The anchor for the panel.
+ * @param messageId (string, required)
+ * For getting the message string from browser.properties:
+ * confirmationHint.<messageId>.label
+ * @param options (object, optional)
+ * An object with the following optional properties:
+ * - event (DOM event): The event that triggered the feedback.
+ * - hideArrow (boolean): Optionally hide the arrow.
+ * - showDescription (boolean): show description text (confirmationHint.<messageId>.description)
+ *
+ */
+ show(anchor, messageId, options = {}) {
+ this._reset();
+
+ this._message.textContent = gBrowserBundle.GetStringFromName(
+ `confirmationHint.${messageId}.label`
+ );
+
+ if (options.showDescription) {
+ this._description.textContent = gBrowserBundle.GetStringFromName(
+ `confirmationHint.${messageId}.description`
+ );
+ this._description.hidden = false;
+ this._panel.classList.add("with-description");
+ } else {
+ this._description.hidden = true;
+ this._panel.classList.remove("with-description");
+ }
+
+ if (options.hideArrow) {
+ this._panel.setAttribute("hidearrow", "true");
+ }
+
+ this._panel.setAttribute("data-message-id", messageId);
+
+ // The timeout value used here allows the panel to stay open for
+ // 1.5s second after the text transition (duration=120ms) has finished.
+ // If there is a description, we show for 4s after the text transition.
+ const DURATION = options.showDescription ? 4000 : 1500;
+ this._panel.addEventListener(
+ "popupshown",
+ () => {
+ this._animationBox.setAttribute("animate", "true");
+ this._timerID = setTimeout(() => {
+ this._panel.hidePopup(true);
+ }, DURATION + 120);
+ },
+ { once: true }
+ );
+
+ this._panel.addEventListener(
+ "popuphidden",
+ () => {
+ // reset the timerId in case our timeout wasn't the cause of the popup being hidden
+ this._reset();
+ },
+ { once: true }
+ );
+
+ this._panel.openPopup(anchor, {
+ position: "bottomcenter topleft",
+ triggerEvent: options.event,
+ });
+ },
+
+ _reset() {
+ if (this._timerID) {
+ clearTimeout(this._timerID);
+ this._timerID = null;
+ }
+ if (this.__panel) {
+ this._panel.removeAttribute("hidearrow");
+ this._animationBox.removeAttribute("animate");
+ this._panel.removeAttribute("data-message-id");
+ }
+ },
+
+ get _panel() {
+ this._ensurePanel();
+ return this.__panel;
+ },
+
+ get _animationBox() {
+ this._ensurePanel();
+ delete this._animationBox;
+ return (this._animationBox = document.getElementById(
+ "confirmation-hint-checkmark-animation-container"
+ ));
+ },
+
+ get _message() {
+ this._ensurePanel();
+ delete this._message;
+ return (this._message = document.getElementById(
+ "confirmation-hint-message"
+ ));
+ },
+
+ get _description() {
+ this._ensurePanel();
+ delete this._description;
+ return (this._description = document.getElementById(
+ "confirmation-hint-description"
+ ));
+ },
+
+ _ensurePanel() {
+ if (!this.__panel) {
+ let wrapper = document.getElementById("confirmation-hint-wrapper");
+ wrapper.replaceWith(wrapper.content);
+ this.__panel = document.getElementById("confirmation-hint");
+ }
+ },
+};
+
+function reportRemoteSubframesEnabledTelemetry() {
+ let categoryLabel = gFissionBrowser ? "Enabled" : "Disabled";
+ if (gFissionBrowser == Services.appinfo.fissionAutostart) {
+ categoryLabel += "ByAutostart";
+ } else {
+ categoryLabel += "ByUser";
+ }
+
+ Services.telemetry
+ .getHistogramById("WINDOW_REMOTE_SUBFRAMES_ENABLED_STATUS")
+ .add(categoryLabel);
+}
+
+if (AppConstants.NIGHTLY_BUILD) {
+ var FissionTestingUI = {
+ init() {
+ // Handle the Fission/Non-Fission testing UI.
+ if (!Services.appinfo.fissionAutostart) {
+ return;
+ }
+
+ const openNonFissionWindowOption = Services.prefs.getBoolPref(
+ "fission.openNonFissionWindowOption",
+ false
+ );
+ if (!openNonFissionWindowOption) {
+ return;
+ }
+
+ let newFissionWindow = document.getElementById("Tools:FissionWindow");
+ let newNonFissionWindow = document.getElementById(
+ "Tools:NonFissionWindow"
+ );
+
+ newFissionWindow.hidden = gFissionBrowser;
+ newNonFissionWindow.hidden = !gFissionBrowser;
+ },
+ };
+}
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
new file mode 100644
index 0000000000..da7d002aba
--- /dev/null
+++ b/browser/base/content/browser.xhtml
@@ -0,0 +1,2341 @@
+#filter substitution
+<?xml version="1.0"?>
+# -*- Mode: 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/.
+
+<!-- The "global.css" stylesheet is imported first to allow other stylesheets to
+ override rules using selectors with the same specificity. This applies to
+ both "content" and "skin" packages, which bug 1385444 will unify later. -->
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<!-- While these stylesheets are defined in Toolkit, they are only used in the
+ main browser window, so we can load them here. Bug 1474241 is on file to
+ consider moving these widgets to the "browser" folder. -->
+<?xml-stylesheet href="chrome://global/content/tabprompts.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/tabprompts.css" type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/tabbrowser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/downloads/downloads.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/usercontext/usercontext.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/controlcenter/panel.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/customizableui/panelUI.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/downloads/downloads.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/searchbar.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmark.css" type="text/css"?>
+
+# All DTD information is stored in a separate file so that it can be shared by
+# hiddenWindowMac.xhtml.
+<!DOCTYPE window [
+#include browser-doctype.inc
+]>
+
+<html id="main-window"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns="http://www.w3.org/1999/xhtml"
+#ifdef XP_MACOSX
+ data-l10n-id="browser-main-window-mac"
+#else
+ data-l10n-id="browser-main-window"
+#endif
+ data-l10n-args="{&quot;content-title&quot;:&quot;CONTENTTITLE&quot;}"
+ data-l10n-attrs="data-content-title-default, data-content-title-private, data-title-default, data-title-private"
+#ifdef XP_WIN
+ chromemargin="0,2,2,2"
+#else
+ chromemargin="0,-1,-1,-1"
+#endif
+ tabsintitlebar="true"
+ windowtype="navigator:browser"
+ macanimationtype="document"
+ macnativefullscreen="true"
+ screenX="4" screenY="4"
+ sizemode="normal"
+ retargetdocumentfocus="urlbar-input"
+ scrolling="false"
+ persist="screenX screenY width height sizemode"
+ data-l10n-sync="true">
+<head>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="browser/branding/sync-brand.ftl"/>
+ <link rel="localization" href="browser/branding/brandings.ftl"/>
+ <link rel="localization" href="toolkit/global/textActions.ftl"/>
+ <link rel="localization" href="browser/browser.ftl"/>
+ <link rel="localization" href="browser/browserContext.ftl"/>
+ <link rel="localization" href="browser/browserSets.ftl"/>
+ <link rel="localization" href="browser/menubar.ftl"/>
+ <link rel="localization" href="browser/protectionsPanel.ftl"/>
+ <link rel="localization" href="browser/appmenu.ftl"/>
+ <link rel="localization" href="preview/interventions.ftl"/>
+ <link rel="localization" href="browser/sidebarMenu.ftl"/>
+ <link rel="localization" href="browser/allTabsMenu.ftl"/>
+ <link rel="localization" href="browser/places.ftl"/>
+ <link rel="localization" href="toolkit/printing/printUI.ftl"/>
+
+ <title data-l10n-id="browser-main-window-title"></title>
+
+# All JS files which are needed by browser.xhtml and other top level windows to
+# support MacOS specific features *must* go into the global-scripts.inc file so
+# that they can be shared with macWindow.inc.xhtml.
+#include global-scripts.inc
+
+<script>
+ /* eslint-env mozilla/browser-window */
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/browser-captivePortal.js", this);
+ if (AppConstants.MOZ_DATA_REPORTING) {
+ Services.scriptloader.loadSubScript("chrome://browser/content/browser-data-submission-info-bar.js", this);
+ }
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Services.scriptloader.loadSubScript("chrome://browser/content/browser-development-helpers.js", this);
+ }
+ Services.scriptloader.loadSubScript("chrome://browser/content/browser-pageActions.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/browser-sidebar.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/browser-tabsintitlebar.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser-tab.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser-tabs.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/places/places-menupopup.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
+
+ window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
+ window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
+ window.onclose = WindowIsClosing;
+
+ window.addEventListener("MozBeforeInitialXULLayout",
+ gBrowserInit.onBeforeInitialXULLayout.bind(gBrowserInit), { once: true });
+
+ // The listener of DOMContentLoaded must be set on window, rather than
+ // document, because the window can go away before the event is fired.
+ // In that case, we don't want to initialize anything, otherwise we
+ // may be leaking things because they will never be destroyed after.
+ window.addEventListener("DOMContentLoaded",
+ gBrowserInit.onDOMContentLoaded.bind(gBrowserInit), { once: true });
+</script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+# All sets except for popupsets (commands, keys, and stringbundles)
+# *must* go into the browser-sets.inc file so that they can be shared with other
+# top level windows in macWindow.inc.xhtml.
+#include browser-sets.inc
+ <popupset id="mainPopupSet">
+ <menupopup id="tabContextMenu"
+ onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);"
+ onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;">
+ <menuitem id="context_reloadTab" data-lazy-l10n-id="reload-tab"
+ oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/>
+ <menuitem id="context_reloadSelectedTabs" data-lazy-l10n-id="reload-tabs" hidden="true"
+ oncommand="gBrowser.reloadMultiSelectedTabs();"/>
+ <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
+ <menuitem id="context_toggleMuteSelectedTabs" hidden="true"
+ oncommand="gBrowser.toggleMuteAudioOnMultiSelectedTabs(TabContextMenu.contextTab);"/>
+ <menuitem id="context_pinTab" data-lazy-l10n-id="pin-tab"
+ oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>
+ <menuitem id="context_unpinTab" data-lazy-l10n-id="unpin-tab" hidden="true"
+ oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/>
+ <menuitem id="context_pinSelectedTabs" data-lazy-l10n-id="pin-selected-tabs" hidden="true"
+ oncommand="gBrowser.pinMultiSelectedTabs();"/>
+ <menuitem id="context_unpinSelectedTabs" data-lazy-l10n-id="unpin-selected-tabs" hidden="true"
+ oncommand="gBrowser.unpinMultiSelectedTabs();"/>
+ <menuitem id="context_duplicateTab" data-lazy-l10n-id="duplicate-tab"
+ oncommand="duplicateTabIn(TabContextMenu.contextTab, 'tab');"/>
+ <menuitem id="context_duplicateTabs" data-lazy-l10n-id="duplicate-tabs"
+ oncommand="TabContextMenu.duplicateSelectedTabs();"/>
+ <menuseparator/>
+ <menuitem id="context_selectAllTabs" data-lazy-l10n-id="select-all-tabs"
+ oncommand="gBrowser.selectAllTabs();"/>
+ <menuitem id="context_bookmarkSelectedTabs"
+ hidden="true"
+ data-lazy-l10n-id="bookmark-selected-tabs"
+ oncommand="PlacesUIUtils.showBookmarkPagesDialog(PlacesCommandHook.uniqueSelectedPages);"/>
+ <menuitem id="context_bookmarkTab"
+ data-lazy-l10n-id="bookmark-tab"
+ oncommand="PlacesUIUtils.showBookmarkPagesDialog(PlacesCommandHook.getUniquePages([TabContextMenu.contextTab]));"/>
+ <menu id="context_reopenInContainer"
+ data-lazy-l10n-id="reopen-in-container"
+ hidden="true">
+ <menupopup oncommand="TabContextMenu.reopenInContainer(event);"
+ onpopupshowing="TabContextMenu.createReopenInContainerMenu(event);"/>
+ </menu>
+ <menu id="context_moveTabOptions"
+ data-lazy-l10n-id="tab-context-move-tabs"
+ data-l10n-args='{"tabCount": 1}'>
+ <menupopup id="moveTabOptionsMenu">
+ <menuitem id="context_moveToStart"
+ data-lazy-l10n-id="move-to-start"
+ tbattr="tabbrowser-multiple"
+ oncommand="gBrowser.moveTabsToStart(TabContextMenu.contextTab);"/>
+ <menuitem id="context_moveToEnd"
+ data-lazy-l10n-id="move-to-end"
+ tbattr="tabbrowser-multiple"
+ oncommand="gBrowser.moveTabsToEnd(TabContextMenu.contextTab);"/>
+ <menuitem id="context_openTabInWindow" data-lazy-l10n-id="move-to-new-window"
+ tbattr="tabbrowser-multiple"
+ oncommand="gBrowser.replaceTabsWithWindow(TabContextMenu.contextTab);"/>
+ </menupopup>
+ </menu>
+ <menu id="context_sendTabToDevice"
+ class="sync-ui-item">
+ <menupopup id="context_sendTabToDevicePopupMenu"
+ onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle, TabContextMenu.contextTab.multiselected);"/>
+ </menu>
+ <menuseparator/>
+ <menu id="context_closeTabOptions"
+ data-lazy-l10n-id="tab-context-close-multiple-tabs">
+ <menupopup id="closeTabOptions">
+ <menuitem id="context_closeTabsToTheEnd" data-lazy-l10n-id="close-tabs-to-the-end"
+ oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab, {animate: true});"/>
+ <menuitem id="context_closeOtherTabs" data-lazy-l10n-id="close-other-tabs"
+ oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/>
+ </menupopup>
+ </menu>
+ <menuitem id="context_undoCloseTab"
+ data-lazy-l10n-id="tab-context-undo-close-tabs"
+ data-l10n-args='{"tabCount": 1}'
+ observes="History:UndoCloseTab"/>
+ <menuitem id="context_closeTab"
+ data-lazy-l10n-id="tab-context-close-tabs"
+ data-l10n-args='{"tabCount": 1}'
+ oncommand="TabContextMenu.closeContextTabs();"/>
+ </menupopup>
+
+ <!-- bug 415444/582485: event.stopPropagation is here for the cloned version
+ of this menupopup, to prevent already-handled clicks on menu items from
+ propagating to the back or forward button.
+ -->
+ <menupopup id="backForwardMenu"
+ onpopupshowing="return FillHistoryMenu(event.target);"
+ oncommand="gotoHistoryIndex(event); event.stopPropagation();"
+ onclick="checkForMiddleClick(this, event);"/>
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <tooltip id="remoteBrowserTooltip"/>
+
+ <menupopup id="new-tab-button-popup"
+ onpopupshowing="return CreateContainerTabMenu(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <!-- for search and content formfill/pw manager -->
+
+ <panel is="autocomplete-richlistbox-popup"
+ type="autocomplete-richlistbox"
+ id="PopupAutoComplete"
+ role="group"
+ noautofocus="true"
+ hidden="true"
+ overflowpadding="4"
+ norolluponanchor="true"
+ nomaxresults="true" />
+
+ <!-- for search with one-off buttons -->
+ <panel is="search-autocomplete-richlistbox-popup"
+ type="autocomplete-richlistbox"
+ id="PopupSearchAutoComplete"
+ role="group"
+ noautofocus="true"
+ hidden="true" />
+
+ <html:template id="dateTimePickerTemplate">
+ <!-- for date/time picker. consumeoutsideclicks is set to never, so that
+ clicks on the anchored input box are never consumed. -->
+ <panel id="DateTimePickerPanel"
+ type="arrow"
+ hidden="true"
+ orient="vertical"
+ noautofocus="true"
+ norolluponanchor="true"
+ consumeoutsideclicks="never"
+ level="parent"
+ tabspecific="true">
+ </panel>
+ </html:template>
+
+ <!-- for select dropdowns. The menupopup is what shows the list of options,
+ and the popuponly menulist makes things like the menuactive attributes
+ work correctly on the menupopup. ContentSelectDropdown expects the
+ popuponly menulist to be its immediate parent. -->
+ <menulist popuponly="true" id="ContentSelectDropdown" hidden="true">
+ <menupopup rolluponmousewheel="true"
+ activateontab="true" position="after_start"
+ level="parent"
+#ifdef XP_WIN
+ consumeoutsideclicks="false" ignorekeys="shortcuts"
+#endif
+ />
+ </menulist>
+
+ <html:template id="printPreviewStackTemplate">
+ <stack class="previewStack" rendering="true" flex="1" previewtype="primary">
+ <vbox class="previewRendering" flex="1">
+ <h1 class="print-pending-label" data-l10n-id="printui-loading"></h1>
+ </vbox>
+ </stack>
+ </html:template>
+
+ <html:template id="invalidFormTemplate">
+ <!-- for invalid form error message -->
+ <panel id="invalid-form-popup" type="arrow" orient="vertical" noautofocus="true" level="parent">
+ <description/>
+ </panel>
+ </html:template>
+
+ <html:template id="editBookmarkPanelTemplate">
+ <panel id="editBookmarkPanel"
+ class="panel-no-padding"
+ type="arrow"
+ orient="vertical"
+ ignorekeys="true"
+ hidden="true"
+ tabspecific="true"
+ aria-labelledby="editBookmarkPanelTitle">
+ <box class="panel-header">
+ <label id="editBookmarkPanelTitle"/>
+ <toolbarbutton tabindex="0" id="editBookmarkPanelInfoButton" class="panel-info-button" oncommand="StarUI.toggleRecommendation();" >
+ <image/>
+ </toolbarbutton>
+ </box>
+ <html:div id="editBookmarkPanelInfoArea">
+ <html:div id="editBookmarkPanelRecommendation"></html:div>
+ <html:div id="editBookmarkPanelFaviconContainer">
+ <html:img id="editBookmarkPanelFavicon"/>
+ </html:div>
+ <html:div id="editBookmarkPanelImage"></html:div>
+ </html:div>
+#include ../../components/places/content/editBookmarkPanel.inc.xhtml
+ <vbox id="editBookmarkPanelBottomContent"
+ flex="1">
+ <checkbox id="editBookmarkPanel_showForNewBookmarks"
+ data-l10n-id="bookmark-panel-show-editor-checkbox"
+ oncommand="StarUI.onShowForNewBookmarksCheckboxCommand();"/>
+ </vbox>
+ <hbox id="editBookmarkPanelBottomButtons"
+ class="panel-footer"
+ data-l10n-id="bookmark-panel"
+ data-l10n-attrs="style">
+#ifndef XP_UNIX
+ <button id="editBookmarkPanelDoneButton"
+ class="editBookmarkPanelBottomButton"
+ data-l10n-id="bookmark-panel-done-button"
+ default="true"
+ oncommand="StarUI.panel.hidePopup();"/>
+ <button id="editBookmarkPanelRemoveButton"
+ class="editBookmarkPanelBottomButton"
+ oncommand="StarUI.removeBookmarkButtonCommand();"/>
+#else
+ <button id="editBookmarkPanelRemoveButton"
+ class="editBookmarkPanelBottomButton"
+ oncommand="StarUI.removeBookmarkButtonCommand();"/>
+ <button id="editBookmarkPanelDoneButton"
+ class="editBookmarkPanelBottomButton"
+ data-l10n-id="bookmark-panel-done-button"
+ default="true"
+ oncommand="StarUI.panel.hidePopup();"/>
+#endif
+ </hbox>
+ </panel>
+ </html:template>
+
+ <html:template id="UITourTooltipTemplate">
+ <!-- UI tour experience -->
+ <panel id="UITourTooltip"
+ type="arrow"
+ noautofocus="true"
+ noautohide="true"
+ align="start"
+ orient="vertical"
+ role="alert">
+ <vbox>
+ <hbox id="UITourTooltipBody">
+ <image id="UITourTooltipIcon"/>
+ <vbox flex="1">
+ <hbox id="UITourTooltipTitleContainer">
+ <label id="UITourTooltipTitle" flex="1"/>
+ <toolbarbutton id="UITourTooltipClose" class="close-icon"
+ tooltiptext="&uiTour.infoPanel.close;"/>
+ </hbox>
+ <description id="UITourTooltipDescription" flex="1"/>
+ </vbox>
+ </hbox>
+ <hbox id="UITourTooltipButtons" flex="1" align="center"/>
+ </vbox>
+ </panel>
+ </html:template>
+ <html:template id="UITourHighlightTemplate">
+ <!-- type="default" forces frames to be created so that the panel's size can be determined -->
+ <panel id="UITourHighlightContainer"
+ type="default"
+ noautofocus="true"
+ noautohide="true"
+ flip="none"
+ consumeoutsideclicks="false">
+ <box id="UITourHighlight"></box>
+ </panel>
+ </html:template>
+
+ <html:template id="dialogStackTemplate">
+ <stack class="dialogStack tab-dialog-box" hidden="true">
+ <vbox class="dialogTemplate dialogOverlay" align="center" topmost="true" hidden="true">
+ <hbox class="dialogBox">
+ <browser class="dialogFrame"
+ autoscroll="false"
+ disablehistory="true"/>
+ </hbox>
+ </vbox>
+ </stack>
+ </html:template>
+
+ <panel id="sidebarMenu-popup"
+ class="cui-widget-panel"
+ role="group"
+ type="arrow"
+ hidden="true"
+ flip="slide"
+ orient="vertical"
+ position="bottomcenter topleft">
+ <toolbarbutton id="sidebar-switcher-bookmarks"
+ type="checkbox"
+ data-l10n-id="sidebar-menu-bookmarks"
+ class="subviewbutton subviewbutton-iconic"
+ key="viewBookmarksSidebarKb"
+ oncommand="SidebarUI.show('viewBookmarksSidebar');"/>
+ <toolbarbutton id="sidebar-switcher-history"
+ type="checkbox"
+ data-l10n-id="sidebar-menu-history"
+ class="subviewbutton subviewbutton-iconic"
+ key="key_gotoHistory"
+ oncommand="SidebarUI.show('viewHistorySidebar');"/>
+ <toolbarbutton id="sidebar-switcher-tabs"
+ type="checkbox"
+ data-l10n-id="sidebar-menu-synced-tabs"
+ class="subviewbutton subviewbutton-iconic sync-ui-item"
+ oncommand="SidebarUI.show('viewTabsSidebar');"/>
+ <toolbarseparator/>
+ <!-- Extension toolbarbuttons go here. -->
+ <toolbarseparator id="sidebar-extensions-separator"/>
+ <toolbarbutton id="sidebar-reverse-position"
+ class="subviewbutton"
+ oncommand="SidebarUI.reversePosition()"/>
+ <toolbarseparator/>
+ <toolbarbutton data-l10n-id="sidebar-menu-close"
+ class="subviewbutton"
+ oncommand="SidebarUI.hide()"/>
+ </panel>
+
+ <menupopup id="toolbar-context-menu"
+ onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator')); ToolbarContextMenu.updateDownloadsAutoHide(this); ToolbarContextMenu.updateExtension(this)">
+ <menuitem oncommand="ToolbarContextMenu.openAboutAddonsForContextAction(this.parentElement)"
+ data-lazy-l10n-id="toolbar-context-menu-manage-extension"
+ contexttype="toolbaritem"
+ class="customize-context-manageExtension"/>
+ <menuitem oncommand="ToolbarContextMenu.removeExtensionForContextAction(this.parentElement)"
+ data-lazy-l10n-id="toolbar-context-menu-remove-extension"
+ contexttype="toolbaritem"
+ class="customize-context-removeExtension"/>
+ <menuitem oncommand="ToolbarContextMenu.reportExtensionForContextAction(this.parentElement, 'toolbar_context_menu')"
+ data-lazy-l10n-id="toolbar-context-menu-report-extension"
+ contexttype="toolbaritem"
+ class="customize-context-reportExtension"/>
+ <menuseparator/>
+ <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode, 'toolbar-context-menu')"
+ data-lazy-l10n-id="toolbar-context-menu-pin-to-overflow-menu"
+ contexttype="toolbaritem"
+ class="customize-context-moveToPanel"/>
+ <menuitem id="toolbar-context-autohide-downloads-button"
+ oncommand="ToolbarContextMenu.onDownloadsAutoHideChange(event);"
+ type="checkbox"
+ data-lazy-l10n-id="toolbar-context-menu-auto-hide-downloads-button"
+ contexttype="toolbaritem"/>
+ <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode, 'toolbar-context-menu')"
+ data-lazy-l10n-id="toolbar-context-menu-remove-from-toolbar"
+ contexttype="toolbaritem"
+ class="customize-context-removeFromToolbar"/>
+ <menuitem id="toolbar-context-reloadSelectedTab"
+ contexttype="tabbar"
+ oncommand="gBrowser.reloadMultiSelectedTabs();"
+ data-lazy-l10n-id="toolbar-context-menu-reload-selected-tab"/>
+ <menuitem id="toolbar-context-reloadSelectedTabs"
+ contexttype="tabbar"
+ oncommand="gBrowser.reloadMultiSelectedTabs();"
+ data-lazy-l10n-id="toolbar-context-menu-reload-selected-tabs"/>
+ <menuitem id="toolbar-context-bookmarkSelectedTab"
+ contexttype="tabbar"
+ oncommand="PlacesUIUtils.showBookmarkPagesDialog(PlacesCommandHook.uniqueSelectedPages);"
+ data-lazy-l10n-id="toolbar-context-menu-bookmark-selected-tab"/>
+ <menuitem id="toolbar-context-bookmarkSelectedTabs"
+ contexttype="tabbar"
+ oncommand="PlacesUIUtils.showBookmarkPagesDialog(PlacesCommandHook.uniqueSelectedPages);"
+ data-lazy-l10n-id="toolbar-context-menu-bookmark-selected-tabs"/>
+ <menuitem id="toolbar-context-selectAllTabs"
+ contexttype="tabbar"
+ oncommand="gBrowser.selectAllTabs();"
+ data-lazy-l10n-id="toolbar-context-menu-select-all-tabs"/>
+ <menuitem id="toolbar-context-undoCloseTab"
+ contexttype="tabbar"
+ data-lazy-l10n-id="toolbar-context-menu-undo-close-tabs"
+ observes="History:UndoCloseTab"/>
+ <menuseparator/>
+ <menuseparator id="viewToolbarsMenuSeparator"/>
+ <!-- XXXgijs: we're using oncommand handler here to avoid the event being
+ redirected to the command element, thus preventing
+ listeners on the menupopup or further up the tree from
+ seeing the command event pass by. The observes attribute is
+ here so that the menuitem is still disabled and re-enabled
+ correctly. -->
+ <menuitem oncommand="gCustomizeMode.enter()"
+ observes="cmd_CustomizeToolbars"
+ class="viewCustomizeToolbar"
+ data-lazy-l10n-id="toolbar-context-menu-view-customize-toolbar"/>
+ </menupopup>
+
+ <menupopup id="blockedPopupOptions"
+ onpopupshowing="gPopupBlockerObserver.fillPopupList(event);"
+ onpopuphiding="gPopupBlockerObserver.onPopupHiding(event);">
+ <menuitem id="blockedPopupAllowSite"
+ accesskey="&allowPopups.accesskey;"
+ oncommand="gPopupBlockerObserver.toggleAllowPopupsForSite(event);"/>
+ <menuitem
+#ifdef XP_WIN
+ label="&editPopupSettings.label;"
+#else
+ label="&editPopupSettingsUnix.label;"
+#endif
+ accesskey="&editPopupSettings.accesskey;"
+ oncommand="gPopupBlockerObserver.editPopupSettings();"/>
+ <menuitem id="blockedPopupDontShowMessage"
+ accesskey="&dontShowMessage.accesskey;"
+ type="checkbox"
+ oncommand="gPopupBlockerObserver.dontShowMessage();"/>
+ <menuseparator id="blockedPopupsSeparator"/>
+ </menupopup>
+
+ <menupopup id="autohide-context"
+ onpopupshowing="FullScreen.getAutohide(this.firstChild);">
+ <menuitem type="checkbox" data-l10n-id="full-screen-autohide"
+ oncommand="FullScreen.setAutohide();"/>
+ <menuseparator/>
+ <menuitem data-l10n-id="full-screen-exit"
+ oncommand="BrowserFullScreen();"/>
+ </menupopup>
+
+ <menupopup id="contentAreaContextMenu" pagemenu="#page-menu-separator"
+ onpopupshowing="if (event.target != this)
+ return true;
+ gContextMenu = new nsContextMenu(this, event.shiftKey);
+ if (gContextMenu.shouldDisplay)
+ updateEditUIVisibility();
+ return gContextMenu.shouldDisplay;"
+ onpopuphiding="if (event.target != this)
+ return;
+ gContextMenu.hiding();
+ gContextMenu = null;
+ updateEditUIVisibility();">
+#include browser-context.inc
+ </menupopup>
+
+ <menupopup id="pictureInPictureToggleContextMenu">
+ <menuitem label="&pictureInPictureHideToggle.label;"
+ accesskey="&pictureInPictureHideToggle.accesskey;"
+ oncommand="PictureInPicture.hideToggle();" />
+ </menupopup>
+
+#include ../../components/places/content/placesContextMenu.inc.xhtml
+
+ <panel id="ctrlTab-panel" hidden="true" norestorefocus="true" level="top">
+ <hbox id="ctrlTab-previews"/>
+ <hbox id="ctrlTab-showAll-container" pack="center"/>
+ </panel>
+
+ <html:template id="pageActionPanelTemplate">
+ <panel id="pageActionPanel"
+ class="cui-widget-panel panel-no-padding"
+ role="group"
+ type="arrow"
+ hidden="true"
+ flip="slide"
+ position="bottomcenter topright"
+ tabspecific="true"
+ noautofocus="true">
+ <panelmultiview id="pageActionPanelMultiView"
+ mainViewId="pageActionPanelMainView"
+ viewCacheId="appMenu-viewCache">
+ <panelview id="pageActionPanelMainView"
+ context="pageActionContextMenu"
+ class="PanelUI-subView">
+ <vbox class="panel-subview-body"/>
+ </panelview>
+ </panelmultiview>
+ </panel>
+ </html:template>
+
+ <html:template id="confirmation-hint-wrapper">
+ <panel id="confirmation-hint"
+ role="alert"
+ type="arrow"
+ flip="slide"
+ position="bottomcenter topright"
+ tabspecific="true"
+ noautofocus="true">
+ <hbox id="confirmation-hint-checkmark-animation-container">
+ <image id="confirmation-hint-checkmark-image"/>
+ </hbox>
+ <vbox id="confirmation-hint-message-container">
+ <label id="confirmation-hint-message"/>
+ <label id="confirmation-hint-description"/>
+ </vbox>
+ </panel>
+ </html:template>
+
+ <menupopup id="pageActionContextMenu"
+ onpopupshowing="BrowserPageActions.onContextMenuShowing(event, this);">
+ <menuitem class="pageActionContextMenuItem builtInUnpinned"
+ oncommand="BrowserPageActions.togglePinningForContextAction();"
+ data-l10n-id="page-action-add-to-urlbar"/>
+ <menuitem class="pageActionContextMenuItem builtInPinned"
+ oncommand="BrowserPageActions.togglePinningForContextAction();"
+ data-l10n-id="page-action-remove-from-urlbar"/>
+ <menuitem class="pageActionContextMenuItem extensionUnpinned"
+ oncommand="BrowserPageActions.togglePinningForContextAction();"
+ data-l10n-id="page-action-add-to-urlbar"/>
+ <menuitem class="pageActionContextMenuItem extensionPinned"
+ oncommand="BrowserPageActions.togglePinningForContextAction();"
+ data-l10n-id="page-action-remove-from-urlbar"/>
+ <menuseparator class="pageActionContextMenuItem extensionPinned extensionUnpinned"/>
+ <menuitem class="pageActionContextMenuItem extensionPinned extensionUnpinned"
+ oncommand="BrowserPageActions.openAboutAddonsForContextAction();"
+ data-l10n-id="page-action-manage-extension"/>
+ <menuitem class="pageActionContextMenuItem extensionPinned extensionUnpinned removeExtensionItem"
+ oncommand="BrowserPageActions.removeExtensionForContextAction();"
+ data-l10n-id="page-action-remove-extension"/>
+ </menupopup>
+
+#include ../../components/places/content/bookmarksHistoryTooltip.inc.xhtml
+
+ <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/>
+
+ <tooltip id="back-button-tooltip">
+ <description class="tooltip-label" data-l10n-id="navbar-tooltip-back"/>
+ <description class="tooltip-label" data-l10n-id="navbar-tooltip-instruction"/>
+ </tooltip>
+
+ <tooltip id="forward-button-tooltip">
+ <description class="tooltip-label" data-l10n-id="navbar-tooltip-forward"/>
+ <description class="tooltip-label" data-l10n-id="navbar-tooltip-instruction"/>
+ </tooltip>
+
+#include popup-notifications.inc
+
+#include ../../components/customizableui/content/panelUI.inc.xhtml
+#include ../../components/controlcenter/content/identityPanel.inc.xhtml
+#include ../../components/controlcenter/content/protectionsPanel.inc.xhtml
+#include ../../components/downloads/content/downloadsPanel.inc.xhtml
+#include ../../../devtools/startup/enableDevToolsPopup.inc.xhtml
+#include browser-allTabsMenu.inc.xhtml
+
+ <hbox id="downloads-animation-container">
+ <vbox id="downloads-notification-anchor" hidden="true">
+ <vbox id="downloads-indicator-notification"/>
+ </vbox>
+ </hbox>
+
+ <tooltip id="dynamic-shortcut-tooltip"
+ onpopupshowing="UpdateDynamicShortcutTooltipText(this);"/>
+
+ <menupopup id="SyncedTabsSidebarContext">
+ <menuitem data-lazy-l10n-id="synced-tabs-context-open"
+ id="syncedTabsOpenSelected" where="current"/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-open-in-new-tab"
+ id="syncedTabsOpenSelectedInTab" where="tab"/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-open-in-new-window"
+ id="syncedTabsOpenSelectedInWindow" where="window"/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-open-in-new-private-window"
+ id="syncedTabsOpenSelectedInPrivateWindow" where="window" private="true"/>
+ <menuseparator/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-bookmark-single-tab"
+ id="syncedTabsBookmarkSelected"/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-copy"
+ id="syncedTabsCopySelected"/>
+ <menuseparator/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-open-all-in-tabs"
+ id="syncedTabsOpenAllInTabs"/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-manage-devices"
+ id="syncedTabsManageDevices"
+ oncommand="gSync.openDevicesManagementPage('syncedtabs-sidebar');"/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-sync-now"
+ id="syncedTabsRefresh"/>
+ </menupopup>
+ <menupopup id="SyncedTabsSidebarTabsFilterContext"
+ class="textbox-contextmenu">
+ <menuitem data-l10n-id="text-action-undo"
+ cmd="cmd_undo"/>
+ <menuseparator/>
+ <menuitem data-l10n-id="text-action-cut"
+ cmd="cmd_cut"/>
+ <menuitem data-l10n-id="text-action-copy"
+ cmd="cmd_copy"/>
+ <menuitem data-l10n-id="text-action-paste"
+ cmd="cmd_paste"/>
+ <menuitem data-l10n-id="text-action-delete"
+ cmd="cmd_delete"/>
+ <menuseparator/>
+ <menuitem data-l10n-id="text-action-select-all"
+ cmd="cmd_selectAll"/>
+ <menuseparator/>
+ <menuitem data-lazy-l10n-id="synced-tabs-context-sync-now"
+ id="syncedTabsRefreshFilter"/>
+ </menupopup>
+
+ <hbox id="statuspanel" inactive="true">
+ <hbox id="statuspanel-inner">
+ <label id="statuspanel-label"
+ role="status"
+ aria-live="off"
+ flex="1"
+ crop="end"/>
+ </hbox>
+ </hbox>
+
+ <html:template id="sharing-tabs-warning-panel-template">
+ <panel id="sharing-tabs-warning-panel"
+ role="alert"
+ flip="slide"
+ type="arrow"
+ orient="vertical"
+ ignorekeys="true"
+ consumeoutsideclicks="never"
+ norolluponanchor="true"
+ onpopupshown="gSharedTabWarning.sharedTabWarningShown();">
+ <hbox type="window" align="start">
+ <image class="screen-icon popup-notification-icon"></image>
+ <vbox flex="1" pack="start">
+ <label>
+ <html:span id="sharing-warning-window-panel-header"
+ role="heading"
+ aria-level="1"
+ data-l10n-id="sharing-warning-window"/>
+ <html:span id="sharing-warning-screen-panel-header"
+ role="heading"
+ aria-level="1"
+ data-l10n-id="sharing-warning-screen"/>
+ </label>
+ <hbox align="center">
+ <button id="sharing-warning-proceed-to-tab" oncommand="gSharedTabWarning.allowSharedTabSwitch();" flex="1" data-l10n-id="sharing-warning-proceed-to-tab"/>
+ </hbox>
+ <hbox pack="start">
+ <checkbox id="sharing-warning-disable-for-session" data-l10n-id="sharing-warning-disable-for-session"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ </panel>
+ </html:template>
+ </popupset>
+
+ <html:template id="appMenu-viewCache">
+ <panelview id="appMenu-mainView" class="PanelUI-subView"
+ descriptionheightworkaround="true">
+ <vbox class="panel-subview-body">
+ <vbox id="appMenu-addon-banners"/>
+ <toolbarbutton id="appMenu-update-banner" class="panel-banner-item"
+ data-l10n-id="appmenuitem-update-banner"
+ data-l10n-attrs="label-update-downloading"
+ label-update-available="&updateAvailable.panelUI.label;"
+ label-update-manual="&updateManual.panelUI.label;"
+ label-update-unsupported="&updateUnsupported.panelUI.label;"
+ label-update-restart="&updateRestart.panelUI.label2;"
+ oncommand="PanelUI._onBannerItemSelected(event)"
+ wrap="true"
+ hidden="true"/>
+ <toolbaritem id="appMenu-fxa-status"
+ class="sync-ui-item"
+ defaultlabel="&fxa.menu.signin.label;"
+ flex="1">
+ <image id="appMenu-fxa-avatar"/>
+ <toolbarbutton id="appMenu-fxa-label"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&fxa.menu.signin.label;"
+ closemenu="none"
+ oncommand="gSync.toggleAccountPanel('PanelUI-fxa', this, event)"/>
+ </toolbaritem>
+ <toolbarseparator class="sync-ui-item"/>
+ <toolbaritem>
+ <toolbarbutton id="appMenu-protection-report-button"
+ class="subviewbutton subviewbutton-iconic"
+ oncommand="gProtectionsHandler.openProtections(); gProtectionsHandler.recordClick('open_full_report', null, 'app_menu');">
+ <image id="appMenu-protection-report-icon" class="toolbarbutton-icon"/>
+ <label id="appMenu-protection-report-text"
+ class="toolbarbutton-text"
+ data-l10n-id="appmenuitem-protection-dashboard-title">
+ </label>
+ </toolbarbutton>
+ </toolbaritem>
+ <toolbarseparator id="appMenu-tp-separator"/>
+ <toolbarbutton id="appMenu-new-window-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&newNavigatorCmd.label;"
+ key="key_newNavigator"
+ command="cmd_newNavigator"/>
+ <toolbarbutton id="appMenu-private-window-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&newPrivateWindow.label;"
+ key="key_privatebrowsing"
+ command="Tools:PrivateBrowsing"/>
+#ifdef NIGHTLY_BUILD
+ <toolbarbutton id="appMenu-fission-window-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="New Fission Window"
+ accesskey="s"
+ command="Tools:FissionWindow"/>
+ <toolbarbutton id="appMenu-non-fission-window-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="New Non-Fission Window"
+ accesskey="s"
+ command="Tools:NonFissionWindow"/>
+#endif
+ <toolbarbutton id="appMenuRestoreLastSession"
+ label="&appMenuHistory.restoreSession.label;"
+ class="subviewbutton subviewbutton-iconic"
+ command="Browser:RestoreLastSession"/>
+ <toolbarseparator/>
+ <toolbaritem id="appMenu-zoom-controls" class="toolbaritem-combined-buttons" closemenu="none">
+ <!-- Use a spacer, because panel sizing code gets confused when using CSS methods. -->
+ <spacer class="before-label"/>
+ <label value="&fullZoom.label;"/>
+ <!-- This spacer keeps the scrollbar from overlapping the view. -->
+ <spacer class="after-label"/>
+ <toolbarbutton id="appMenu-zoomReduce-button"
+ class="subviewbutton subviewbutton-iconic"
+ command="cmd_fullZoomReduce"
+ data-l10n-id="appmenuitem-zoom-reduce"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <toolbarbutton id="appMenu-zoomReset-button"
+ class="subviewbutton"
+ command="cmd_fullZoomReset"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <toolbarbutton id="appMenu-zoomEnlarge-button"
+ class="subviewbutton subviewbutton-iconic"
+ command="cmd_fullZoomEnlarge"
+ data-l10n-id="appmenuitem-zoom-enlarge"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <toolbarseparator orient="vertical"/>
+ <toolbarbutton id="appMenu-fullscreen-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&fullScreenCmd.label;"
+ observes="View:FullScreen"
+ type="checkbox"
+ closemenu="auto"
+ onclick="if (event.button == 0) this.closest('panel').hidePopup();"
+ tooltip="dynamic-shortcut-tooltip"/>
+ </toolbaritem>
+ <toolbarseparator/>
+ <toolbaritem id="appMenu-edit-controls" class="toolbaritem-combined-buttons" closemenu="none">
+ <!-- Use a spacer, because panel sizing code gets confused when using CSS methods. -->
+ <spacer class="before-label"/>
+ <label value="&editMenu.label;"/>
+ <!-- This spacer keeps the scrollbar from overlapping the view. -->
+ <spacer class="after-label"/>
+ <toolbarbutton id="appMenu-cut-button"
+ class="subviewbutton subviewbutton-iconic"
+ command="cmd_cut"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <toolbarbutton id="appMenu-copy-button"
+ class="subviewbutton subviewbutton-iconic"
+ command="cmd_copy"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <toolbarbutton id="appMenu-paste-button"
+ class="subviewbutton subviewbutton-iconic"
+ command="cmd_paste"
+ tooltip="dynamic-shortcut-tooltip"/>
+ </toolbaritem>
+ <toolbarseparator/>
+ <toolbarbutton id="appMenu-library-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&places.library.title;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-libraryView', this)"/>
+ <toolbarbutton id="appMenu-logins-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&logins.label;"
+ oncommand="LoginHelper.openPasswordManager(window, { entryPoint: 'mainmenu' })"
+ />
+ <toolbarbutton id="appMenu-addons-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&addons.label;"
+ key="key_openAddons"
+ command="Tools:Addons"
+ />
+ <toolbarbutton id="appMenu-preferences-button"
+ class="subviewbutton subviewbutton-iconic"
+#ifdef XP_WIN
+ label="&preferencesCmd2.label;"
+#else
+ label="&preferencesCmdUnix.label;"
+#ifdef XP_MACOSX
+ key="key_preferencesCmdMac"
+#endif
+#endif
+ oncommand="openPreferences()"
+ />
+ <toolbarbutton id="appMenu-customize-button"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenuitem-customize-mode"
+ command="cmd_CustomizeToolbars"
+ />
+ <toolbarseparator/>
+ <toolbarbutton id="appMenu-open-file-button"
+ class="subviewbutton"
+ label="&openFileCmd.label;"
+ key="openFileKb"
+ command="Browser:OpenFile"
+ />
+ <toolbarbutton id="appMenu-save-file-button"
+ class="subviewbutton"
+ data-l10n-id="appmenuitem-save-page"
+ key="key_savePage"
+ command="Browser:SavePage"
+ />
+ <toolbarbutton id="appMenu-print-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&printCmd.label;"
+ key="printKb"
+#ifdef XP_MACOSX
+ command="cmd_print"
+#else
+ command="cmd_printPreview"
+#endif
+ />
+ <toolbarseparator/>
+ <toolbarbutton id="appMenu-find-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&findOnCmd.label;"
+ key="key_find"
+ command="cmd_find"/>
+ <toolbarbutton id="appMenu-more-button"
+ class="subviewbutton subviewbutton-nav"
+ label="&moreMenu.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-moreView', this)"/>
+ <toolbarbutton id="appMenu-developer-button"
+ class="subviewbutton subviewbutton-nav"
+ label="&webDeveloperMenu.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('PanelUI-developer', this)"/>
+ <toolbarbutton id="appMenu-whatsnew-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ hidden="true"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('PanelUI-whatsNew', this)"/>
+ <toolbarbutton id="appMenu-help-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&appMenuHelp.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('PanelUI-helpView', this)"/>
+#ifndef XP_MACOSX
+ <toolbarseparator/>
+ <toolbarbutton id="appMenu-quit-button"
+ class="subviewbutton subviewbutton-iconic"
+#ifdef XP_WIN
+ data-l10n-id="menu-quit-button-win"
+#else
+ data-l10n-id="menu-quit-button"
+#endif
+ key="key_quitApplication"
+ command="cmd_quitApplication"/>
+#endif
+ </vbox>
+ </panelview>
+
+ <!-- This is a placeholder app menu which should be replaced with the "real"
+ Proton app menu before the Proton pref starts getting enabled. -->
+ <panelview id="appMenu-protonMainView" class="PanelUI-subView"
+ descriptionheightworkaround="true">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appMenu-new-window-button2"
+ class="subviewbutton subviewbutton-iconic"
+ label="&newNavigatorCmd.label;"
+ key="key_newNavigator"
+ command="cmd_newNavigator"/>
+ <toolbarbutton id="appMenu-library-button2"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&places.library.title;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-libraryView', this)"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appMenu-quit-button2"
+ class="subviewbutton subviewbutton-iconic"
+#ifdef XP_WIN
+ data-l10n-id="menu-quit-button-win"
+#else
+ data-l10n-id="menu-quit-button"
+#endif
+ key="key_quitApplication"
+ command="cmd_quitApplication"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-history" flex="1">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appMenuViewHistorySidebar"
+ label="&appMenuHistory.viewSidebar.label;"
+ label-checked="&appMenuHistory.hideSidebar.label;"
+ label-unchecked="&appMenuHistory.viewSidebar.label;"
+ type="checkbox"
+ class="subviewbutton subviewbutton-iconic"
+ key="key_gotoHistory"
+ oncommand="SidebarUI.toggle('viewHistorySidebar');">
+ <observes element="sidebar-box" attribute="positionend"/>
+ </toolbarbutton>
+ <toolbarbutton id="appMenuClearRecentHistory"
+ label="&appMenuHistory.clearRecent.label;"
+ class="subviewbutton subviewbutton-iconic"
+ command="Tools:Sanitize"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appMenuRecentlyClosedTabs"
+ label="&historyUndoMenu.label;"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-library-recentlyClosedTabs', this)"/>
+ <toolbarbutton id="appMenuRecentlyClosedWindows"
+ label="&historyUndoWindowMenu.label;"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-library-recentlyClosedWindows', this)"/>
+ <toolbarseparator/>
+ <label value="&appMenuHistory.recentHistory.label;"
+ class="subview-subheader"/>
+ <toolbaritem id="appMenu_historyMenu"
+ orient="vertical"
+ smoothscroll="false"
+ flatList="true"
+ tooltip="bhTooltip">
+ <!-- history menu items will go here -->
+ </toolbaritem>
+ </vbox>
+ <toolbarbutton id="PanelUI-historyMore"
+ class="panel-subview-footer subviewbutton"
+ label="&appMenuHistory.showAll.label;"
+ oncommand="PlacesCommandHook.showPlacesOrganizer('History'); CustomizableUI.hidePanelForNode(this);"/>
+ </panelview>
+
+ <panelview id="appMenu-library-recentlyClosedTabs"/>
+ <panelview id="appMenu-library-recentlyClosedWindows"/>
+
+ <panelview id="PanelUI-containers" flex="1">
+ <vbox id="PanelUI-containersItems"/>
+ </panelview>
+
+ <panelview id="PanelUI-helpView" flex="1" class="PanelUI-subView">
+ <vbox id="PanelUI-helpItems" class="panel-subview-body"/>
+ </panelview>
+
+ <panelview id="PanelUI-developer" flex="1">
+ <vbox id="PanelUI-developerItems" class="panel-subview-body"/>
+ </panelview>
+
+ <panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="panelMenuBookmarkThisPage"
+ class="subviewbutton subviewbutton-iconic"
+ command="Browser:AddBookmarkAs"
+ data-l10n-id="menu-bookmark-this-page"
+ onclick="PanelUI.hide();"/>
+ <toolbarbutton id="panelMenu_bookmarkingTools"
+ data-l10n-id="bookmarks-tools"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="BookmarkingUI.showBookmarkingTools(this);"/>
+ <toolbarbutton id="panelMenu_searchBookmarks"
+ data-l10n-id="bookmarks-search"
+ class="subviewbutton subviewbutton-iconic"
+ oncommand="PlacesCommandHook.searchBookmarks(); PanelUI.hide();"/>
+ <toolbarseparator/>
+ <label id="panelMenu_recentBookmarks"
+ data-l10n-id="bookmarks-recent-bookmarks"
+ class="subview-subheader"/>
+ <toolbaritem id="panelMenu_bookmarksMenu"
+ orient="vertical"
+ smoothscroll="false"
+ flatList="true"
+ tooltip="bhTooltip">
+ <!-- bookmarks menu items will go here -->
+ </toolbaritem>
+ </vbox>
+ <toolbarbutton id="panelMenu_showAllBookmarks"
+ data-l10n-id="bookmarks-show-all-bookmarks"
+ class="subviewbutton panel-subview-footer"
+ command="Browser:ShowAllBookmarks"
+ onclick="PanelUI.hide();"/>
+ </panelview>
+
+ <panelview id="PanelUI-profiler" flex="1" descriptionheightworkaround="true">
+ <vbox id="PanelUI-profiler-container">
+ <vbox id="PanelUI-profiler-header" animationready="false">
+ <hbox id="PanelUI-profiler-header-bar">
+ <label flex="1" data-l10n-id="profiler-popup-title" />
+ <vbox class="PanelUI-profiler-toolbarbutton-container">
+ <toolbarbutton id="PanelUI-profiler-info-button"
+ class="panel-info-button"
+ data-l10n-id="profiler-popup-reveal-description-button">
+ <image/>
+ </toolbarbutton>
+ </vbox>
+ </hbox>
+ <hbox id="PanelUI-profiler-info">
+ <vbox>
+ <hbox id="PanelUI-profiler-info-graphic" flex="1">
+ <spacer flex="1" />
+ <vbox>
+ <spacer flex="1" />
+ <image class="PanelUI-profiler-info-icon" />
+ </vbox>
+ </hbox>
+ <label data-l10n-id="profiler-popup-description-title" />
+ <description data-l10n-id="profiler-popup-description" />
+ <hbox>
+ <button id="PanelUI-profiler-learn-more"
+ tabindex="-1"
+ data-l10n-id="profiler-popup-learn-more" />
+ <space flex="1" />
+ </hbox>
+ </vbox>
+ </hbox>
+ </vbox>
+ <vbox id="PanelUI-profiler-content">
+ <vbox id="PanelUI-profiler-content-settings">
+ <label class="PanelUI-profiler-content-label"
+ data-l10n-id="profiler-popup-settings" />
+ <menulist id="PanelUI-profiler-presets"
+ flex="1"
+ value="custom">
+ <menupopup id="PanelUI-profiler-presets-menupopup" presetsbuilt="false">
+ <!-- The rest of the values get dynamically inserted. The "presetsbuilt"
+ attribute will get updated to "true" once the presets have been
+ built. -->
+ <menuitem id="PanelUI-profiler-presets-custom"
+ data-l10n-id="profiler-popup-presets-custom"
+ value="custom"/>
+ </menupopup>
+ </menulist>
+ <!-- The following description gets inserted dynamically. -->
+ <description id="PanelUI-profiler-content-description" />
+ <hbox id="PanelUI-profiler-content-custom">
+ <button id="PanelUI-profiler-content-custom-button"
+ data-l10n-id="profiler-popup-edit-settings">
+ </button>
+ </hbox>
+ </vbox>
+ <hbox id="PanelUI-profiler-content-recording">
+ <spacer flex="1" />
+ <image class="PanelUI-profiler-recording-icon" />
+ <label class="PanelUI-profiler-recording-label" data-l10n-id="profiler-popup-recording-screen" />
+ <spacer flex="1" />
+ </hbox>
+ <description id="PanelUI-profiler-locked"
+ data-l10n-id="profiler-popup-disabled" />
+ <hbox id="PanelUI-profiler-inactive" class="PanelUI-profiler-buttons">
+ <spacer flex="1" />
+ <vbox>
+ <button data-l10n-id="profiler-popup-start-recording-button"
+ id="PanelUI-profiler-startRecording"
+ class="PanelUI-profiler-button PanelUI-profiler-button-primary" />
+ <label class="PanelUI-profiler-shortcut"
+ data-l10n-id="profiler-popup-start-shortcut" />
+ </vbox>
+ <spacer flex="1" />
+ </hbox>
+ <hbox id="PanelUI-profiler-active" class="PanelUI-profiler-buttons">
+ <vbox flex="1">
+ <button data-l10n-id="profiler-popup-discard-button"
+ class="PanelUI-profiler-button"
+ id="PanelUI-profiler-stopAndDiscard" />
+ <label class="PanelUI-profiler-shortcut"
+ data-l10n-id="profiler-popup-start-shortcut" />
+ </vbox>
+ <vbox flex="1">
+ <button data-l10n-id="profiler-popup-capture-button"
+ class="PanelUI-profiler-button PanelUI-profiler-button-primary"
+ id="PanelUI-profiler-stopAndCapture" />
+ <label data-l10n-id="profiler-popup-capture-shortcut"
+ class="PanelUI-profiler-shortcut" />
+ </vbox>
+ </hbox>
+ </vbox>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-characterEncodingView" flex="1">
+ <vbox class="panel-subview-body">
+ <vbox id="PanelUI-characterEncodingView-pinned"
+ class="PanelUI-characterEncodingView-list"/>
+ <toolbarseparator/>
+ <vbox id="PanelUI-characterEncodingView-charsets"
+ class="PanelUI-characterEncodingView-list"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-panicView" flex="1"
+ descriptionheightworkaround="true">
+ <vbox class="panel-subview-body">
+ <hbox id="PanelUI-panic-timeframe">
+ <image id="PanelUI-panic-timeframe-icon" alt=""/>
+ <vbox flex="1">
+ <description data-l10n-id="panic-main-timeframe-desc" id="PanelUI-panic-mainDesc"></description>
+ <radiogroup id="PanelUI-panic-timeSpan" aria-labelledby="PanelUI-panic-mainDesc" closemenu="none">
+ <radio id="PanelUI-panic-5min" data-l10n-id="panic-button-5min" selected="true"
+ value="5" class="subviewradio"/>
+ <radio id="PanelUI-panic-2hr" data-l10n-id="panic-button-2hr"
+ value="2" class="subviewradio"/>
+ <radio id="PanelUI-panic-day" data-l10n-id="panic-button-day"
+ value="6" class="subviewradio"/>
+ </radiogroup>
+ </vbox>
+ </hbox>
+ <vbox id="PanelUI-panic-explanations">
+ <label id="PanelUI-panic-actionlist-main-label" data-l10n-id="panic-button-action-desc"></label>
+
+ <label id="PanelUI-panic-actionlist-windows" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-tabs-and-windows"></label>
+ <label id="PanelUI-panic-actionlist-cookies" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-cookies"></label>
+ <label id="PanelUI-panic-actionlist-history" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-history"></label>
+ <label id="PanelUI-panic-actionlist-newwindow" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-open-new-window"></label>
+
+ <label id="PanelUI-panic-warning" data-l10n-id="panic-button-undo-warning"></label>
+ </vbox>
+ <button id="PanelUI-panic-view-button"
+ data-l10n-id="panic-button-forget-button"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="appMenu-moreView" title="&moreMenu.label;" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appMenu-taskmanager-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&taskManagerCmd.label;"
+ oncommand="switchToTabHavingURI('about:performance', true)"/>
+ <toolbarbutton id="appMenu-characterencoding-button"
+ class="subviewbutton subviewbutton-nav"
+ label="&charsetMenu2.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('PanelUI-characterEncodingView', this)"/>
+ <toolbarbutton id="appMenu-workoffline-button"
+ class="subviewbutton"
+ data-l10n-id="more-menu-go-offline"
+ type="checkbox"
+ command="cmd_toggleOfflineStatus"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-remotetabs" flex="1" class="PanelUI-subView"
+ descriptionheightworkaround="true">
+ <vbox class="panel-subview-body">
+ <!-- this widget has 3 boxes in the body, but only 1 is ever visible -->
+ <!-- When Sync is ready to sync -->
+ <vbox id="PanelUI-remotetabs-main" hidden="true">
+ <vbox id="PanelUI-remotetabs-buttons">
+ <toolbarbutton id="PanelUI-remotetabs-view-sidebar"
+ class="subviewbutton subviewbutton-iconic"
+ label="&appMenuRemoteTabs.sidebar.label;"
+ label-checked="&appMenuRemoteTabs.hidesidebar.label;"
+ label-unchecked="&appMenuRemoteTabs.sidebar.label;"
+ oncommand="SidebarUI.toggle('viewTabsSidebar', this);"/>
+ <toolbarbutton id="PanelUI-remotetabs-view-managedevices"
+ class="subviewbutton subviewbutton-iconic"
+ label="&appMenuRemoteTabs.managedevices.label;"
+ oncommand="gSync.openDevicesManagementPage('syncedtabs-menupanel');">
+ <observes element="sidebar-box" attribute="positionend"/>
+ </toolbarbutton>
+ <toolbarbutton id="PanelUI-remotetabs-syncnow"
+ data-l10n-id="fxa-toolbar-sync-now"
+ syncinglabel="fxa-toolbar-sync-syncing-tabs"
+ class="syncNowBtn subviewbutton subviewbutton-iconic"
+ oncommand="gSync.doSync();"
+ onmouseover="gSync.refreshSyncButtonsTooltip();"
+ closemenu="none"/>
+ <toolbarseparator id="PanelUI-remotetabs-separator"/>
+ </vbox>
+ <deck id="PanelUI-remotetabs-deck">
+ <!-- Sync is ready to Sync and the "tabs" engine is enabled -->
+ <vbox id="PanelUI-remotetabs-tabspane">
+ <vbox id="PanelUI-remotetabs-tabslist"
+ showAllLabel="&appMenuRemoteTabs.showAll.label;"
+ showAllTooltipText="&appMenuRemoteTabs.showAll.tooltip;"
+ showMoreLabel="&appMenuRemoteTabs.showMore.label;"
+ showMoreTooltipText="&appMenuRemoteTabs.showMore.tooltip;"
+ notabsforclientlabel="&appMenuRemoteTabs.notabs.label;"
+ />
+ </vbox>
+ <!-- Sync is ready to Sync but the "tabs" engine isn't enabled-->
+ <hbox id="PanelUI-remotetabs-tabsdisabledpane" pack="center" flex="1">
+ <vbox class="PanelUI-remotetabs-instruction-box" align="center">
+ <hbox pack="center">
+ <image class="fxaSyncIllustrationIssue"/>
+ </hbox>
+ <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label>
+ <hbox pack="center">
+ <toolbarbutton class="PanelUI-remotetabs-button"
+ id="PanelUI-remotetabs-tabsdisabledpane-button"
+ label="&appMenuRemoteTabs.opensyncprefs.label;"
+ oncommand="gSync.openPrefs('synced-tabs');"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ <!-- Sync is ready to Sync but we are still fetching the tabs to show -->
+ <vbox id="PanelUI-remotetabs-fetching">
+ <!-- Show intentionally blank panel, see bug 1239845 -->
+ </vbox>
+ <!-- Sync has only 1 (ie, this) device connected -->
+ <hbox id="PanelUI-remotetabs-nodevicespane" pack="center" flex="1">
+ <vbox class="PanelUI-remotetabs-instruction-box" align="center">
+ <hbox pack="center">
+ <image class="fxaSyncIllustrationIssue"/>
+ </hbox>
+ <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.noclients.subtitle;</label>
+ <toolbarbutton id="PanelUI-remotetabs-connect-device-button"
+ class="PanelUI-remotetabs-button"
+ label="&appMenuRemoteTabs.connectdevice.label;"
+ oncommand="gSync.openConnectAnotherDevice('synced-tabs');"/>
+ </vbox>
+ </hbox>
+ </deck>
+ </vbox>
+ <!-- a box to ensure contained boxes are centered horizonally -->
+ <hbox pack="center" flex="1">
+ <!-- When Sync is not configured -->
+ <vbox id="PanelUI-remotetabs-setupsync"
+ flex="1"
+ align="center"
+ class="PanelUI-remotetabs-instruction-box"
+ hidden="true">
+ <image class="fxaSyncIllustration"/>
+ <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.welcome.label;</label>
+ <toolbarbutton class="PanelUI-remotetabs-button"
+ id="PanelUI-remotetabs-setupsync-button"
+ label="&appMenuRemoteTabs.signintosync.label;"
+ oncommand="gSync.openPrefs('synced-tabs');"/>
+ </vbox>
+ <!-- When Sync is not enabled -->
+ <vbox id="PanelUI-remotetabs-syncdisabled"
+ flex="1"
+ align="center"
+ class="PanelUI-remotetabs-instruction-box"
+ hidden="true">
+ <image class="fxaSyncIllustration"/>
+ <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.welcome.label;</label>
+ <toolbarbutton class="PanelUI-remotetabs-button"
+ id="PanelUI-remotetabs-syncdisabled-button"
+ label="&appMenuRemoteTabs.turnonsync.label;"
+ oncommand="gSync.openPrefs('synced-tabs');"/>
+ </vbox>
+ <!-- When Sync needs re-authentication -->
+ <vbox id="PanelUI-remotetabs-reauthsync"
+ flex="1"
+ align="center"
+ class="PanelUI-remotetabs-instruction-box"
+ hidden="true">
+ <image class="fxaSyncIllustrationIssue"/>
+ <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.welcome.label;</label>
+ <toolbarbutton class="PanelUI-remotetabs-button"
+ id="PanelUI-remotetabs-reauthsync-button"
+ label="&appMenuRemoteTabs.signintosync.label;"
+ oncommand="gSync.openPrefs('synced-tabs');"/>
+ </vbox>
+ <!-- When Sync needs verification -->
+ <vbox id="PanelUI-remotetabs-unverified"
+ flex="1"
+ align="center"
+ class="PanelUI-remotetabs-instruction-box"
+ hidden="true">
+ <image class="fxaSyncIllustrationIssue"/>
+ <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.unverified.label;</label>
+ <toolbarbutton class="PanelUI-remotetabs-button"
+ id="PanelUI-remotetabs-unverified-button"
+ label="&appMenuRemoteTabs.opensyncprefs.label;"
+ oncommand="gSync.openPrefs('synced-tabs');"/>
+ </vbox>
+ </hbox>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-fxa" title="&fxa.menu.account.label;" class="PanelUI-subView" descriptionheightworkaround="true">
+ <vbox id="PanelUI-fxa-menu" class="panel-subview-body">
+ <toolbarbutton id="fxa-manage-account-button"
+ align="center"
+ class="fxa-menu-header subviewbutton"
+ oncommand="gSync.clickFxAMenuHeaderButton(this);">
+ <image role="presentation" id="fxa-menu-avatar"/>
+ <vbox flex="1">
+ <label id="fxa-menu-header-title"
+ crop="end"
+ value="&fxa.menu.signin.label;"
+ defaultLabel="&fxa.menu.signin.label;"/>
+ <label id="fxa-menu-header-description"
+ crop="end"
+ value="&fxa.menu.turnOnSync.label;"
+ defaultLabel="&fxa.menu.turnOnSync.label;"/>
+ </vbox>
+ </toolbarbutton>
+ <toolbarseparator/>
+ <toolbarbutton id="PanelUI-fxa-menu-sendtab-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="gSync.showSendToDeviceViewFromFxaMenu(this);"/>
+ <toolbarseparator/>
+ <!-- The `Connect Another Device` button is disabled by default until the user logs into Sync. -->
+ <toolbarbutton id="PanelUI-fxa-menu-connect-device-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&fxa.menu.connectAnotherDevice2.label;"
+ disabled="true"
+ oncommand="gSync.openConnectAnotherDeviceFromFxaMenu(this);"/>
+ <toolbarseparator/>
+ <!-- The `Sync Now` button is hidden by default until the user logs into Sync. -->
+ <toolbarbutton id="PanelUI-fxa-menu-syncnow-button"
+ data-l10n-id="fxa-toolbar-sync-now"
+ syncinglabel="fxa-toolbar-sync-syncing"
+ hidden="true"
+ class="syncNowBtn subviewbutton subviewbutton-iconic"
+ onmouseover="gSync.refreshSyncButtonsTooltip();"
+ oncommand="gSync.doSyncFromFxaMenu(this);"
+ closemenu="none"/>
+ <toolbarbutton id="PanelUI-fxa-menu-setup-sync-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&fxa.menu.setupSync.label;"
+ oncommand="gSync.openPrefsFromFxaMenu('sync_settings', this);"/>
+ <toolbarbutton id="PanelUI-fxa-menu-remotetabs-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&appMenuRemoteTabs.label;"
+ closemenu="none"
+ oncommand="gSync.showRemoteTabsFromFxaMenu(this);"/>
+ <toolbarbutton id="PanelUI-fxa-menu-sync-prefs-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&fxa.menu.syncSettings2.label;"
+ hidden="true"
+ oncommand="gSync.openPrefsFromFxaMenu('sync_settings', this);"/>
+ <toolbarseparator/>
+ <toolbarbutton id="PanelUI-fxa-menu-logins-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&logins.label;"
+ oncommand="LoginHelper.openPasswordManager(window, { entryPoint: 'fxamenu' })"/>
+ <toolbarseparator id="fxa-menu-service-separator"/>
+ <label id="fxa-menu-service-label"
+ value="&fxa.menu.firefoxServices.label;"
+ class="subview-subheader"/>
+ <toolbarbutton id="PanelUI-fxa-menu-monitor-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&monitorFullName;"
+ type="open-to-new"
+ oncommand="gSync.openMonitorFromFxaMenu(this);"/>
+ <toolbarbutton id="PanelUI-fxa-menu-send-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&sendFullName;"
+ type="open-to-new"
+ oncommand="gSync.openSendFromFxaMenu(this);"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-fxa-menu-account-panel" flex="1" title="&fxa.menu.accountSettings.label;" class="PanelUI-subView" descriptionheightworkaround="true">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="PanelUI-fxa-menu-account-settings-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&fxa.menu.manageAccount2.label;"
+ type="open-to-new"
+ oncommand="gSync.openFxAManagePageFromFxaMenu(this)"/>
+ <toolbarseparator/>
+ <toolbarbutton id="PanelUI-fxa-menu-account-signout-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&fxa.menu.signOut.label;"
+ oncommand="gSync.disconnect();"/>
+ </vbox>
+ </panelview>
+
+ <!-- This panelview is used to contain the dynamically created buttons for send tab to devices -->
+ <panelview id="PanelUI-sendTabToDevice" flex="1" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="PanelUI-sendTabToDevice-syncingDevices" class="subviewbutton subviewbutton-iconic pageAction-sendToDevice-notReady"
+ label="&sendToDevice.syncNotReady.label;"
+ disabled="true"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-fxa-menu-sendtab-not-configured" flex="1" class="PanelUI-subView">
+ <vbox id="PanelUI-fxa-sendtab-not-configured" align="center" class="panel-subview-body">
+ <image class="fxaSendToDeviceLogo" role="presentation"/>
+ <label class="PanelUI-fxa-service-description-label">&fxa.service.sendTab.description;</label>
+ <toolbarbutton id="PanelUI-fxa-menu-sendtab-not-configured-button"
+ class="PanelUI-fxa-signin-button"
+ label="&fxa.menu.signin.label;"
+ oncommand="gSync.openPrefsFromFxaMenu('send_tab', this);"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-fxa-menu-sendtab-no-devices" flex="1" class="PanelUI-subView">
+ <vbox id="PanelUI-fxa-sendtab-no-devices" align="center" class="panel-subview-body">
+ <image class="fxaSendToDeviceLogo" role="presentation"/>
+ <label class="PanelUI-fxa-service-description-label">&fxa.service.sendTab.description;</label>
+ <toolbarbutton id="PanelUI-fxa-menu-sendtab-connect-device-button"
+ class="PanelUI-fxa-signin-button"
+ label="&appMenuRemoteTabs.connectdevice.label;"
+ oncommand="gSync.openConnectAnotherDeviceFromFxaMenu(this);"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="appMenu-libraryView" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appMenu-library-bookmarks-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ data-l10n-id="library-bookmarks-menu"
+ closemenu="none"
+ oncommand="BookmarkingUI.showSubView(this);"/>
+ <toolbarbutton id="appMenu-library-pocket-button"
+ class="subviewbutton subviewbutton-iconic"
+ label="&pocketMenuitem.label;"
+ oncommand="Pocket.openList(event)"/>
+ <toolbarbutton id="appMenu-library-history-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&historyMenu.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('PanelUI-history', this)"/>
+ <toolbarbutton id="appMenu-library-downloads-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&libraryDownloads.label;"
+ closemenu="none"
+ oncommand="DownloadsSubview.show(this);"/>
+ <toolbarbutton id="appMenu-library-remotetabs-button"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav sync-ui-item"
+ label="&appMenuRemoteTabs.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('PanelUI-remotetabs', this)"/>
+ <toolbarseparator hidden="true"/>
+ <label value="&appMenuRecentHighlights.label;"
+ hidden="true"
+ class="subview-subheader"/>
+ <toolbaritem id="appMenu-library-recentHighlights"
+ hidden="true"
+ orient="vertical"
+ smoothscroll="false"
+ flatList="true"
+ tooltip="bhTooltip">
+ <!-- Recent Highlights will go here -->
+ </toolbaritem>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-bookmarkingTools" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="panelMenu_toggleBookmarksMenu"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="bookmarks-tools-menu-button-visibility"
+ data-l10n-args='{ "isVisible": false }'
+ oncommand="BookmarkingUI.toggleMenuButtonInToolbar(this);"/>
+ <toolbarbutton id="panelMenu_viewBookmarksSidebar"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="bookmarks-tools-sidebar-visibility"
+ data-l10n-args='{ "isVisible": false }'
+ key="viewBookmarksSidebarKb"
+ oncommand="SidebarUI.toggle('viewBookmarksSidebar', this);">
+ <observes element="sidebar-box" attribute="positionend"/>
+ </toolbarbutton>
+ <toolbarbutton id="panelMenu_viewBookmarksToolbar"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="bookmarks-tools-toolbar-visibility"
+ data-l10n-args='{ "isVisible": false }'
+ oncommand="BookmarkingUI.toggleBookmarksToolbar('bookmark-tools');"/>
+ </vbox>
+ </panelview>
+
+ <panelview id="PanelUI-whatsNew" class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <box id="PanelUI-whatsNew-title" class="panel-header">
+ <label data-l10n-id="whatsnew-panel-header"/>
+ </box>
+ <toolbaritem id="PanelUI-whatsNew-content"
+ orient="vertical"
+ smoothscroll="false">
+ <html:div id="PanelUI-whatsNew-message-container" role="document">
+ <!-- What's New messages will be rendered here -->
+ </html:div>
+ </toolbaritem>
+ </vbox>
+ <checkbox id="panelMenu-toggleWhatsNew"
+ class="panelMenu-toggleWhatsNew-checkbox"
+ onclick="ToolbarPanelHub.toggleWhatsNewPref(event)"
+ data-l10n-id="whatsnew-panel-footer-checkbox"/>
+ </panelview>
+ </html:template>
+
+ <!-- Temporary wrapper until we move away from XUL flex to allow a negative
+ margin-top to slide the toolbox off screen in fullscreen layout.-->
+ <box>
+ <toolbox id="navigator-toolbox" flex="1">
+
+ <vbox id="titlebar">
+ <!-- Menu -->
+ <toolbar type="menubar" id="toolbar-menubar"
+ class="browser-toolbar chromeclass-menubar titlebar-color"
+ customizable="true"
+ mode="icons"
+#ifdef MENUBAR_CAN_AUTOHIDE
+ toolbarname="&menubarCmd.label;"
+ accesskey="&menubarCmd.accesskey;"
+ autohide="true"
+#endif
+ context="toolbar-context-menu">
+ <toolbaritem id="menubar-items" align="center">
+# The entire main menubar is placed into browser-menubar.inc, so that it can be
+# shared with other top level windows in macWindow.inc.xhtml.
+#include browser-menubar.inc
+ </toolbaritem>
+ <spacer flex="1" skipintoolbarset="true" style="-moz-box-ordinal-group: 1000;"/>
+#include titlebar-items.inc.xhtml
+ </toolbar>
+
+ <toolbar id="TabsToolbar"
+ class="browser-toolbar titlebar-color"
+ fullscreentoolbar="true"
+ customizable="true"
+ customizationtarget="TabsToolbar-customization-target"
+ mode="icons"
+ aria-label="&tabsToolbar.label;"
+ context="toolbar-context-menu"
+ flex="1">
+
+ <hbox class="titlebar-spacer" type="pre-tabs"/>
+
+ <hbox flex="1" align="end" class="toolbar-items">
+ <hbox id="TabsToolbar-customization-target" flex="1">
+ <tabs id="tabbrowser-tabs"
+ is="tabbrowser-tabs"
+ flex="1"
+ aria-multiselectable="true"
+ setfocus="false"
+ tooltip="tabbrowser-tab-tooltip"
+ stopwatchid="FX_TAB_CLICK_MS">
+ <hbox class="tab-drop-indicator" hidden="true"/>
+ <arrowscrollbox id="tabbrowser-arrowscrollbox" orient="horizontal" flex="1" style="min-width: 1px;" clicktoscroll="true" scrolledtostart="true" scrolledtoend="true">
+ <tab is="tabbrowser-tab" class="tabbrowser-tab" selected="true" visuallyselected="true" fadein="true"/>
+ <toolbarbutton id="tabs-newtab-button"
+ class="toolbarbutton-1"
+ command="cmd_newNavigatorTab"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <spacer class="closing-tabs-spacer" style="width: 0;"/>
+ </arrowscrollbox>
+ <html:span id="tabbrowser-tab-a11y-desc" hidden="true"/>
+ </tabs>
+
+ <toolbarbutton id="new-tab-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional"
+ label="&tabCmd.label;"
+ command="cmd_newNavigatorTab"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="dynamic-shortcut-tooltip"
+ ondrop="newTabButtonObserver.onDrop(event)"
+ ondragover="newTabButtonObserver.onDragOver(event)"
+ ondragenter="newTabButtonObserver.onDragOver(event)"
+ ondragexit="newTabButtonObserver.onDragExit(event)"
+ cui-areatype="toolbar"
+ removable="true"/>
+
+ <toolbarbutton id="alltabs-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button"
+ badged="true"
+ oncommand="gTabsPanel.showAllTabsPanel(event);"
+ label="&listAllTabs.label;"
+ tooltiptext="&listAllTabs.label;"
+ removable="false"/>
+ </hbox>
+ </hbox>
+
+ <hbox class="titlebar-spacer" type="post-tabs"/>
+
+#ifndef XP_MACOSX
+ <button class="accessibility-indicator" tooltiptext="&accessibilityIndicator.tooltip;"
+ aria-live="polite"/>
+ <hbox class="private-browsing-indicator"/>
+#endif
+
+#include titlebar-items.inc.xhtml
+
+#ifdef XP_MACOSX
+ <!-- OS X does not natively support RTL for its titlebar items, so we prevent this secondary
+ buttonbox from reversing order in RTL by forcing an LTR direction. -->
+ <hbox id="titlebar-secondary-buttonbox" dir="ltr">
+ <button class="accessibility-indicator" tooltiptext="&accessibilityIndicator.tooltip;" aria-live="polite"/>
+ <hbox class="private-browsing-indicator"/>
+ <hbox id="titlebar-fullscreen-button"/>
+ </hbox>
+#endif
+ </toolbar>
+
+ </vbox>
+
+ <toolbar id="nav-bar"
+ class="browser-toolbar"
+ aria-label="&navbar.accessibleLabel;"
+ fullscreentoolbar="true" mode="icons" customizable="true"
+ customizationtarget="nav-bar-customization-target"
+ overflowable="true"
+ overflowbutton="nav-bar-overflow-button"
+ overflowtarget="widget-overflow-list"
+ overflowpanel="widget-overflow"
+ context="toolbar-context-menu">
+
+ <toolbartabstop/>
+ <hbox id="nav-bar-customization-target" flex="1">
+ <toolbarbutton id="back-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ data-l10n-id="toolbar-button-back"
+ removable="false" overflows="false"
+ keepbroadcastattributeswhencustomizing="true"
+ command="Browser:BackOrBackDuplicate"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="back-button-tooltip"
+ context="backForwardMenu"/>
+ <toolbarbutton id="forward-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ data-l10n-id="toolbar-button-forward"
+ removable="false" overflows="false"
+ keepbroadcastattributeswhencustomizing="true"
+ command="Browser:ForwardOrForwardDuplicate"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="forward-button-tooltip"
+ context="backForwardMenu"/>
+ <toolbaritem id="stop-reload-button" class="chromeclass-toolbar-additional"
+ data-l10n-id="toolbar-button-stop-reload"
+ removable="true" overflows="false">
+ <toolbarbutton id="reload-button" class="toolbarbutton-1"
+ data-l10n-id="toolbar-button-reload"
+ command="Browser:ReloadOrDuplicate"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="dynamic-shortcut-tooltip">
+ <box class="toolbarbutton-animatable-box">
+ <image class="toolbarbutton-animatable-image"/>
+ </box>
+ </toolbarbutton>
+ <toolbarbutton id="stop-button" class="toolbarbutton-1"
+ data-l10n-id="toolbar-button-stop"
+ command="Browser:Stop"
+ tooltip="dynamic-shortcut-tooltip">
+ <box class="toolbarbutton-animatable-box">
+ <image class="toolbarbutton-animatable-image"/>
+ </box>
+ </toolbarbutton>
+ </toolbaritem>
+ <toolbarbutton id="home-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ removable="true"
+ label="&homeButton.label;"
+ ondragover="homeButtonObserver.onDragOver(event)"
+ ondragenter="homeButtonObserver.onDragOver(event)"
+ ondrop="homeButtonObserver.onDrop(event)"
+ ondragexit="homeButtonObserver.onDragExit(event)"
+ key="goHome"
+ onclick="BrowserHome(event);"
+ cui-areatype="toolbar"
+ tooltiptext="&homeButton.defaultPage.tooltip;"/>
+ <toolbarspring cui-areatype="toolbar" class="chromeclass-toolbar-additional"/>
+ <toolbaritem id="urlbar-container" flex="400" persist="width"
+ removable="false"
+ class="chromeclass-location" overflows="false">
+ <toolbartabstop/>
+ <hbox id="urlbar" flex="1"
+ context=""
+ focused="true"
+ pageproxystate="invalid">
+ <hbox id="urlbar-background"/>
+ <hbox id="urlbar-input-container"
+ flex="1"
+ pageproxystate="invalid">
+ <box id="urlbar-search-button"
+ class="chromeclass-toolbar-additional"/>
+ <!-- Use onclick instead of normal popup= syntax since the popup
+ code fires onmousedown, and hence eats our favicon drag events. -->
+ <box id="tracking-protection-icon-container" align="center"
+ role="button"
+ onclick="gProtectionsHandler.handleProtectionsButtonEvent(event);"
+ onkeypress="gProtectionsHandler.handleProtectionsButtonEvent(event);"
+ onmouseover="gProtectionsHandler.onTrackingProtectionIconHoveredOrFocused();"
+ onfocus="gProtectionsHandler.onTrackingProtectionIconHoveredOrFocused();"
+ tooltip="tracking-protection-icon-tooltip">
+ <box id="tracking-protection-icon-box">
+ <image id="tracking-protection-icon"/>
+ <box id="tracking-protection-icon-animatable-box" flex="1">
+ <image id="tracking-protection-icon-animatable-image" flex="1"/>
+ </box>
+ </box>
+ <tooltip id="tracking-protection-icon-tooltip">
+ <description id="tracking-protection-icon-tooltip-label" class="tooltip-label"/>
+ </tooltip>
+ </box>
+ <box id="identity-box" role="button"
+ align="center"
+ data-l10n-id="urlbar-identity-button"
+ pageproxystate="invalid"
+ onclick="gIdentityHandler.handleIdentityButtonEvent(event);"
+ onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);"
+ ondragstart="gIdentityHandler.onDragStart(event);">
+ <image id="identity-icon"
+ consumeanchor="identity-box"
+ onclick="PageProxyClickHandler(event);"/>
+ <image id="permissions-granted-icon"
+ data-l10n-id="urlbar-permissions-granted"/>
+ <box style="pointer-events: none;">
+ <image class="sharing-icon" id="webrtc-sharing-icon"/>
+ <image class="sharing-icon geo-icon" id="geo-sharing-icon"/>
+ <image class="sharing-icon xr-icon" id="xr-sharing-icon"/>
+ </box>
+ <box id="blocked-permissions-container" align="center">
+ <image data-permission-id="geo" class="blocked-permission-icon geo-icon" role="button"
+ data-l10n-id="urlbar-geolocation-blocked"/>
+ <image data-permission-id="xr" class="blocked-permission-icon xr-icon" role="button"
+ data-l10n-id="urlbar-xr-blocked"/>
+ <image data-permission-id="desktop-notification" class="blocked-permission-icon desktop-notification-icon" role="button"
+ data-l10n-id="urlbar-web-notifications-blocked"/>
+ <image data-permission-id="camera" class="blocked-permission-icon camera-icon" role="button"
+ data-l10n-id="urlbar-camera-blocked"/>
+ <image data-permission-id="microphone" class="blocked-permission-icon microphone-icon" role="button"
+ data-l10n-id="urlbar-microphone-blocked"/>
+ <image data-permission-id="screen" class="blocked-permission-icon screen-icon" role="button"
+ data-l10n-id="urlbar-screen-blocked"/>
+ <image data-permission-id="persistent-storage" class="blocked-permission-icon persistent-storage-icon" role="button"
+ data-l10n-id="urlbar-persistent-storage-blocked"/>
+ <image data-permission-id="popup" class="blocked-permission-icon popup-icon" role="button"
+ data-l10n-id="urlbar-popup-blocked"/>
+ <image data-permission-id="autoplay-media" class="blocked-permission-icon autoplay-media-icon" role="button"
+ data-l10n-id="urlbar-autoplay-media-blocked"/>
+ <image data-permission-id="canvas" class="blocked-permission-icon canvas-icon" role="button"
+ data-l10n-id="urlbar-canvas-blocked"/>
+ <image data-permission-id="midi" class="blocked-permission-icon midi-icon" role="button"
+ data-l10n-id="urlbar-midi-blocked"/>
+ <image data-permission-id="install" class="blocked-permission-icon install-icon" role="button"
+ data-l10n-id="urlbar-install-blocked"/>
+ </box>
+ <box id="notification-popup-box"
+ hidden="true"
+ onmouseover="document.getElementById('identity-box').classList.add('no-hover');"
+ onmouseout="document.getElementById('identity-box').classList.remove('no-hover');"
+ align="center">
+ <image id="default-notification-icon" class="notification-anchor-icon" role="button"
+ data-l10n-id="urlbar-default-notification-anchor"/>
+ <image id="geo-notification-icon" class="notification-anchor-icon geo-icon" role="button"
+ data-l10n-id="urlbar-geolocation-notification-anchor"/>
+ <image id="xr-notification-icon" class="notification-anchor-icon xr-icon" role="button"
+ data-l10n-id="urlbar-xr-notification-anchor"/>
+ <image id="autoplay-media-notification-icon" class="notification-anchor-icon autoplay-media-icon" role="button"
+ data-l10n-id="urlbar-autoplay-notification-anchor"/>
+ <image id="addons-notification-icon" class="notification-anchor-icon install-icon" role="button"
+ data-l10n-id="urlbar-addons-notification-anchor"/>
+ <image id="canvas-notification-icon" class="notification-anchor-icon" role="button"
+ data-l10n-id="urlbar-canvas-notification-anchor"/>
+ <image id="indexedDB-notification-icon" class="notification-anchor-icon indexedDB-icon" role="button"
+ data-l10n-id="urlbar-indexed-db-notification-anchor"/>
+ <image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button"
+ data-l10n-id="urlbar-password-notification-anchor"/>
+ <stack id="plugins-notification-icon" class="notification-anchor-icon" role="button" align="center" data-l10n-id="urlbar-plugins-notification-anchor">
+ <image class="plugin-icon" />
+ <image id="plugin-icon-badge" />
+ </stack>
+ <image id="web-notifications-notification-icon" class="notification-anchor-icon desktop-notification-icon" role="button"
+ data-l10n-id="urlbar-web-notification-anchor"/>
+ <image id="webRTC-shareDevices-notification-icon" class="notification-anchor-icon camera-icon" role="button"
+ data-l10n-id="urlbar-web-rtc-share-devices-notification-anchor"/>
+ <image id="webRTC-shareMicrophone-notification-icon" class="notification-anchor-icon microphone-icon" role="button"
+ data-l10n-id="urlbar-web-rtc-share-microphone-notification-anchor"/>
+ <image id="webRTC-shareScreen-notification-icon" class="notification-anchor-icon screen-icon" role="button"
+ data-l10n-id="urlbar-web-rtc-share-screen-notification-anchor"/>
+ <image id="servicesInstall-notification-icon" class="notification-anchor-icon service-icon" role="button"
+ data-l10n-id="urlbar-services-notification-anchor"/>
+ <image id="translate-notification-icon" class="notification-anchor-icon translation-icon" role="button"
+ data-l10n-id="urlbar-translate-notification-anchor"/>
+ <image id="translated-notification-icon" class="notification-anchor-icon translation-icon in-use" role="button"
+ data-l10n-id="urlbar-translated-notification-anchor"/>
+ <image id="eme-notification-icon" class="notification-anchor-icon drm-icon" role="button"
+ data-l10n-id="urlbar-eme-notification-anchor"/>
+ <image id="persistent-storage-notification-icon" class="notification-anchor-icon persistent-storage-icon" role="button"
+ data-l10n-id="urlbar-persistent-storage-notification-anchor"/>
+ <image id="midi-notification-icon" class="notification-anchor-icon midi-icon" role="button"
+ data-l10n-id="urlbar-midi-notification-anchor"/>
+ <image id="webauthn-notification-icon" class="notification-anchor-icon" role="button"
+ data-l10n-id="urlbar-web-authn-anchor"/>
+ <image id="storage-access-notification-icon" class="notification-anchor-icon storage-access-icon" role="button"
+ data-l10n-id="urlbar-storage-access-anchor"/>
+ </box>
+ <image id="remote-control-icon"
+ data-l10n-id="urlbar-remote-control-notification-anchor"/>
+ <label id="identity-icon-label" class="plain" crop="center" flex="1"/>
+ </box>
+ <box id="urlbar-label-box" align="center">
+ <label id="urlbar-label-switchtab" class="urlbar-label" data-l10n-id="urlbar-switch-to-tab"/>
+ <label id="urlbar-label-extension" class="urlbar-label" data-l10n-id="urlbar-extension"/>
+ <label id="urlbar-label-search-mode" class="urlbar-label"/>
+ </box>
+ <html:div id="urlbar-search-mode-indicator">
+ <html:span id="urlbar-search-mode-indicator-title"/>
+ <html:div id="urlbar-search-mode-indicator-close"
+ class="close-button"
+ role="button"/>
+ </html:div>
+ <moz-input-box tooltip="aHTMLTooltip"
+ class="urlbar-input-box"
+ flex="1"
+ role="combobox"
+ aria-owns="urlbar-results">
+ <html:input id="urlbar-scheme"
+ required="required"/>
+ <html:input id="urlbar-input"
+ anonid="input"
+ aria-controls="urlbar-results"
+ aria-autocomplete="both"
+ inputmode="mozAwesomebar"
+ data-l10n-id="urlbar-placeholder"
+ data-l10n-attrs="placeholder"/>
+ </moz-input-box>
+ <image id="urlbar-go-button"
+ class="urlbar-icon"
+ onclick="gURLBar.handleCommand(event);"
+ data-l10n-id="urlbar-go-button"/>
+ <hbox id="page-action-buttons" context="pageActionContextMenu">
+ <toolbartabstop/>
+ <hbox id="contextual-feature-recommendation" role="button" hidden="true">
+ <hbox id="cfr-label-container">
+ <label id="cfr-label"/>
+ </hbox>
+ <image id="cfr-button"
+ class="urlbar-icon urlbar-page-action"
+ role="presentation"/>
+ </hbox>
+ <hbox id="userContext-icons" hidden="true">
+ <label id="userContext-label"/>
+ <image id="userContext-indicator"/>
+ </hbox>
+ <image id="reader-mode-button"
+ class="urlbar-icon urlbar-page-action"
+ tooltip="dynamic-shortcut-tooltip"
+ role="button"
+ hidden="true"
+ onclick="AboutReaderParent.buttonClick(event);"/>
+ <toolbarbutton id="urlbar-zoom-button"
+ onclick="FullZoom.reset(); FullZoom.resetScalingZoom();"
+ tooltip="dynamic-shortcut-tooltip"
+ hidden="true"/>
+ <box id="pageActionSeparator" class="urlbar-page-action"/>
+ <image id="pageActionButton"
+ class="urlbar-icon urlbar-page-action"
+ role="button"
+ data-l10n-id="urlbar-page-action-button"
+ onmousedown="BrowserPageActions.mainButtonClicked(event);"
+ onkeypress="BrowserPageActions.mainButtonClicked(event);"/>
+ <image id="pocket-button"
+ class="urlbar-icon urlbar-page-action"
+ data-l10n-id="urlbar-pocket-button"
+ role="button"
+ hidden="true"
+ onclick="BrowserPageActions.doCommandForAction(PageActions.actionForID('pocket'), event, this);"/>
+ <hbox id="star-button-box"
+ hidden="true"
+ class="urlbar-icon-wrapper urlbar-page-action"
+ onclick="BrowserPageActions.doCommandForAction(PageActions.actionForID('bookmark'), event, this);">
+ <image id="star-button"
+ class="urlbar-icon"
+ role="button"/>
+ <hbox id="star-button-animatable-box">
+ <image id="star-button-animatable-image"
+ role="presentation"/>
+ </hbox>
+ </hbox>
+ </hbox>
+ </hbox>
+ </hbox>
+ <toolbartabstop/>
+ </toolbaritem>
+
+ <toolbarspring cui-areatype="toolbar" class="chromeclass-toolbar-additional"/>
+
+ <!-- This is a placeholder for the Downloads Indicator. It is visible
+ during the customization of the toolbar, in the palette, and before
+ the Downloads Indicator overlay is loaded. -->
+ <toolbarbutton id="downloads-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional"
+ badged="true"
+ key="key_openDownloads"
+ onmousedown="DownloadsIndicatorView.onCommand(event);"
+ onkeypress="DownloadsIndicatorView.onCommand(event);"
+ ondrop="DownloadsIndicatorView.onDrop(event);"
+ ondragover="DownloadsIndicatorView.onDragOver(event);"
+ ondragenter="DownloadsIndicatorView.onDragOver(event);"
+ label="&downloads.label;"
+ removable="true"
+ overflows="false"
+ cui-areatype="toolbar"
+ hidden="true"
+ tooltip="dynamic-shortcut-tooltip"
+ indicator="true">
+ <!-- The panel's anchor area is smaller than the outer button, but must
+ always be visible and must not move or resize when the indicator
+ state changes, otherwise the panel could change its position or lose
+ its arrow unexpectedly. -->
+ <stack id="downloads-indicator-anchor"
+ consumeanchor="downloads-button">
+ <box id="downloads-indicator-icon"/>
+ <stack id="downloads-indicator-progress-outer">
+ <box id="downloads-indicator-progress-inner"/>
+ </stack>
+ </stack>
+ </toolbarbutton>
+
+ <toolbarbutton id="library-button" class="toolbarbutton-1 chromeclass-toolbar-additional subviewbutton-nav"
+ removable="true"
+ onmousedown="PanelUI.showSubView('appMenu-libraryView', this, event);"
+ onkeypress="PanelUI.showSubView('appMenu-libraryView', this, event);"
+ closemenu="none"
+ cui-areatype="toolbar"
+ tooltiptext="&libraryButton.tooltip;"
+ label="&places.library.title;"/>
+
+ <toolbarbutton id="fxa-toolbar-menu-button" class="toolbarbutton-1 chromeclass-toolbar-additional subviewbutton-nav"
+ badged="true"
+ onmousedown="gSync.toggleAccountPanel('PanelUI-fxa', this, event)"
+ onkeypress="gSync.toggleAccountPanel('PanelUI-fxa', this, event)"
+ consumeanchor="fxa-toolbar-menu-button"
+ closemenu="none"
+ label="&fxa.menu.firefoxAccount;"
+ tooltiptext="&fxa.menu.firefoxAccount;"
+ cui-areatype="toolbar"
+ removable="true">
+ <vbox>
+ <image id="fxa-avatar-image"/>
+ </vbox>
+ </toolbarbutton>
+ </hbox>
+
+ <toolbarbutton id="nav-bar-overflow-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional overflow-button"
+ skipintoolbarset="true"
+ tooltiptext="&navbarOverflow.label;">
+ <box class="toolbarbutton-animatable-box">
+ <image class="toolbarbutton-animatable-image"/>
+ </box>
+ </toolbarbutton>
+
+ <toolbaritem id="PanelUI-button"
+ removable="false">
+ <toolbarbutton id="ion-button"
+ class="toolbarbutton-1"
+ hidden="true"
+ badged="true"
+ tooltiptext="Ion"
+ onmousedown="switchToTabHavingURI('about:ion', true);"
+ onkeypress="switchToTabHavingURI('about:ion', true);"/>
+ <toolbarbutton id="whats-new-menu-button"
+ class="toolbarbutton-1"
+ hidden="true"
+ badged="true"
+ onmousedown="PanelUI.showSubView('PanelUI-whatsNew', this, event);"
+ onkeypress="PanelUI.showSubView('PanelUI-whatsNew', this, event);"/>
+ <toolbarbutton id="PanelUI-menu-button"
+ class="toolbarbutton-1"
+ badged="true"
+ consumeanchor="PanelUI-button"
+ label="&brandShortName;"
+ tooltiptext="&appmenu.tooltip;"/>
+ </toolbaritem>
+
+ <hbox id="window-controls" hidden="true" pack="end" skipintoolbarset="true"
+ style="-moz-box-ordinal-group: 1000;">
+ <toolbarbutton id="minimize-button"
+ data-l10n-id="browser-window-minimize-button"
+ oncommand="window.minimize();"/>
+
+ <toolbarbutton id="restore-button"
+ data-l10n-id="browser-window-restore-down-button"
+ oncommand="BrowserFullScreen();"/>
+
+ <toolbarbutton id="close-button"
+ data-l10n-id="browser-window-close-button"
+ oncommand="BrowserTryToCloseWindow();"/>
+ </hbox>
+
+ <box id="library-animatable-box" class="toolbarbutton-animatable-box">
+ <image class="toolbarbutton-animatable-image"/>
+ </box>
+ </toolbar>
+
+ <toolbar id="PersonalToolbar"
+ mode="icons"
+ class="browser-toolbar chromeclass-directories"
+ context="toolbar-context-menu"
+ data-l10n-id="bookmarks-toolbar"
+ data-l10n-attrs="toolbarname"
+ customizable="true">
+ <toolbartabstop skipintoolbarset="true"/>
+
+ <hbox id="personal-toolbar-empty" skipintoolbarset="true" removable="false" hidden="true">
+ <description id="personal-toolbar-empty-description"
+ data-l10n-id="bookmarks-toolbar-empty-message"
+ onclick="BookmarkingUI.openLibraryIfLinkClicked(event);"
+ onkeydown="BookmarkingUI.openLibraryIfLinkClicked(event);">
+ <html:a data-l10n-name="manage-bookmarks" class="text-link" tabindex="0"/>
+ </description>
+ </hbox>
+
+ <toolbaritem id="personal-bookmarks"
+ data-l10n-id="bookmarks-toolbar-placeholder"
+ cui-areatype="toolbar"
+ removable="true">
+ <toolbarbutton id="bookmarks-toolbar-placeholder"
+ class="bookmark-item"
+ data-l10n-id="bookmarks-toolbar-placeholder-button"/>
+ <toolbarbutton id="bookmarks-toolbar-button"
+ class="toolbarbutton-1"
+ flex="1"
+ data-l10n-id="bookmarks-toolbar-placeholder-button"
+ oncommand="PlacesToolbarHelper.onPlaceholderCommand();"/>
+ <hbox flex="1"
+ id="PlacesToolbar"
+ context="placesContext"
+ onmouseup="BookmarksEventHandler.onMouseUp(event);"
+ onclick="BookmarksEventHandler.onClick(event, this._placesView);"
+ oncommand="BookmarksEventHandler.onCommand(event);"
+ tooltip="bhTooltip"
+ popupsinherittooltip="true">
+ <hbox flex="1">
+ <hbox id="PlacesToolbarDropIndicatorHolder" align="center" collapsed="true">
+ <image id="PlacesToolbarDropIndicator"
+ collapsed="true"/>
+ </hbox>
+ <scrollbox orient="horizontal"
+ id="PlacesToolbarItems"
+ flex="1"/>
+ <toolbarbutton type="menu"
+ id="PlacesChevron"
+ class="toolbarbutton-1"
+ collapsed="true"
+ data-l10n-id="bookmarks-toolbar-chevron"
+ onpopupshowing="document.getElementById('PlacesToolbar')
+ ._placesView._onChevronPopupShowing(event);">
+ <menupopup id="PlacesChevronPopup"
+ is="places-popup"
+ placespopup="true"
+ tooltip="bhTooltip" popupsinherittooltip="true"
+ context="placesContext"/>
+ </toolbarbutton>
+ </hbox>
+ </hbox>
+ </toolbaritem>
+ </toolbar>
+
+ <html:template id="BrowserToolbarPalette">
+ <toolbarbutton id="import-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional"
+ oncommand="MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_BOOKMARKS_TOOLBAR]);"
+ data-l10n-id="browser-import-button2"/>
+
+ <toolbarbutton id="print-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+#ifdef XP_MACOSX
+ command="cmd_print"
+#else
+ command="cmd_printPreview"
+ print-button-title="&printButton.tooltip;"
+#endif
+ keepbroadcastattributeswhencustomizing="true"
+ tooltip="dynamic-shortcut-tooltip"
+ label="&printButton.label;"/>
+
+
+ <toolbarbutton id="new-window-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ label="&newNavigatorCmd.label;"
+ command="cmd_newNavigator"
+ tooltip="dynamic-shortcut-tooltip"
+ ondrop="newWindowButtonObserver.onDrop(event)"
+ ondragover="newWindowButtonObserver.onDragOver(event)"
+ ondragenter="newWindowButtonObserver.onDragOver(event)"
+ ondragexit="newWindowButtonObserver.onDragExit(event)"/>
+
+ <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ observes="View:FullScreen"
+ type="checkbox"
+ label="&fullScreenCmd.label;"
+ tooltip="dynamic-shortcut-tooltip"/>
+
+ <toolbarbutton id="bookmarks-menu-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional subviewbutton-nav"
+ type="menu"
+ data-l10n-id="bookmarks-menu-button"
+ tooltip="dynamic-shortcut-tooltip"
+ ondragenter="PlacesMenuDNDHandler.onDragEnter(event);"
+ ondragover="PlacesMenuDNDHandler.onDragOver(event);"
+ ondragleave="PlacesMenuDNDHandler.onDragLeave(event);"
+ ondrop="PlacesMenuDNDHandler.onDrop(event);"
+ oncommand="BookmarkingUI.onCommand(event);">
+ <menupopup id="BMB_bookmarksPopup"
+ type="arrow"
+ is="places-popup-arrow"
+ class="cui-widget-panel cui-widget-panelview cui-widget-panelWithFooter PanelUI-subView"
+ placespopup="true"
+ context="placesContext"
+ openInTabs="children"
+ side="top"
+ onmouseup="BookmarksEventHandler.onMouseUp(event);"
+ oncommand="BookmarksEventHandler.onCommand(event);"
+ onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);"
+ onpopupshowing="BookmarkingUI.onPopupShowing(event);
+ BookmarkingUI.attachPlacesView(event, this);"
+ tooltip="bhTooltip" popupsinherittooltip="true">
+ <menuitem id="BMB_viewBookmarksSidebar"
+ class="menuitem-iconic subviewbutton"
+ data-l10n-id="bookmarks-tools-sidebar-visibility"
+ data-l10n-args='{ "isVisible": false }'
+ oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
+ <!-- NB: temporary solution for bug 985024, this should go away soon. -->
+ <menuitem id="BMB_bookmarksShowAllTop"
+ class="menuitem-iconic subviewbutton"
+ data-l10n-id="bookmarks-show-all-bookmarks"
+ command="Browser:ShowAllBookmarks"
+ key="manBookmarkKb"/>
+ <menuseparator/>
+ <menu id="BMB_bookmarksToolbar"
+ class="menu-iconic bookmark-item subviewbutton"
+ data-l10n-id="bookmarks-toolbar-menu"
+ container="true">
+ <menupopup id="BMB_bookmarksToolbarPopup"
+ is="places-popup"
+ placespopup="true"
+ nofooterpopup="true"
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`,
+ PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);">
+ <menuitem id="BMB_viewBookmarksToolbar"
+ class="menuitem-iconic subviewbutton"
+ data-l10n-id="bookmarks-tools-toolbar-visibility"
+ data-l10n-args='{ "isVisible": false }'
+ oncommand="BookmarkingUI.toggleBookmarksToolbar('bookmarks-widget');"/>
+ <menuseparator/>
+ <!-- Bookmarks toolbar items -->
+ </menupopup>
+ </menu>
+ <menu id="BMB_unsortedBookmarks"
+ class="menu-iconic bookmark-item subviewbutton"
+ data-l10n-id="bookmarks-other-bookmarks-menu"
+ container="true">
+ <menupopup id="BMB_unsortedBookmarksPopup"
+ is="places-popup"
+ placespopup="true"
+ nofooterpopup="true"
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`,
+ PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"/>
+ </menu>
+ <menu id="BMB_mobileBookmarks"
+ class="menu-iconic bookmark-item subviewbutton"
+ data-l10n-id="bookmarks-mobile-bookmarks-menu"
+ hidden="true"
+ container="true">
+ <menupopup id="BMB_mobileBookmarksPopup"
+ is="places-popup"
+ placespopup="true"
+ nofooterpopup="true"
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, `place:parent=${PlacesUtils.bookmarks.mobileGuid}`,
+ PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"/>
+ </menu>
+
+ <menuseparator/>
+ <!-- Bookmarks menu items will go here -->
+ <menuitem id="BMB_bookmarksShowAll"
+ class="subviewbutton panel-subview-footer"
+ data-l10n-id="bookmarks-show-all-bookmarks"
+ command="Browser:ShowAllBookmarks"
+ key="manBookmarkKb"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbaritem id="search-container"
+ class="chromeclass-toolbar-additional"
+ title="&searchItem.title;"
+ align="center"
+ flex="175"
+ persist="width">
+ <toolbartabstop/>
+ <searchbar id="searchbar" flex="1"/>
+ <toolbartabstop/>
+ </toolbaritem>
+ </html:template>
+ </toolbox>
+ </box>
+
+ <hbox flex="1" id="browser">
+ <vbox id="sidebar-box" hidden="true" class="chromeclass-extrachrome">
+ <box id="sidebar-header" align="center">
+ <toolbarbutton id="sidebar-switcher-target" flex="1" class="tabbable">
+ <image id="sidebar-icon" consumeanchor="sidebar-switcher-target"/>
+ <label id="sidebar-title" crop="end" flex="1" control="sidebar"/>
+ <image id="sidebar-switcher-arrow"/>
+ </toolbarbutton>
+ <image id="sidebar-throbber"/>
+# To ensure the button label's intrinsic width doesn't expand the sidebar
+# if the label is long, the button needs flex=1.
+# To ensure the button doesn't expand unnecessarily for short labels, the
+# spacer should significantly out-flex the button.
+ <spacer flex="1000"/>
+ <toolbarbutton id="sidebar-close" class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="SidebarUI.hide();"/>
+ </box>
+ <browser id="sidebar" flex="1" autoscroll="false" disablehistory="true" disablefullscreen="true"
+ style="min-width: 14em; width: 18em; max-width: 36em;" tooltip="aHTMLTooltip"/>
+ </vbox>
+
+ <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/>
+ <vbox id="appcontent" flex="1">
+ <!-- gHighPriorityNotificationBox will be added here lazily. -->
+ <tabbox id="tabbrowser-tabbox"
+ flex="1" tabcontainer="tabbrowser-tabs">
+ <tabpanels id="tabbrowser-tabpanels"
+ flex="1" class="plain" selectedIndex="0"/>
+ </tabbox>
+ </vbox>
+ </hbox>
+
+ <html:template id="customizationPanel">
+ <box id="customization-container" flex="1" hidden="true"><![CDATA[
+#include ../../components/customizableui/content/customizeMode.inc.xhtml
+ ]]></box>
+ </html:template>
+
+ <html:div id="fullscreen-and-pointerlock-wrapper">
+ <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true">
+ <html:div class="pointerlockfswarning-domain-text">
+ <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/>
+ </html:div>
+ <html:div class="pointerlockfswarning-generic-text"
+ data-l10n-id="fullscreen-warning-no-domain"></html:div>
+ <html:button id="fullscreen-exit-button"
+ onclick="FullScreen.exitDomFullScreen();"
+#ifdef XP_MACOSX
+ data-l10n-id="fullscreen-exit-mac-button"
+#else
+ data-l10n-id="fullscreen-exit-button"
+#endif
+ >
+ </html:button>
+ </html:div>
+
+ <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true">
+ <html:div class="pointerlockfswarning-domain-text">
+ <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/>
+ </html:div>
+ <html:div class="pointerlockfswarning-generic-text"
+ data-l10n-id="pointerlock-warning-no-domain"></html:div>
+ </html:div>
+ </html:div>
+
+ <vbox id="browser-bottombox" layer="true">
+ <!-- gNotificationBox will be added here lazily. -->
+ </vbox>
+
+ <html:div id="a11y-announcement" role="alert"/>
+
+ <!-- Put it at the very end to make sure it's not covered by anything. -->
+ <html:div id="fullscr-toggler" hidden="hidden"/>
+</html:body>
+</html>
diff --git a/browser/base/content/contentTheme.js b/browser/base/content/contentTheme.js
new file mode 100644
index 0000000000..48629ed9e4
--- /dev/null
+++ b/browser/base/content/contentTheme.js
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+{
+ function _isTextColorDark(r, g, b) {
+ return 0.2125 * r + 0.7154 * g + 0.0721 * b <= 110;
+ }
+
+ const inContentVariableMap = [
+ [
+ "--newtab-background-color",
+ {
+ lwtProperty: "ntp_background",
+ },
+ ],
+ [
+ "--newtab-text-primary-color",
+ {
+ lwtProperty: "ntp_text",
+ processColor(rgbaChannels, element) {
+ if (!rgbaChannels) {
+ element.removeAttribute("lwt-newtab");
+ element.removeAttribute("lwt-newtab-brighttext");
+ return null;
+ }
+
+ element.setAttribute("lwt-newtab", "true");
+ const { r, g, b, a } = rgbaChannels;
+ if (!_isTextColorDark(r, g, b)) {
+ element.setAttribute("lwt-newtab-brighttext", "true");
+ } else {
+ element.removeAttribute("lwt-newtab-brighttext");
+ }
+
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+ },
+ },
+ ],
+ [
+ "--lwt-sidebar-background-color",
+ {
+ lwtProperty: "sidebar",
+ processColor(rgbaChannels) {
+ if (!rgbaChannels) {
+ return null;
+ }
+ const { r, g, b } = rgbaChannels;
+ // Drop alpha channel
+ return `rgb(${r}, ${g}, ${b})`;
+ },
+ },
+ ],
+ [
+ "--lwt-sidebar-text-color",
+ {
+ lwtProperty: "sidebar_text",
+ processColor(rgbaChannels, element) {
+ if (!rgbaChannels) {
+ element.removeAttribute("lwt-sidebar");
+ element.removeAttribute("lwt-sidebar-brighttext");
+ return null;
+ }
+
+ element.setAttribute("lwt-sidebar", "true");
+ const { r, g, b, a } = rgbaChannels;
+ if (!_isTextColorDark(r, g, b)) {
+ element.setAttribute("lwt-sidebar-brighttext", "true");
+ } else {
+ element.removeAttribute("lwt-sidebar-brighttext");
+ }
+
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+ },
+ },
+ ],
+ [
+ "--lwt-sidebar-highlight-background-color",
+ {
+ lwtProperty: "sidebar_highlight",
+ },
+ ],
+ [
+ "--lwt-sidebar-highlight-text-color",
+ {
+ lwtProperty: "sidebar_highlight_text",
+ processColor(rgbaChannels, element) {
+ if (!rgbaChannels) {
+ element.removeAttribute("lwt-sidebar-highlight");
+ return null;
+ }
+ element.setAttribute("lwt-sidebar-highlight", "true");
+
+ const { r, g, b, a } = rgbaChannels;
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+ },
+ },
+ ],
+ ];
+
+ /**
+ * ContentThemeController handles theme updates sent by the frame script.
+ * To be able to use ContentThemeController, you must add your page to the whitelist
+ * in LightweightThemeChildListener.jsm
+ */
+ const ContentThemeController = {
+ /**
+ * Tell the frame script that the page supports theming, and watch for updates
+ * from the frame script.
+ */
+ init() {
+ addEventListener("LightweightTheme:Set", this);
+ },
+
+ /**
+ * Handle theme updates from the frame script.
+ * @param {Object} event object containing the theme update.
+ */
+ handleEvent({ type, detail }) {
+ if (type == "LightweightTheme:Set") {
+ let { data } = detail;
+ if (!data) {
+ data = {};
+ }
+ // XUL documents don't have a body
+ const element = document.body
+ ? document.body
+ : document.documentElement;
+ this._setProperties(element, data);
+ }
+ },
+
+ /**
+ * Set a CSS variable to a given value
+ * @param {Element} elem The element where the CSS variable should be added.
+ * @param {string} variableName The CSS variable to set.
+ * @param {string} value The new value of the CSS variable.
+ */
+ _setProperty(elem, variableName, value) {
+ if (value) {
+ elem.style.setProperty(variableName, value);
+ } else {
+ elem.style.removeProperty(variableName);
+ }
+ },
+
+ /**
+ * Apply theme data to an element
+ * @param {Element} root The element where the properties should be applied.
+ * @param {Object} themeData The theme data.
+ */
+ _setProperties(elem, themeData) {
+ for (let [cssVarName, definition] of inContentVariableMap) {
+ const { lwtProperty, processColor } = definition;
+ let value = themeData[lwtProperty];
+
+ if (processColor) {
+ value = processColor(value, elem);
+ } else if (value) {
+ const { r, g, b, a } = value;
+ value = `rgba(${r}, ${g}, ${b}, ${a})`;
+ }
+
+ this._setProperty(elem, cssVarName, value);
+ }
+ },
+ };
+ ContentThemeController.init();
+}
diff --git a/browser/base/content/defaultthemes/1.header.jpg b/browser/base/content/defaultthemes/1.header.jpg
new file mode 100644
index 0000000000..58c52f86a3
--- /dev/null
+++ b/browser/base/content/defaultthemes/1.header.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/1.icon.jpg b/browser/base/content/defaultthemes/1.icon.jpg
new file mode 100644
index 0000000000..67b316d9f2
--- /dev/null
+++ b/browser/base/content/defaultthemes/1.icon.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/1.preview.jpg b/browser/base/content/defaultthemes/1.preview.jpg
new file mode 100644
index 0000000000..1394c5936b
--- /dev/null
+++ b/browser/base/content/defaultthemes/1.preview.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/2.header.jpg b/browser/base/content/defaultthemes/2.header.jpg
new file mode 100644
index 0000000000..8a4aec3531
--- /dev/null
+++ b/browser/base/content/defaultthemes/2.header.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/2.icon.jpg b/browser/base/content/defaultthemes/2.icon.jpg
new file mode 100644
index 0000000000..4eeed30caf
--- /dev/null
+++ b/browser/base/content/defaultthemes/2.icon.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/2.preview.jpg b/browser/base/content/defaultthemes/2.preview.jpg
new file mode 100644
index 0000000000..cc45cfc944
--- /dev/null
+++ b/browser/base/content/defaultthemes/2.preview.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/3.header.png b/browser/base/content/defaultthemes/3.header.png
new file mode 100644
index 0000000000..aa3eab5210
--- /dev/null
+++ b/browser/base/content/defaultthemes/3.header.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/3.icon.png b/browser/base/content/defaultthemes/3.icon.png
new file mode 100644
index 0000000000..4c57477502
--- /dev/null
+++ b/browser/base/content/defaultthemes/3.icon.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/3.preview.png b/browser/base/content/defaultthemes/3.preview.png
new file mode 100644
index 0000000000..7f488fd48b
--- /dev/null
+++ b/browser/base/content/defaultthemes/3.preview.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/4.header.png b/browser/base/content/defaultthemes/4.header.png
new file mode 100644
index 0000000000..5966a9ae71
--- /dev/null
+++ b/browser/base/content/defaultthemes/4.header.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/4.icon.png b/browser/base/content/defaultthemes/4.icon.png
new file mode 100644
index 0000000000..7097a15df2
--- /dev/null
+++ b/browser/base/content/defaultthemes/4.icon.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/4.preview.png b/browser/base/content/defaultthemes/4.preview.png
new file mode 100644
index 0000000000..868c9a0cb7
--- /dev/null
+++ b/browser/base/content/defaultthemes/4.preview.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/5.header.png b/browser/base/content/defaultthemes/5.header.png
new file mode 100644
index 0000000000..f48a846347
--- /dev/null
+++ b/browser/base/content/defaultthemes/5.header.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/5.icon.jpg b/browser/base/content/defaultthemes/5.icon.jpg
new file mode 100644
index 0000000000..b3e103ee5f
--- /dev/null
+++ b/browser/base/content/defaultthemes/5.icon.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/5.preview.jpg b/browser/base/content/defaultthemes/5.preview.jpg
new file mode 100644
index 0000000000..78c2f12485
--- /dev/null
+++ b/browser/base/content/defaultthemes/5.preview.jpg
Binary files differ
diff --git a/browser/base/content/docs/tabbrowser/async-tab-switcher.rst b/browser/base/content/docs/tabbrowser/async-tab-switcher.rst
new file mode 100644
index 0000000000..ab758cbd3d
--- /dev/null
+++ b/browser/base/content/docs/tabbrowser/async-tab-switcher.rst
@@ -0,0 +1,239 @@
+.. _tabbrowser_async_tab_switcher:
+
+==================
+Async tab switcher
+==================
+
+At a very high level, the async tab switcher is responsible for telling tabs with out-of-process (or “remote”) ``<xul:browser>``’s to render and upload their contents to the compositor, and then update the UI to show that content as a tab switch. Similarly, the async tab switcher is responsible for telling tabs that have been switched away from to stop rendering their content, and for the compositor to release those contents.
+
+Briefly introducing Layers and the Compositor
+=============================================
+
+For out-of-process tabs, the presentation portion of Gecko computes the final contents of a tab inside the tabs content process, and then uploads that information to the compositor. This uploaded information is usually referred to as *layers*.
+
+The compositor is what eventually presents these layers to the user as pixels. The compositor can retain several sets of layers without necessarily showing them to the user, but this consumes memory. Layers that are no longer needed are released.
+
+From here forward, "contents of a tab" will be referred to as that tab's *layers*.
+
+.. _async-tab-switcher.useful-properties:
+
+renderLayers, hasLayers, docShellIsActive
+=========================================
+
+``<xul:browser>``'s have a number of useful properties exposed on them that the async tab switcher uses:
+
+``renderLayers``
+ For remote ``<xul:browser>``'s, setting this to ``true`` from ``false`` means to ask the content process to render the layers for that ``<xul:browser>`` and upload them to the compositor. Setting this to ``false`` from ``true`` means to ask the content process to stop rendering the layers and for the compositor to release the layers. Setting this property to ``true`` when it is already ``true`` or ``false`` when it is already ``false`` is a no-op. When this property returns ``true``, this means that layers have been requested for this tab, but there is no guarantee that the layers have been received by the compositor yet. Similarly, when this property returns ``false``, this means that this browser has been asked to stop rendering layers, but there is no guarantee that the layers have been released by the compositor yet.
+
+ For non-remote ``<xul:browser>``'s, ``renderLayers`` is an alias for ``docShellIsActive``.
+
+``hasLayers``
+ For remote ``<xul:browser>``'s, this read-only property returns ``true`` if the compositor has layers for this tab, and ``false`` otherwise.
+
+ For non-remote ``<xul:browser>``'s, ``hasLayers`` returns the value for ``docShellIsActive``.
+
+``docShellIsActive``
+ For remote ``<xul:browser>``'s, setting ``docShellIsActive`` to ``true`` also sets ``renderLayers`` to true, and then sends a message to the content process to set its top-level docShell active state to ``true``. Similarly, setting ``docShellIsActive`` to ``false`` also sets ``renderLayers`` to false, and then sends a message to the content process to set its top-level docShell active state to ``false``.
+
+ For non-remote ``<xul:browser>``'s, ``docShellIsActive`` forwards to the ``isActive`` property on the ``<xul:browser>``'s top-level docShell.
+
+ Setting a docShell to be active causes the tab's visibilitychange event to fire to indicate that the tab has become visible. Media that was waiting to be played until the tab is selected will also begin to play.
+
+ An active docShell is also required in order to generate a print preview of the loaded document.
+
+
+Requirements
+============
+
+There are a number of requirements that the tab switcher must satisfy. In no particular order, they are:
+
+1. The switcher must be prepared to switch between any mixture of remote and non-remote tabs. Non-remote tabs include tabs pointed at about:addons, about:config, and others
+
+2. We want to avoid switching the toolbar state (for example, the URL bar input, security indicators, toolbar button states) until we are ready to show the layers of the tab that we're switching to
+
+3. Only one tab should appear to be selected in the tab strip at any given time
+
+4. We want to avoid switching keyboard focus to a selected tab until the layers for the tab are ready - but only if the user doesn’t change focus between the start and end of the async tab switch
+
+5. If the layers for a tab are not available after a certain amount of time, we should “complete” the tab switch by displaying the “tab switch spinner” - an animated spinner against a white background. This way, we at least show the user some activity, despite the fact that we don’t have the layers of the tab to show them
+
+6. The printing UI uses tabs to show print preview, which requires that the print-previewed tab is in the background and yet also have its docShell be "active" - a state that's usually reserved for the selected tab. See :ref:`async-tab-switcher.useful-properties`
+
+7. ``<xul:tab>``'s and ``<xul:browser>``'s might be created or destroyed at any time during an async tab switch
+
+8. It should be possible to render layers for a tab, despite it not having been set as active (this is used for :ref:`async-tab-switcher.warming`)
+
+Lifecycle
+=========
+
+Per window, an async tab switcher instance is only supposed to exist if one or more tabs still need to have their layers loaded or unloaded. This means that an async tab switcher instance might exist even though a tab switch appears to the user to have completed. This also means that an async tab switcher might continue to exist and handle a new tab switch if the user initiates that tab switch before some background tabs have had their layers unloaded.
+
+There’s only one async tab switcher at a time per window, and it’s owned by the ``<xul:tabbrowser>``.
+
+A ``<xul:tabbrowser>`` starts without an async tab switcher, and only once a tab switch (or warming) is initiated by the user is the switcher instantiated.
+
+Once the switcher determines that the tab that the user has requested is being shown, and all background tabs have been properly unloaded or destroyed, the async tab switcher cleans up and destroys itself.
+
+.. _async-tab-switcher.states:
+
+Tab states
+==========
+
+While the async tab switcher exists, it maps each ``<xul:tab>`` in the window to one of the following internal states:
+
+``STATE_UNLOADED``
+ Layers for this ``<xul:tab>`` are not being uploaded to the compositor, and we haven't requested that the tab start doing so. This tab is fully in the background.
+
+ When a tab is in ``STATE_UNLOADED``, this means that the associated ``<xul:browser>`` either does not exist, or will have its ``renderLayers`` and ``hasLayers`` properties both return ``false``.
+
+ If a tab is in this state, it must have either initialized there, or transitioned from ``STATE_UNLOADING``.
+
+ When logging states, this state is indicated by the ``unloaded`` string.
+
+``STATE_LOADING``
+ Layers for this ``<xul:tab>`` have not yet been reported as "received" by the compositor, but we've asked the tab to start rendering. This usually means that we want to switch to the tab, or at least to warm it up.
+
+ When a tab is in ``STATE_LOADING``, this means that the associated ``<xul:browser>`` will have its ``renderLayers`` property return ``true`` and its ``hasLayers`` property return ``false``.
+
+ If a tab is in this state, it must have either initialized there, or transitioned from ``STATE_UNLOADED``.
+
+ When logging states, this state is indicated by the ``loading`` string.
+
+``STATE_LOADED``
+ Layers for this ``<xul:tab>`` are available on the compositor and can be displayed. This means that the tab is either being shown to the user, or could be very quickly shown to the user.
+
+ If a tab is in this state, it must have either initialized there, or transitioned from ``STATE_LOADING``.
+
+ When a tab is in ``STATE_LOADED``, this means that the associated ``<xul:browser>`` will have its ``renderLayers`` and ``hasLayers`` properties both return ``true``.
+
+ When logging states, this state is indicated by the ``loaded`` string.
+
+``STATE_UNLOADING``
+ Layers for this ``<xul:tab>`` were at one time available on the compositor, but we've asked the tab to unload them to preserve memory. This usually means that we've switched away from this tab, or have stopped warming it up.
+
+ When a tab is in ``STATE_UNLOADING``, this means that the associated ``<xul:browser>`` will have its ``renderLayers`` property return ``false`` and its ``hasLayers`` property return ``true``.
+
+ If a tab is in this state, it must have either initialized there, or transitioned from ``STATE_LOADED``.
+
+ When logging states, this state is indicated by the ``unloading`` string.
+
+Having a tab render its layers is done by settings its state to ``STATE_LOADING``. Once the layers have been received, the switcher will automatically set the state to ``STATE_LOADED``. Similarly, telling a tab to stop rendering is done by settings its state to ``STATE_UNLOADING``. The switcher will automatically set the state to ``STATE_UNLOADED`` once the layers have fully unloaded.
+
+Stepping through a simple tab switch
+====================================
+
+In our simple scenario, suppose the user has a single browser window with two tabs: a tab at index **0** and a tab at index **1**. Both tabs are completed loaded, and **0** is currently selected and displaying its content.
+
+The user chooses to switch to tab **1**. An async tab switcher is instantiated, and it immediately attaches a number of event handlers to the window. Among them are handlers for the ``MozLayerTreeReady`` and ``MozLayerTreeCleared`` events.
+
+The switcher then creates an internal mapping from ``<xul:tab>>``'s to states. That mapping is:
+
+.. code-block:: none
+
+ // This is using the logging syntax laid out in the `Tab states` section.
+ 0:(loaded) 1:(unloaded)
+
+Be sure to refer to :ref:`async-tab-switcher.states` for an explanation of the terminology and :ref:`async-tab-switcher.logging` syntax for states.
+
+This last example translates to:
+
+ The tab at index **0**, is in ``STATE_LOADED`` and the tab at index **1** is in ``STATE_UNLOADED``.
+
+Now that initialization done, the switcher is asked to request **1**. It does this by putting **1** into ``STATE_LOADING`` and requesting that **1**'s layers be rendered. The new state mapping is:
+
+.. code-block:: none
+
+ 0:(loaded) 1:(loading)
+
+At this point, the user is still looking at tab **0**, and none of the UI is showing any visible indication of tab change.
+
+Now the switcher is waiting, so it goes back to the event loop. During this time, if any code were to ask the tabbrowser which tab is selected, it'd return **1**, since it's *logically* selected despite not being *visually* selected.
+
+Eventually, the layers for **1** are uploaded to the compositor, and the ``<xul:browser>`` for **1** fires its ``MozLayerTreeReady`` event. This is when the switcher changes its internal state again:
+
+.. code-block:: none
+
+ 0:(loaded) 1:(loaded)
+
+So now layers for both **0** and **1** are uploading and available on the compositor. At this point, the switcher updates the visual state of the browser, and flips the ``<xul:deck>`` to display **1**, and the user experiences the tab switch.
+
+The switcher isn't done, however. After a predefined amount of time (dictated by ``UNLOAD_DELAY``), tabs that aren't currently selected but in ``STATE_LOADED`` are put into ``STATE_UNLOADING``. Now the internal state looks like this:
+
+.. code-block:: none
+
+ 0:(unloading) 1:(loaded)
+
+Having requested that **0** go into ``STATE_UNLOADING``, the switcher returns back to the event loop. The user, meanwhile, continues to use ``1``.
+
+Eventually, the layers for **0** are cleared from the compositor, and the ``<xul:browser>`` for **0** fires its ``MozLayerTreeCleared`` event. This is when the switcher changes its internal state once more:
+
+.. code-block:: none
+
+ 0:(unloaded) 1:(loaded)
+
+The tab at **0** is now in ``STATE_UNLOADED``. Since the last requested tab **1** is in ``STATE_LOADED`` and all other background tabs are in ``STATE_UNLOADED``, the switcher decides its work is done. It deregisters its event handlers, and then destroys itself.
+
+.. _async-tab-switcher.unloading-background:
+
+Unloading background tabs
+=========================
+
+While an async tab switcher exists, it will periodically scan the window for tabs that are in ``STATE_LOADED`` but are also in the background. These tabs will then be put into ``STATE_UNLOADING``. Only once all background tabs have settled into the ``STATE_UNLOADED`` state are the background tabs considered completely cleared.
+
+The background scanning interval is ``UNLOAD_DELAY``, in milliseconds.
+
+Perceived performance optimizations
+===================================
+
+We use a few tricks and optimizations to help improve the perceived performance of tab switches.
+
+1. Sometimes users switch between the same tabs quickly. We want to optimize for this case by not releasing the layers for tabs until some time has gone by. That way, quick switching just resolves in a re-composite in the compositor, as opposed to a full re-paint and re-upload of the layers from a remote tab’s content process.
+
+2. When a tab hasn’t ever been seen before, and is still in the process of loading (right now, dubiously checked by looking for the “busy” attribute on the ``<xul:tab>``) we show a blank content area until its layers are finally ready. The idea here is to shift perceived lag from the async tab switcher to the network by showing the blank space instead of the tab switch spinner.
+
+3. “Warming” is a nascent optimization that will allow us to pre-emptively render and cache the layers for tabs that we think the user is likely to switch to soon. After a timeout (``browser.tabs.remote.warmup.unloadDelayMs``), “warmed” tabs that aren’t switched to have their layers unloaded and cleared from the cache.
+
+4. On platforms that support ``occlusionstatechange`` events (as of this writing, only macOS) and ``sizemodechange`` events (Windows, macOS and Linux), we stop rendering the layers for the currently selected tab when the window is minimized or fully occluded by another window.
+
+5. Based on the browser.tabs.remote.tabCacheSize pref, we keep recently used tabs'
+layers around to speed up tab switches by avoiding the round trip to the content
+process. This uses a simple array (``_tabLayerCache``) inside tabbrowser.js, which
+we examine when determining if we want to unload a tab's layers or not. This is still
+experimental as of Nightly 62.
+
+.. _async-tab-switcher.warming:
+
+Warming
+=======
+
+Tab warming allows the browser to proactively render and upload layers to the compositor for tabs that the user is likely to switch to. The simplest example is when a user's mouse cursor is hovering over a tab. When this occurs, the async tab switcher is told to put that tab into a warming list, and to set its state to ``STATE_LOADING``, even though the user hasn't yet clicked on it.
+
+Warming a tab queues up a timer to unload background tabs (if no such timer already exists), which will clear out the warmed tab if the user doesn't eventually click on it. The unload will occur even if the user continues to hover the tab.
+
+If the user does happen to click on the warmed tab, the tab can be in either one of two states:
+
+``STATE_LOADING``
+ In this case, the user requested the tab switch before the layers were rendered and received by the compositor. We'll at least have shaved off the time between warming and selection to display the tab's contents to the user.
+
+``STATE_LOADED``
+ In this case, the user requested the tab switch after the layers had been rendered and received by the compositor. We can switch to the tab immediately.
+
+Warming is controlled by the following preferences:
+
+``browser.tabs.remote.warmup.enabled``
+ Whether or not the warming optimization is enabled.
+
+``browser.tabs.remote.warmup.maxTabs``
+ The maximum number of tabs that can be warming simultaneously. If the number of warmed tabs exceeds this amount, all background tabs are unloaded (see :ref:`async-tab-switcher.unloading-background`).
+
+``browser.tabs.remote.warmup.unloadDelayMs``
+ The amount of time to wait between the first tab being warmed, and unloading all background tabs (see :ref:`async-tab-switcher.unloading-background`).
+
+.. _async-tab-switcher.logging:
+
+Logging
+=======
+
+The async tab switcher has some logging capabilities that make it easier to debug and reason about its behaviour. Setting the hidden ``browser.tabs.remote.logSwitchTiming`` pref to true will put logging into the Browser Console.
+
+Alternatively, setting the ``useDumpForLogging`` property to true within the source code of the tab switcher will dump those logs to stdout.
diff --git a/browser/base/content/docs/tabbrowser/index.rst b/browser/base/content/docs/tabbrowser/index.rst
new file mode 100644
index 0000000000..ee0882c092
--- /dev/null
+++ b/browser/base/content/docs/tabbrowser/index.rst
@@ -0,0 +1,14 @@
+.. _tabbrowser:
+
+===================
+tabbrowser
+===================
+
+One ``<xul:tabbrowser>`` exists per browser window, and is responsible for displaying and managing the contents of a windows tabs. As the browser has evolved, the responsibilities of ``<xul:tabbrowser>`` have also grown.
+
+At this point, ``<xul:tabbrowser>`` is arguably one of the largest and most complex pieces of code used by the browser's user interface.
+
+.. toctree::
+ :maxdepth: 1
+
+ async-tab-switcher
diff --git a/browser/base/content/global-scripts.inc b/browser/base/content/global-scripts.inc
new file mode 100644
index 0000000000..6be9565767
--- /dev/null
+++ b/browser/base/content/global-scripts.inc
@@ -0,0 +1,26 @@
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# JS files which are needed by browser.xhtml but no other top level windows to
+# support MacOS specific features should be loaded directly from browser.xhtml
+# rather than this file.
+
+# If you update this list, you may need to add a mapping within the following
+# file so that ESLint works correctly:
+# tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
+
+<script type="text/javascript">
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+Services.scriptloader.loadSubScript("chrome://browser/content/browser.js", this);
+Services.scriptloader.loadSubScript("chrome://browser/content/browser-places.js", this);
+Services.scriptloader.loadSubScript("chrome://global/content/globalOverlay.js", this);
+Services.scriptloader.loadSubScript("chrome://global/content/editMenuOverlay.js", this);
+Services.scriptloader.loadSubScript("chrome://browser/content/utilityOverlay.js", this);
+if (AppConstants.platform == "macosx") {
+ Services.scriptloader.loadSubScript("chrome://global/content/macWindowMenu.js", this);
+}
+
+</script>
diff --git a/browser/base/content/hiddenWindowMac.xhtml b/browser/base/content/hiddenWindowMac.xhtml
new file mode 100644
index 0000000000..298c989b4e
--- /dev/null
+++ b/browser/base/content/hiddenWindowMac.xhtml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+# -*- Mode: 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/.
+#define HIDDEN_WINDOW
+
+<?xml-stylesheet href="chrome://browser/skin/webRTC-menubar-indicator.css" type="text/css"?>
+
+<!DOCTYPE window [
+#include browser-doctype.inc
+]>
+
+<window id="main-window"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ data-l10n-sync="true">
+
+#include macWindow.inc.xhtml
+
+<!-- Dock menu -->
+<popupset>
+ <menupopup id="menu_mac_dockmenu">
+ <!-- The command cannot be cmd_newNavigator because we need to activate
+ the application. -->
+ <menuitem label="&newNavigatorCmd.label;" oncommand="OpenBrowserWindowFromDockMenu();"
+ id="macDockMenuNewWindow" />
+ <menuitem label="&newPrivateWindow.label;" oncommand="OpenBrowserWindowFromDockMenu({private: true});"
+ id="macDockMenuNewPrivateWindow" />
+ </menupopup>
+</popupset>
+
+</window>
diff --git a/browser/base/content/history-swipe-arrow.svg b/browser/base/content/history-swipe-arrow.svg
new file mode 100644
index 0000000000..9c0c0a5672
--- /dev/null
+++ b/browser/base/content/history-swipe-arrow.svg
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 width="256" height="512" xmlns="http://www.w3.org/2000/svg">
+ <ellipse ry="256" rx="256" cy="256" cx="0" stroke-width="0" stroke="#000" fill="#38383d"/>
+ <path d="m181.335236,247.749695l-96.549957,0l35.774963,-35.774994a8.33333,8.33333 0 0 0 -11.783325,-11.783325l-49.999969,49.999954a8.33333,8.33333 0 0 0 0,11.782944l49.999969,50a8.33333,8.33333 0 0 0 11.783325,-11.783325l-35.774963,-35.774994l96.549957,0a8.33333,8.33333 0 0 0 0,-16.66626z" fill="rgba(249, 249, 250, .8)"/>
+</svg> \ No newline at end of file
diff --git a/browser/base/content/logos/etp-mobile.svg b/browser/base/content/logos/etp-mobile.svg
new file mode 100644
index 0000000000..dfb6eed9c5
--- /dev/null
+++ b/browser/base/content/logos/etp-mobile.svg
@@ -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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="57">
+ <defs>
+ <linearGradient id="a" x1="13.375%" x2="86.625%" y1="0%" y2="100%">
+ <stop offset="0%" stop-color="#9059FF"/>
+ <stop offset="100%" stop-color="#0250BB"/>
+ </linearGradient>
+ </defs>
+ <path fill="context-fill" fill-opacity=".8" fill-rule="evenodd" d="M43.436 34.835c.173 1.234.01 1.996-.491 2.286L23.723 48.218c-.749.433-1.745.113-2.225-.718L3.975 17.148c-.479-.828-.258-1.853.49-2.285L23.688 3.765c.751-.434 1.747-.11 2.225.718l17.524 30.352zM33.89 49.232l4.619-2.667-1.616-2.8-4.619 2.668 1.616 2.799zM2.738 11.289C.191 12.76-.43 16.453 1.351 19.538l19.382 33.571c1.78 3.085 5.29 4.393 7.838 2.923l18.487-10.674c2.547-1.47 3.169-5.164 1.388-8.249L29.063 3.538C27.283.453 23.773-.855 21.226.615L2.738 11.29z"/>
+ <path fill="url(#a)" d="M15.763 23.5c1.117 1.935 1.84 3.32 2.268 3.93a10.404 10.404 0 0 0 4.784 4.195 5.807 5.807 0 0 0 4.095.184 5.804 5.804 0 0 0 1.888-3.638 10.411 10.411 0 0 0-1.24-6.241c-.314-.676-1.152-1.995-2.27-3.93l-5.233 1.935-4.292 3.566zm12.535 10.713l-.141.063a8.585 8.585 0 0 1-6.446-.132 12.733 12.733 0 0 1-5.778-5.057c-.667-.946-1.933-3.14-2.638-4.36a2.537 2.537 0 0 1 .566-3.22l4.798-3.99 5.852-2.16a2.537 2.537 0 0 1 3.074 1.12c.704 1.22 1.97 3.413 2.452 4.467a12.732 12.732 0 0 1 1.49 7.533 8.585 8.585 0 0 1-3.108 5.648l-.12.088zm-7.197-12.466l-2.683 2.229c.659 1.115 1.174 1.949 1.45 2.338a7.927 7.927 0 0 0 3.77 3.427 3.692 3.692 0 0 0 2.196.229l.01-.006-4.743-8.217z"/>
+</svg>
diff --git a/browser/base/content/logos/lockwise.svg b/browser/base/content/logos/lockwise.svg
new file mode 100644
index 0000000000..d3e354cdc1
--- /dev/null
+++ b/browser/base/content/logos/lockwise.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="44.25" x2="18.88" y1="55.37" y2="11.44"><stop offset="0" stop-color="#ff980e"/><stop offset=".11" stop-color="#ff851b"/><stop offset=".57" stop-color="#ff3750"/><stop offset=".8" stop-color="#f92261"/><stop offset="1" stop-color="#f5156c"/></linearGradient><linearGradient id="b" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="44.12" x2="23.37" y1="62.59" y2="13.68"><stop offset="0" stop-color="#fff261" stop-opacity=".8"/><stop offset=".06" stop-color="#fff261" stop-opacity=".68"/><stop offset=".19" stop-color="#fff261" stop-opacity=".48"/><stop offset=".31" stop-color="#fff261" stop-opacity=".31"/><stop offset=".42" stop-color="#fff261" stop-opacity=".17"/><stop offset=".53" stop-color="#fff261" stop-opacity=".08"/><stop offset=".63" stop-color="#fff261" stop-opacity=".02"/><stop offset=".72" stop-color="#fff261" stop-opacity="0"/></linearGradient><linearGradient id="c" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="54.08" x2="54.08" y1="8.93" y2="42.2"><stop offset="0" stop-color="#0090ed"/><stop offset=".5" stop-color="#9059ff"/><stop offset=".81" stop-color="#b833e1"/></linearGradient><linearGradient id="d" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="16.46" x2="37.88" y1="7.08" y2="43.53"><stop offset=".02" stop-color="#0090ed"/><stop offset=".49" stop-color="#9059ff"/><stop offset="1" stop-color="#b833e1"/></linearGradient><linearGradient id="e" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="19.25" x2="6.77" y1="21.12" y2="33.61"><stop offset=".14" stop-color="#592acb" stop-opacity="0"/><stop offset=".33" stop-color="#542bc8" stop-opacity=".03"/><stop offset=".53" stop-color="#462fbf" stop-opacity=".11"/><stop offset=".74" stop-color="#2f35b1" stop-opacity=".25"/><stop offset=".95" stop-color="#0f3d9c" stop-opacity=".44"/><stop offset="1" stop-color="#054096" stop-opacity=".5"/></linearGradient><linearGradient id="f" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="57" x2="50.71" y1="34.92" y2="24.03"><stop offset="0" stop-color="#722291" stop-opacity=".5"/><stop offset=".5" stop-color="#b833e1" stop-opacity="0"/></linearGradient><linearGradient id="g" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="43.72" x2="36.42" y1="19.33" y2="11.1"><stop offset="0" stop-color="#054096" stop-opacity=".5"/><stop offset=".03" stop-color="#0f3d9c" stop-opacity=".44"/><stop offset=".17" stop-color="#2f35b1" stop-opacity=".25"/><stop offset=".3" stop-color="#462fbf" stop-opacity=".11"/><stop offset=".43" stop-color="#542bc8" stop-opacity=".03"/><stop offset=".56" stop-color="#592acb" stop-opacity="0"/></linearGradient><path d="M57.45 25.11A218.35 218.35 0 0 0 38.82 6.48a10.81 10.81 0 0 0-13.77 0A219.81 219.81 0 0 0 6.42 25.11a10.83 10.83 0 0 0 0 13.78 218.35 218.35 0 0 0 18.63 18.63 10.84 10.84 0 0 0 13.8 0c3.43-3.1 6.56-6.09 9.57-9.15a3.1 3.1 0 0 0-.24-4.27l-9.25-8.63a10.62 10.62 0 0 0 3.56-8.4 10.78 10.78 0 0 0-10.08-10.26 10.7 10.7 0 0 0-8.37 18c.21.22.42.42.64.62l-3.35 3a2.7 2.7 0 0 0 3.61 4l3.7-3.35.1-.1a5.07 5.07 0 0 0 1.48-3.79 5.2 5.2 0 0 0-1.78-3.71 5.3 5.3 0 1 1 7.47-.63 4.24 4.24 0 0 1-.64.63 5.2 5.2 0 0 0-1.83 3.73A5 5 0 0 0 34.92 39l.06.07 7.77 7.27c-2.38 2.36-4.86 4.7-7.5 7.09a5.51 5.51 0 0 1-6.61 0 214 214 0 0 1-18.2-18.19 5.55 5.55 0 0 1 0-6.62 214 214 0 0 1 18.2-18.19 5.51 5.51 0 0 1 6.61 0 213.86 213.86 0 0 1 18.19 18.23 5.54 5.54 0 0 1 0 6.61c-.93 1-1.86 2.1-2.8 3.06a2.7 2.7 0 1 0 4 3.65c.92-1 1.87-2 2.8-3.12a10.84 10.84 0 0 0 .01-13.75z" fill="url(#a)"/><path d="M57.56 25.1A218.7 218.7 0 0 0 38.91 6.46a10.82 10.82 0 0 0-13.79 0A217.26 217.26 0 0 0 6.48 25.11a10.82 10.82 0 0 0 0 13.79 217.26 217.26 0 0 0 18.65 18.64 10.85 10.85 0 0 0 13.81 0c3.43-3.09 6.56-6.09 9.58-9.15a3.11 3.11 0 0 0-.24-4.28L39 35.45a10.62 10.62 0 0 0 3.56-8.4A10.79 10.79 0 0 0 32.5 16.79a10.71 10.71 0 0 0-8.38 18.05c.2.21.41.42.63.61l-3.35 3a2.71 2.71 0 0 0 3.62 4l3.7-3.36.1-.09a5.08 5.08 0 0 0 1.48-3.8 5.2 5.2 0 0 0-1.78-3.71 5.3 5.3 0 1 1 7.48-.58 5.43 5.43 0 0 1-.64.63 5.2 5.2 0 0 0-1.83 3.73A5.08 5.08 0 0 0 35 39.05l.07.06 7.77 7.29c-2.38 2.36-4.86 4.7-7.5 7.09a5.54 5.54 0 0 1-6.63 0A217.3 217.3 0 0 1 10.5 35.28a5.55 5.55 0 0 1 0-6.62 214.33 214.33 0 0 1 18.21-18.21 5.52 5.52 0 0 1 6.62 0 214.33 214.33 0 0 1 18.21 18.21 5.52 5.52 0 0 1 0 6.62c-.93 1-1.86 2.1-2.8 3.06a2.7 2.7 0 0 0 4 3.65c.93-1 1.88-2.05 2.8-3.13a10.84 10.84 0 0 0 .02-13.76z" fill="url(#b)"/><path d="M53.41 28.69a5.51 5.51 0 0 1 0 6.61c-.93 1.05-1.86 2.1-2.8 3.06a2.7 2.7 0 0 0 4 3.65c.92-1 1.87-2 2.8-3.13 3.34-3.73-4-10.19-4-10.19z" fill="url(#c)"/><path d="M42.75 46.38c-2.38 2.36-4.86 4.7-7.5 7.09a5.51 5.51 0 0 1-6.61 0 214 214 0 0 1-18.2-18.19 5.55 5.55 0 0 1 0-6.62l-1.22 1.4a9 9 0 0 0 .15 12.08 216.71 216.71 0 0 0 15.68 15.38 10.84 10.84 0 0 0 13.8 0c1.95-1.77 4.12-3.8 6.07-5.68a2.35 2.35 0 0 0 .08-3.35l-.06-.07z" fill="url(#d)"/><path d="M9.39 42.17c2 2.21 4.08 4.33 6.17 6.4l.83-1.35c.7-1.12 1.4-2.21 2.15-3.3-2.69-2.73-5.36-5.56-8.08-8.6a5.55 5.55 0 0 1 0-6.62l-1.22 1.4a9 9 0 0 0 .13 12z" fill="url(#e)" opacity=".9"/><path d="M53.41 28.69a5.51 5.51 0 0 1 0 6.61c-.93 1.05-1.86 2.1-2.8 3.06a2.7 2.7 0 0 0 4 3.65c.92-1 1.87-2 2.8-3.13 3.34-3.73-4-10.19-4-10.19z" fill="url(#f)"/><path d="M44.89 48.42l-2.14-2c-2.38 2.36-4.86 4.7-7.5 7.09a5.4 5.4 0 0 1-4.25 1v5.43c.31 0 .62.05.93.05a10.45 10.45 0 0 0 6.91-2.49c2-1.77 4.12-3.8 6.07-5.68A2.35 2.35 0 0 0 45 48.5a.6.6 0 0 1-.11-.08z" fill="url(#g)" opacity=".9"/></svg>
diff --git a/browser/base/content/logos/monitor.svg b/browser/base/content/logos/monitor.svg
new file mode 100644
index 0000000000..0a734f4f50
--- /dev/null
+++ b/browser/base/content/logos/monitor.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="44.39" x2="17.83" y1="55.11" y2="9.1"><stop offset="0" stop-color="#ff980e"/><stop offset=".21" stop-color="#ff7139"/><stop offset=".36" stop-color="#ff5854"/><stop offset=".46" stop-color="#ff4f5e"/><stop offset=".69" stop-color="#ff3750"/><stop offset=".86" stop-color="#f92261"/><stop offset="1" stop-color="#f5156c"/></linearGradient><linearGradient id="b" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="44.39" x2="17.83" y1="55.11" y2="9.1"><stop offset="0" stop-color="#fff44f" stop-opacity=".8"/><stop offset=".75" stop-color="#fff44f" stop-opacity="0"/></linearGradient><linearGradient id="c" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="44.49" x2="44.49" y1="3.82" y2="58.55"><stop offset="0" stop-color="#3a8ee6"/><stop offset=".24" stop-color="#5c79f0"/><stop offset=".63" stop-color="#9059ff"/><stop offset="1" stop-color="#c139e6"/></linearGradient><linearGradient id="d" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="35.2" x2="59.52" y1="60.58" y2="36.25"><stop offset="0" stop-color="#6e008b" stop-opacity=".5"/><stop offset=".5" stop-color="#c846cb" stop-opacity="0"/></linearGradient><linearGradient id="e" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="59.67" x2="45.66" y1="30.62" y2="16.61"><stop offset=".14" stop-color="#6a2bea" stop-opacity="0"/><stop offset=".3" stop-color="#662ce6" stop-opacity=".09"/><stop offset=".47" stop-color="#592fdb" stop-opacity=".19"/><stop offset=".64" stop-color="#4534c9" stop-opacity=".29"/><stop offset=".81" stop-color="#283baf" stop-opacity=".39"/><stop offset=".99" stop-color="#03448d" stop-opacity=".49"/><stop offset="1" stop-color="#00458b" stop-opacity=".5"/></linearGradient><linearGradient id="f" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="38.67" x2="41.95" y1="21.69" y2="17.77"><stop offset="0" stop-color="#960e18" stop-opacity=".6"/><stop offset=".17" stop-color="#a91522" stop-opacity=".47"/><stop offset=".51" stop-color="#d9283c" stop-opacity=".19"/><stop offset=".75" stop-color="#ff3750" stop-opacity="0"/></linearGradient><path d="m54.55 15.53-5.71-3.26-13-7.46-.41-.23a6.88 6.88 0 0 0 -6.88 0l-.42.23-18.28 10.48-.41.24a6.83 6.83 0 0 0 -3.44 5.92v21.89a6.86 6.86 0 0 0 3.44 5.92l18.7 10.74a2.75 2.75 0 0 0 1.44.38 2.91 2.91 0 0 0 2.49-1.42 2.83 2.83 0 0 0 -1.07-3.96l-18.29-10.44a2 2 0 0 1 -1-1.69v-20.95a2 2 0 0 1 1-1.69l3.29-1.85 15-8.63a2 2 0 0 1 2 0l18.27 10.48a2 2 0 0 1 1 1.69v20.95a1.94 1.94 0 0 1 -1 1.69l-6.18 3.54-3.09-4.71a15 15 0 1 0 -5 2.92l4.75 7.15a1.91 1.91 0 0 0 .24.32 3.18 3.18 0 0 0 .33.3.27.27 0 0 0 .08.07l.41.25h.1a1.3 1.3 0 0 0 .39.14.14.14 0 0 0 .09 0 2.34 2.34 0 0 0 .45.07.3.3 0 0 1 .13 0h.39a.23.23 0 0 0 .11 0 2.57 2.57 0 0 0 .48-.11.26.26 0 0 0 .12 0l.37-.16h.08l8.94-5.13a6.83 6.83 0 0 0 3.54-5.9v-21.86a6.75 6.75 0 0 0 -3.45-5.92zm-31.74 16.85a9.18 9.18 0 1 1 9.19 9.11 9.15 9.15 0 0 1 -9.19-9.11z" fill="url(#a)"/><path d="m54.55 15.53-5.71-3.26-13-7.46-.41-.23a6.88 6.88 0 0 0 -6.88 0l-.42.23-18.28 10.48-.41.24a6.83 6.83 0 0 0 -3.44 5.92v21.89a6.86 6.86 0 0 0 3.44 5.92l18.7 10.74a2.75 2.75 0 0 0 1.44.38 2.91 2.91 0 0 0 2.49-1.42 2.83 2.83 0 0 0 -1.07-3.96l-18.29-10.44a2 2 0 0 1 -1-1.69v-20.95a2 2 0 0 1 1-1.69l3.29-1.85 15-8.63a2 2 0 0 1 2 0l18.27 10.48a2 2 0 0 1 1 1.69v20.95a1.94 1.94 0 0 1 -1 1.69l-6.18 3.54-3.09-4.71a15 15 0 1 0 -5 2.92l4.75 7.15a1.91 1.91 0 0 0 .24.32 3.18 3.18 0 0 0 .33.3.27.27 0 0 0 .08.07l.41.25h.1a1.3 1.3 0 0 0 .39.14.14.14 0 0 0 .09 0 2.34 2.34 0 0 0 .45.07.3.3 0 0 1 .13 0h.39a.23.23 0 0 0 .11 0 2.57 2.57 0 0 0 .48-.11.26.26 0 0 0 .12 0l.37-.16h.08l8.94-5.13a6.83 6.83 0 0 0 3.54-5.9v-21.86a6.75 6.75 0 0 0 -3.45-5.92zm-31.74 16.85a9.18 9.18 0 1 1 9.19 9.11 9.15 9.15 0 0 1 -9.19-9.11z" fill="url(#b)"/><path d="m54.55 15.53-5.71-3.26-7.94-4.55a6.11 6.11 0 0 0 -5.9-.08l-4 2.11a2 2 0 0 1 2 0l18.3 10.48a1.94 1.94 0 0 1 1 1.69v20.95a1.93 1.93 0 0 1 -1 1.69l-6.23 3.54 1 1.55a4.05 4.05 0 0 0 5.41 1.35l3.06-1.75a6.82 6.82 0 0 0 3.46-5.91v-21.9a6.75 6.75 0 0 0 -3.45-5.91z" fill="url(#c)"/><path d="m52.26 21.92v10.16h5.74v-10.64a6.83 6.83 0 0 0 -3.44-5.92l-5.7-3.26-8-4.55a6.14 6.14 0 0 0 -5.86-.09l-4 2.12a2 2 0 0 1 2 0l18.27 10.48a2 2 0 0 1 .99 1.7z" fill="url(#d)"/><path d="m52.26 33.72v9.14a2 2 0 0 1 -1 1.69l-6.19 3.54 1 1.55a4.06 4.06 0 0 0 5.42 1.36l3.06-1.75a6.84 6.84 0 0 0 3.45-5.92v-9.63h-5.74z" fill="url(#e)" opacity=".9"/><path d="m41.17 32.38a9.18 9.18 0 1 0 -9.17 9.11 9.14 9.14 0 0 0 9.17-9.11z" fill="none"/><path d="m44.64 47.43-2.68-4a15 15 0 0 1 -4.96 2.88l2.86 4.31c1.63-1.02 3.22-2.08 4.78-3.19z" fill="url(#f)" opacity=".9"/></svg>
diff --git a/browser/base/content/logos/proxy-dark.svg b/browser/base/content/logos/proxy-dark.svg
new file mode 100644
index 0000000000..f2f4d327b4
--- /dev/null
+++ b/browser/base/content/logos/proxy-dark.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 width="253" height="262" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="79.078%" y1="61.129%" x2="52.439%" y2="48.719%" id="a"><stop stop-color="#054096" stop-opacity=".5" offset="0%"/><stop stop-color="#173BA1" stop-opacity=".442" offset="9.995%"/><stop stop-color="#3434B3" stop-opacity=".329" offset="29.49%"/><stop stop-color="#482EC1" stop-opacity=".217" offset="48.88%"/><stop stop-color="#552BC8" stop-opacity=".107" offset="67.97%"/><stop stop-color="#592ACB" stop-opacity="0" offset="86.4%"/></linearGradient><linearGradient x1="-.008%" y1="50.069%" x2="100.124%" y2="50.069%" id="b"><stop stop-color="#9059FF" offset="0%"/><stop stop-color="#F770FF" offset="100%"/></linearGradient><linearGradient x1="10.951%" y1="41.295%" x2="102.369%" y2="60.901%" id="c"><stop stop-color="#54FFBD" offset=".103%"/><stop stop-color="#0DF" offset="100%"/></linearGradient></defs><g fill="none"><path d="M226.4 168.3H119.7c-3.1 0-5.7 2.5-5.7 5.7v17c0 25.1 20.4 45.5 45.5 45.5h59c18.9 0 34.1-15.3 34.1-34.1v-14.9c.1-8.6-8.4-19.2-26.2-19.2z" fill="#008787"/><path d="M112.4.3H5.7C2.6.3 0 2.8 0 6v17c0 25.1 20.4 45.5 45.5 45.5h59c18.9 0 34.1-15.3 34.1-34.1V19.5c.1-8.6-8.4-19.2-26.2-19.2z" opacity=".9" fill="url(#a)" transform="translate(114 168)"/><path d="M219 55.1L125.9 2.9c-6.7-3.7-14.8-3.7-21.4 0L11.4 55.1C4.5 59 .2 66.3.2 74.2v104.4c0 7.9 4.3 15.2 11.2 19.1l93.1 52.2c3.3 1.9 7 2.8 10.7 2.8 3.7 0 7.4-.9 10.7-2.8l93.1-52.2c6.9-3.9 11.2-11.2 11.2-19.1V74.2c0-7.9-4.3-15.2-11.2-19.1z" fill="#20123A"/><path d="M187.2 80.3l-67.8-38.7c-2.6-1.5-5.9-1.5-8.5 0L43 80.3c-2.7 1.5-4.3 4.4-4.3 7.4V165c0 3.1 1.6 5.9 4.3 7.4l67.8 38.7c1.3.7 2.8 1.1 4.2 1.1 1.4 0 2.9-.4 4.2-1.1l67.8-38.7c2.7-1.5 4.3-4.4 4.3-7.4V87.7c.2-3-1.4-5.8-4.1-7.4zm-12.8 69.8l-50.7-28.7V63.8l50.7 28.9v57.4zm-59.3 43.8l-59.3-33.8V92.7l50.7-28.9v62.3c0 3.6 2.2 6.6 5.3 7.9.5.6 1.2 1.1 1.9 1.5l52.2 29.5-50.8 28.9z" fill="url(#b)"/><path d="M140 6.1H42c-23.2 0-42 18.8-42 42v20.6c0 3.1 2.5 5.7 5.7 5.7h98.1c23.2 0 42-18.8 42-42V.5c-.1 3.1-2.6 5.6-5.8 5.6z" fill="url(#c)" transform="translate(107 187)"/><path d="M150.8 226.9c2.1-1.2 3.2-3.1 3.2-5.7 0-5.1-3.5-7.9-9.8-7.9h-11.7v28.2h11.8c6.3 0 10.1-2.8 10.1-8.4.1-2.9-1.2-5-3.6-6.2zm-12.8-8.8h6.4c2.9 0 4.2 1.2 4.2 3.2 0 1.9-1.2 3.4-4.1 3.4H138v-6.6zm6.4 18.5H138v-7h6.2c3.4 0 4.8 1.3 4.8 3.5 0 2.1-1.6 3.5-4.6 3.5zm15.3 4.9h19v-5.2h-13.5v-6.4h13.5v-5.1h-13.5v-6.4h13.5v-5.1h-19zm44.7-28.2h-21v5h7.8v23.2h5.4v-23.2h7.8zm14.6 0h-5.4L203 241.5h5.6l1.9-5.1h11.8l1.9 5.1h5.6L219 213.3zm-6.8 18.1l4.1-11.2 4.1 11.2h-8.2z" fill="#20123A"/></g></svg>
diff --git a/browser/base/content/logos/proxy-light.svg b/browser/base/content/logos/proxy-light.svg
new file mode 100644
index 0000000000..01fc54a758
--- /dev/null
+++ b/browser/base/content/logos/proxy-light.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 width="253" height="262" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="79.077%" y1="61.129%" x2="52.439%" y2="48.719%" id="a"><stop stop-color="#054096" stop-opacity=".5" offset="0%"/><stop stop-color="#173BA1" stop-opacity=".442" offset="9.995%"/><stop stop-color="#3434B3" stop-opacity=".329" offset="29.49%"/><stop stop-color="#482EC1" stop-opacity=".217" offset="48.88%"/><stop stop-color="#552BC8" stop-opacity=".107" offset="67.97%"/><stop stop-color="#592ACB" stop-opacity="0" offset="86.4%"/></linearGradient><linearGradient x1="-.075%" y1="50.017%" x2="99.986%" y2="50.017%" id="b"><stop stop-color="#9059FF" offset="0%"/><stop stop-color="#F770FF" offset="100%"/></linearGradient><linearGradient x1="10.951%" y1="41.295%" x2="102.369%" y2="60.901%" id="c"><stop stop-color="#54FFBD" offset=".103%"/><stop stop-color="#0DF" offset="100%"/></linearGradient></defs><g fill="none"><path d="M226.4 168.3H119.7c-3.1 0-5.7 2.5-5.7 5.7v17c0 25.1 20.4 45.5 45.5 45.5h59c18.9 0 34.1-15.3 34.1-34.1v-14.9c.1-8.6-8.4-19.2-26.2-19.2z" fill="#008787"/><path d="M112.4.3H5.7C2.6.3 0 2.8 0 6v17c0 25.1 20.4 45.5 45.5 45.5h59c18.9 0 34.1-15.3 34.1-34.1V19.5c.1-8.6-8.4-19.2-26.2-19.2z" opacity=".9" fill="url(#a)" transform="translate(114 168)"/><path d="M219 55.1L125.9 2.9c-6.7-3.7-14.8-3.7-21.4 0L11.4 55.1C4.5 59 .2 66.3.2 74.2v104.4c0 7.9 4.3 15.2 11.2 19.1l93.1 52.2c3.3 1.9 7 2.8 10.7 2.8 3.7 0 7.4-.9 10.7-2.8l93.1-52.2c6.9-3.9 11.2-11.2 11.2-19.1V74.2c0-7.9-4.3-15.2-11.2-19.1z" fill="url(#b)"/><path d="M187.2 80.3l-67.8-38.7c-2.6-1.5-5.9-1.5-8.5 0L43 80.3c-2.7 1.5-4.3 4.4-4.3 7.4V165c0 3.1 1.6 5.9 4.3 7.4l67.8 38.7c1.3.7 2.8 1.1 4.2 1.1 1.4 0 2.9-.4 4.2-1.1l67.8-38.7c2.7-1.5 4.3-4.4 4.3-7.4V87.7c.2-3-1.4-5.8-4.1-7.4zm-12.8 69.8l-50.7-28.7V63.8l50.7 28.9v57.4zm-59.3 43.8l-59.3-33.8V92.7l50.7-28.9v62.3c0 3.6 2.2 6.6 5.3 7.9.5.6 1.2 1.1 1.9 1.5l52.2 29.5-50.8 28.9z" fill="#20133A"/><path d="M140 6.1H42c-23.2 0-42 18.8-42 42v20.6c0 3.1 2.5 5.7 5.7 5.7h98.1c23.2 0 42-18.8 42-42V.5c-.1 3.1-2.6 5.6-5.8 5.6z" fill="url(#c)" transform="translate(107 187)"/><path d="M150.8 226.9c2.1-1.2 3.2-3.1 3.2-5.7 0-5.1-3.5-7.9-9.8-7.9h-11.7v28.2h11.8c6.3 0 10.1-2.8 10.1-8.4.1-2.9-1.2-5-3.6-6.2zm-12.8-8.8h6.4c2.9 0 4.2 1.2 4.2 3.2 0 1.9-1.2 3.4-4.1 3.4H138v-6.6zm6.4 18.5H138v-7h6.2c3.4 0 4.8 1.3 4.8 3.5 0 2.1-1.6 3.5-4.6 3.5zm15.3 4.9h19v-5.2h-13.5v-6.4h13.5v-5.1h-13.5v-6.4h13.5v-5.1h-19zm44.7-28.2h-21v5h7.8v23.2h5.4v-23.2h7.8zm14.6 0h-5.4L203 241.5h5.6l1.9-5.1h11.8l1.9 5.1h5.6L219 213.3zm-6.8 18.1l4.1-11.2 4.1 11.2h-8.2z" fill="#20123A"/></g></svg>
diff --git a/browser/base/content/logos/send.svg b/browser/base/content/logos/send.svg
new file mode 100644
index 0000000000..f754d727e8
--- /dev/null
+++ b/browser/base/content/logos/send.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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="80" height="80"><defs><linearGradient id="a" x1="57.082" y1="5.474" x2="18.997" y2="71.439" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff9640"/><stop offset=".6" stop-color="#fc4055"/><stop offset="1" stop-color="#e31587"/></linearGradient><linearGradient id="b" x1="57.082" y1="5.474" x2="18.997" y2="71.439" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff36e" stop-opacity=".8"/><stop offset=".094" stop-color="#fff36e" stop-opacity=".699"/><stop offset=".752" stop-color="#fff36e" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="48.99" y1="47.048" x2="66.606" y2="16.537" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0090ed"/><stop offset=".386" stop-color="#5b6df8"/><stop offset=".629" stop-color="#9059ff"/><stop offset="1" stop-color="#b833e1"/></linearGradient><linearGradient id="d" x1="48.305" y1="37.697" x2="75.234" y2="44.176" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#054096" stop-opacity=".5"/><stop offset=".054" stop-color="#0f3d9c" stop-opacity=".441"/><stop offset=".261" stop-color="#2f35b1" stop-opacity=".249"/><stop offset=".466" stop-color="#462fbf" stop-opacity=".111"/><stop offset=".669" stop-color="#542bc8" stop-opacity=".028"/><stop offset=".864" stop-color="#592acb" stop-opacity="0"/></linearGradient><linearGradient id="e" x1="66.607" y1="16.536" x2="58.343" y2="30.85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#722291" stop-opacity=".5"/><stop offset=".5" stop-color="#722291" stop-opacity="0"/></linearGradient></defs><path fill="none" d="M0 0h80v80H0z"/><path d="M40 0A40.136 40.136 0 0 0 0 39.562 4.4 4.4 0 0 0 4.4 44H36v21.284l-10.174-10.16a4 4 0 1 0-5.652 5.661l17 16.977a4 4 0 0 0 5.652 0l17-16.977a4 4 0 1 0-5.652-5.661L44 65.284V44h31.6a4.4 4.4 0 0 0 4.4-4.447A40.133 40.133 0 0 0 40 0zM8.248 36a32 32 0 0 1 63.505 0z" fill="url(#a)"/><path d="M40 0A40.136 40.136 0 0 0 0 39.562 4.4 4.4 0 0 0 4.4 44H36v21.284l-10.174-10.16a4 4 0 1 0-5.652 5.661l17 16.977a4 4 0 0 0 5.652 0l17-16.977a4 4 0 1 0-5.652-5.661L44 65.284V44h31.6a4.4 4.4 0 0 0 4.4-4.447A40.133 40.133 0 0 0 40 0zM8.248 36a32 32 0 0 1 63.505 0z" fill="url(#b)"/><path d="M44 8.259A32.157 32.157 0 0 1 71.753 36H52a8 8 0 0 0-8 8h31.6a4.428 4.428 0 0 0 3.124-1.3A4.48 4.48 0 0 0 80 39.553c0-22.196-24.462-30.11-36-31.294z" fill="url(#c)"/><path d="M52 36a8 8 0 0 0-8 8h31.6a4.416 4.416 0 0 0 2.973-1.179L71.753 36z" opacity=".9" fill="url(#d)"/><path d="M80 39.553c0-22.2-24.443-30.124-36-31.294A32.157 32.157 0 0 1 71.753 36l6.821 6.821c.048-.044.105-.078.151-.124A4.48 4.48 0 0 0 80 39.553z" fill="url(#e)"/></svg> \ No newline at end of file
diff --git a/browser/base/content/logos/tracking-protection-dark-theme.svg b/browser/base/content/logos/tracking-protection-dark-theme.svg
new file mode 100644
index 0000000000..ef411610bd
--- /dev/null
+++ b/browser/base/content/logos/tracking-protection-dark-theme.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64"><defs><linearGradient id="a" x1="34.8" y1="17.75" x2="20.9" y2="41.82" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ab71ff"/><stop offset="1" stop-color="#00b3f4"/></linearGradient><linearGradient id="b" x1="44.47" y1="3.13" x2="15.83" y2="52.75" xlink:href="#a"/><radialGradient id="c" cx="32.08" cy="29.82" fx="55.31" fy="9.098" r="31.42" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#ab71ff"/><stop offset=".32" stop-color="#a772ff"/><stop offset=".44" stop-color="#9c77fe"/><stop offset=".56" stop-color="#897efd"/><stop offset=".68" stop-color="#6f88fb"/><stop offset=".8" stop-color="#4d95f9"/><stop offset=".92" stop-color="#24a5f6"/><stop offset="1" stop-color="#00b3f4"/></radialGradient></defs><path d="M20.28 27.8c.78 8.5 2.12 11.5 5 15.4A13 13 0 0 0 32 47.79V16.13l-12 2c0 4.6.13 8.06.28 9.67z" fill="url(#a)"/><path d="M56 13.57a6.52 6.52 0 0 0-5.43-6.45L32 4 13.43 7.12A6.52 6.52 0 0 0 8 13.57c0 4.25 0 11.89.33 15.33.91 9.88 2.76 15.29 7.32 21.45a24.94 24.94 0 0 0 16 9.63L32 60h.33a24.94 24.94 0 0 0 16-9.63c4.56-6.16 6.41-11.57 7.32-21.45.35-3.47.35-11.11.35-15.35zm-6.31 14.78c-.81 8.85-2.25 13.15-6.16 18.42A19.25 19.25 0 0 1 32 54a19.17 19.17 0 0 1-11.53-7.19c-3.91-5.27-5.35-9.58-6.16-18.42C14.09 26 14 20.64 14 13.58a.55.55 0 0 1 .43-.55L32 10l17.57 3a.55.55 0 0 1 .43.55c0 7.08-.09 12.45-.31 14.8z" fill="url(#b)"/><path d="M56 13.57a6.52 6.52 0 0 0-5.43-6.45L32 4 13.43 7.12A6.52 6.52 0 0 0 8 13.57c0 4.25 0 11.89.33 15.33.91 9.88 2.76 15.29 7.32 21.45a24.94 24.94 0 0 0 16 9.63L32 60h.33a24.94 24.94 0 0 0 16-9.63c4.56-6.16 6.41-11.57 7.32-21.45.35-3.47.35-11.11.35-15.35zm-6.31 14.78c-.81 8.85-2.25 13.15-6.16 18.42A19.25 19.25 0 0 1 32 54a19.17 19.17 0 0 1-11.53-7.19c-3.91-5.27-5.35-9.58-6.16-18.42C14.09 26 14 20.64 14 13.58a.55.55 0 0 1 .43-.55L32 10l17.57 3a.55.55 0 0 1 .43.55c0 7.08-.09 12.45-.31 14.8zm-29.41-.55c.78 8.5 2.12 11.5 5 15.4A13 13 0 0 0 32 47.79V16.13l-12 2c0 4.6.13 8.06.28 9.67z" fill="url(#c)"/></svg>
diff --git a/browser/base/content/logos/tracking-protection.svg b/browser/base/content/logos/tracking-protection.svg
new file mode 100644
index 0000000000..0d554caaac
--- /dev/null
+++ b/browser/base/content/logos/tracking-protection.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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" viewBox="0 0 64 64"><defs><radialGradient id="a" cx="32.06" cy="29.49" fx="53.493" fy="9.619" r="31.22" gradientUnits="userSpaceOnUse"><stop offset=".26" stop-color="#7542e5"/><stop offset=".43" stop-color="#7243e5"/><stop offset=".56" stop-color="#6845e4"/><stop offset=".68" stop-color="#574ae3"/><stop offset=".8" stop-color="#3f50e2"/><stop offset=".91" stop-color="#2158e1"/><stop offset="1" stop-color="#0060df"/></radialGradient></defs><path d="M56 13.57a6.52 6.52 0 0 0-5.43-6.45L32 4 13.43 7.12A6.52 6.52 0 0 0 8 13.57c0 4.25 0 11.89.33 15.33.91 9.88 2.76 15.29 7.32 21.45a24.94 24.94 0 0 0 16 9.63L32 60h.33a24.94 24.94 0 0 0 16-9.63c4.56-6.16 6.41-11.57 7.32-21.45.35-3.47.35-11.11.35-15.35zm-6.31 14.78c-.81 8.85-2.25 13.15-6.16 18.42A19.25 19.25 0 0 1 32 54a19.17 19.17 0 0 1-11.53-7.19c-3.91-5.27-5.35-9.58-6.16-18.42C14.09 26 14 20.64 14 13.58a.55.55 0 0 1 .43-.55L32 10l17.57 3a.55.55 0 0 1 .43.55c0 7.08-.09 12.45-.31 14.8zm-29.41-.55c.78 8.5 2.12 11.5 5 15.4A13 13 0 0 0 32 47.79V16.13l-12 2c0 4.6.13 8.06.28 9.67z" fill="url(#a)"/></svg>
diff --git a/browser/base/content/logos/vpn-dark.svg b/browser/base/content/logos/vpn-dark.svg
new file mode 100644
index 0000000000..8c54617052
--- /dev/null
+++ b/browser/base/content/logos/vpn-dark.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 width="192" height="192" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M96 21.6c-7.953 0-14.4 6.447-14.4 14.4S88.047 50.4 96 50.4s14.4-6.447 14.4-14.4-6.447-14.4-14.4-14.4zM62.4 36C62.4 17.443 77.443 2.4 96 2.4c18.557 0 33.6 15.043 33.6 33.6 0 18.557-15.043 33.6-33.6 33.6a33.45 33.45 0 01-15.985-4.039L65.561 80.015A33.397 33.397 0 0168.21 86.4h55.582c4.131-13.88 16.988-24 32.209-24 18.557 0 33.6 15.043 33.6 33.6 0 18.557-15.043 33.6-33.6 33.6a33.452 33.452 0 01-15.985-4.039l-14.454 14.454A33.452 33.452 0 01129.6 156c0 18.557-15.043 33.6-33.6 33.6-18.557 0-33.6-15.043-33.6-33.6 0-18.557 15.043-33.6 33.6-33.6a33.452 33.452 0 0115.985 4.039l14.454-14.454a33.37 33.37 0 01-2.648-6.385H68.209c-4.131 13.879-16.988 24-32.209 24-18.557 0-33.6-15.043-33.6-33.6 0-18.557 15.043-33.6 33.6-33.6a33.45 33.45 0 0115.985 4.039l14.454-14.454A33.45 33.45 0 0162.4 36zm19.2 120c0-7.953 6.447-14.4 14.4-14.4s14.4 6.447 14.4 14.4-6.447 14.4-14.4 14.4-14.4-6.447-14.4-14.4zM36 81.6c-7.953 0-14.4 6.447-14.4 14.4s6.447 14.4 14.4 14.4 14.4-6.447 14.4-14.4S43.953 81.6 36 81.6zM141.6 96c0-7.953 6.447-14.4 14.4-14.4s14.4 6.447 14.4 14.4-6.447 14.4-14.4 14.4-14.4-6.447-14.4-14.4z" fill="#fff"/>
+</svg>
diff --git a/browser/base/content/logos/vpn-light.svg b/browser/base/content/logos/vpn-light.svg
new file mode 100644
index 0000000000..710e63a177
--- /dev/null
+++ b/browser/base/content/logos/vpn-light.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 width="188" height="188" xmlns="http://www.w3.org/2000/svg">
+ <path d="M94 19.6c-7.953 0-14.4 6.447-14.4 14.4S86.047 48.4 94 48.4s14.4-6.447 14.4-14.4-6.447-14.4-14.4-14.4zM60.4 34C60.4 15.443 75.443.4 94 .4c18.557 0 33.6 15.043 33.6 33.6 0 18.557-15.043 33.6-33.6 33.6a33.45 33.45 0 01-15.985-4.039L63.561 78.015A33.397 33.397 0 0166.21 84.4h55.582c4.131-13.88 16.988-24 32.209-24 18.557 0 33.6 15.043 33.6 33.6 0 18.557-15.043 33.6-33.6 33.6a33.452 33.452 0 01-15.985-4.039l-14.454 14.454A33.452 33.452 0 01127.6 154c0 18.557-15.043 33.6-33.6 33.6-18.557 0-33.6-15.043-33.6-33.6 0-18.557 15.043-33.6 33.6-33.6a33.452 33.452 0 0115.985 4.039l14.454-14.454a33.37 33.37 0 01-2.648-6.385H66.209c-4.131 13.879-16.988 24-32.209 24C15.443 127.6.4 112.557.4 94 .4 75.443 15.443 60.4 34 60.4a33.45 33.45 0 0115.985 4.039l14.454-14.454A33.45 33.45 0 0160.4 34zm19.2 120c0-7.953 6.447-14.4 14.4-14.4s14.4 6.447 14.4 14.4-6.447 14.4-14.4 14.4-14.4-6.447-14.4-14.4zM34 79.6c-7.953 0-14.4 6.447-14.4 14.4s6.447 14.4 14.4 14.4 14.4-6.447 14.4-14.4S41.953 79.6 34 79.6zM139.6 94c0-7.953 6.447-14.4 14.4-14.4s14.4 6.447 14.4 14.4-6.447 14.4-14.4 14.4-14.4-6.447-14.4-14.4z" fill="#000" fill-rule="evenodd"/>
+</svg>
diff --git a/browser/base/content/macWindow.inc.xhtml b/browser/base/content/macWindow.inc.xhtml
new file mode 100644
index 0000000000..d6327086ff
--- /dev/null
+++ b/browser/base/content/macWindow.inc.xhtml
@@ -0,0 +1,35 @@
+# -*- Mode: 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/.
+
+# This include file should only contain things that are needed to support MacOS
+# specific features that are needed for all top level windows. If the feature is
+# also needed in browser.xhtml, it should go in one of the various include files
+# below that are shared with browser.xhtml. When including this file,
+# browser-doctype.inc must also be included.
+
+<linkset>
+ <html:link rel="localization" href="branding/brand.ftl"/>
+ <html:link rel="localization" href="browser/branding/sync-brand.ftl"/>
+ <html:link rel="localization" href="toolkit/global/textActions.ftl"/>
+ <html:link rel="localization" href="browser/browserSets.ftl"/>
+ <html:link rel="localization" href="browser/menubar.ftl"/>
+</linkset>
+
+# All JS files which are needed by browser.xhtml and other top level windows to
+# support MacOS specific features *must* go into the global-scripts.inc file so
+# that they can be shared with browser.xhtml.
+#include global-scripts.inc
+
+<script src="chrome://browser/content/nonbrowser-mac.js"></script>
+
+# All sets except for popupsets (commands, keys, and stringbundles)
+# *must* go into the browser-sets.inc file so that they can be shared with
+# browser.xhtml
+#include browser-sets.inc
+
+# The entire main menubar is placed into browser-menubar.inc, so that it can be
+# shared with browser.xhtml.
+#include browser-menubar.inc
diff --git a/browser/base/content/moz.build b/browser/base/content/moz.build
new file mode 100644
index 0000000000..8e29145029
--- /dev/null
+++ b/browser/base/content/moz.build
@@ -0,0 +1,176 @@
+# -*- 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("defaultthemes/**"):
+ BUG_COMPONENT = ("Firefox", "Theme")
+
+with Files("docs/**"):
+ BUG_COMPONENT = ("Core", "Security")
+
+with Files("pageinfo/**"):
+ BUG_COMPONENT = ("Firefox", "Page Info Window")
+
+with Files("test/about/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/alerts/**"):
+ BUG_COMPONENT = ("Toolkit", "Notifications and Alerts")
+
+with Files("test/captivePortal/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/chrome/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/contextMenu/**"):
+ BUG_COMPONENT = ("Firefox", "Menus")
+
+with Files("test/forms/**"):
+ BUG_COMPONENT = ("Core", "Layout: Form Controls")
+
+with Files("test/historySwipeAnimation/**"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("test/keyboard/**"):
+ BUG_COMPONENT = ("Firefox", "Keyboard Navigation")
+
+with Files("test/outOfProcess/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/pageActions/**"):
+ BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
+
+with Files("test/pageinfo/**"):
+ BUG_COMPONENT = ("Firefox", "Page Info Window")
+
+with Files("test/performance/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/performance/browser_appmenu.js"):
+ BUG_COMPONENT = ("Firefox", "Menus")
+
+with Files("test/permissions/**"):
+ BUG_COMPONENT = ("Firefox", "Site Permissions")
+
+with Files("test/plugins/**"):
+ BUG_COMPONENT = ("Core", "Plug-ins")
+
+with Files("test/popupNotifications/**"):
+ BUG_COMPONENT = ("Toolkit", "Notifications and Alerts")
+
+with Files("test/popups/**"):
+ BUG_COMPONENT = ("Firefox", "Site Permissions")
+
+with Files("test/referrer/**"):
+ BUG_COMPONENT = ("Core", "DOM: Navigation")
+
+with Files("test/sanitize/**"):
+ BUG_COMPONENT = ("Toolkit", "Data Sanitization")
+
+with Files("test/siteIdentity/**"):
+ BUG_COMPONENT = ("Firefox", "Site Identity")
+
+with Files("test/sidebar/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/startup/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/static/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/statuspanel/**"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("test/sync/**"):
+ BUG_COMPONENT = ("Firefox", "Sync")
+
+with Files("test/tabdialogs/**"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("test/tabPrompts/**"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("test/tabcrashed/**"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("test/tabs/**"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("test/touch/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("test/protectionsUI/**"):
+ BUG_COMPONENT = ("Firefox", "Protections UI")
+
+with Files("test/webextensions/**"):
+ BUG_COMPONENT = ("WebExtensions", "Untriaged")
+
+with Files("test/webrtc/**"):
+ BUG_COMPONENT = ("Core", "WebRTC")
+
+with Files("test/zoom/**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("aboutNetError.xhtml"):
+ BUG_COMPONENT = ("Firefox", "Security")
+
+with Files("test/caps/**"):
+ BUG_COMPONENT = ("Firefox", "Security")
+
+with Files("blockedSite.xhtml"):
+ BUG_COMPONENT = ("Toolkit", "Safe Browsing")
+
+with Files("browser-addons.js"):
+ BUG_COMPONENT = ("Toolkit", "Add-ons Manager")
+
+with Files("*menu*"):
+ BUG_COMPONENT = ("Firefox", "Menus")
+
+with Files("browser-customization.js"):
+ BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
+
+with Files("browser-fullZoom.js"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("browser-gestureSupport.js"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("browser-pageActions.js"):
+ BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
+
+with Files("browser-places.js"):
+ BUG_COMPONENT = ("Firefox", "Bookmarks & History")
+
+with Files("browser-safebrowsing.js"):
+ BUG_COMPONENT = ("Toolkit", "Safe Browsing")
+
+with Files("browser-sync.js"):
+ BUG_COMPONENT = ("Firefox", "Sync")
+
+with Files("contentSearch*"):
+ BUG_COMPONENT = ("Firefox", "Search")
+
+with Files("hiddenWindowMac.xhtml"):
+ BUG_COMPONENT = ("Firefox", "Site Permissions")
+
+with Files("macWindow.inc.xhtml"):
+ BUG_COMPONENT = ("Firefox", "Shell Integration")
+
+with Files("tabbrowser*"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("browser-allTabsMenu.js"):
+ BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
+with Files("webext-panels*"):
+ BUG_COMPONENT = ("WebExtensions", "Frontend")
+
+with Files("webrtcIndicator*"):
+ BUG_COMPONENT = ("Firefox", "Site Permissions")
diff --git a/browser/base/content/newInstall.js b/browser/base/content/newInstall.js
new file mode 100644
index 0000000000..2ec90e3bf1
--- /dev/null
+++ b/browser/base/content/newInstall.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/. */
+
+const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm",
+ {}
+);
+
+function init() {
+ document.querySelector("button").addEventListener("command", () => {
+ window.close();
+ });
+
+ if (navigator.platform == "MacIntel") {
+ hideMenus();
+ window.addEventListener("unload", showMenus);
+ }
+}
+
+let gHidden = [];
+let gCollapsed = [];
+
+function hideItem(id) {
+ let hiddenDoc = Services.appShell.hiddenDOMWindow.document;
+ let element = hiddenDoc.getElementById(id);
+ element.hidden = true;
+ gHidden.push(element);
+}
+
+function collapseItem(id) {
+ let hiddenDoc = Services.appShell.hiddenDOMWindow.document;
+ let element = hiddenDoc.getElementById(id);
+ element.collapsed = true;
+ gCollapsed.push(element);
+}
+
+function hideMenus() {
+ hideItem("macDockMenuNewWindow");
+ hideItem("macDockMenuNewPrivateWindow");
+ collapseItem("aboutName");
+ collapseItem("menu_preferences");
+ collapseItem("menu_mac_services");
+}
+
+function showMenus() {
+ for (let menu of gHidden) {
+ menu.hidden = false;
+ }
+
+ for (let menu of gCollapsed) {
+ menu.collapsed = false;
+ }
+}
diff --git a/browser/base/content/newInstall.xhtml b/browser/base/content/newInstall.xhtml
new file mode 100644
index 0000000000..c286643775
--- /dev/null
+++ b/browser/base/content/newInstall.xhtml
@@ -0,0 +1,28 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+%syncBrandDTD;
+<!ENTITY % newInstallDTD SYSTEM "chrome://browser/locale/newInstall.dtd">
+%newInstallDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/newInstall.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init()" title="&window.title;" style="&window.style;">
+ <script src="chrome://browser/content/newInstall.js"></script>
+ <hbox align="start" flex="1">
+ <image id="alert" role="presentation"/>
+ <vbox align="end" flex="1">
+ <description class="main-text">&mainText;</description>
+ <description>&sync;</description>
+ <button label="&continue-button;"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/browser/base/content/newInstallPage.html b/browser/base/content/newInstallPage.html
new file mode 100644
index 0000000000..3b7604030f
--- /dev/null
+++ b/browser/base/content/newInstallPage.html
@@ -0,0 +1,55 @@
+<!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>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="connect-src https:; default-src chrome:; object-src 'none'">
+ <meta name="referrer" content="no-referrer">
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+ <link rel="stylesheet" type="text/css" href="chrome://browser/skin/newInstallPage.css">
+ <link rel="localization" href="branding/brand.ftl">
+ <link rel="localization" href="browser/branding/sync-brand.ftl">
+ <link rel="localization" href="browser/newInstallPage.ftl">
+ <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png">
+ <title data-l10n-id="title"></title>
+ <script src="chrome://browser/content/newInstallPage.js"></script>
+</head>
+<body>
+ <div id="main">
+ <div id="header">
+ <img role="presentation" src="chrome://branding/content/horizontal-lockup.svg">
+ </div>
+ <div id="content">
+ <div id="info">
+ <h1 data-l10n-id="heading"></h1>
+ <h3 data-l10n-id="changed-title"></h3>
+ <p data-l10n-id="changed-desc-profiles"></p>
+ <p data-l10n-id="changed-desc-dedicated"></p>
+ <p data-l10n-id="lost"></p>
+ <h3 data-l10n-id="options-title"></h3>
+ <p data-l10n-id="options-do-nothing"></p>
+ <p data-l10n-id="options-use-sync"></p>
+ <p>
+ <span data-l10n-id="resources"></span><br>
+ <a data-l10n-id="support-link" href="https://support.mozilla.org/kb/profile-manager-create-and-remove-firefox-profiles" target="_blank" rel="noopener"></a>
+ </p>
+ </div>
+ <form id="sync">
+ <h1 id="sync-header" data-l10n-id="sync-header"></h1>
+ <p id="sync-label"><label data-l10n-id="sync-label" for="sync-input"></label></p>
+ <p id="sync-input-container"><input data-l10n-id="sync-input" id="sync-input" type="email" required name="email"></p>
+ <p id="sync-terms" data-l10n-id="sync-terms">
+ <a data-l10n-name="terms" href="https://accounts.firefox.com/legal/terms" target="_blank" rel="noopener"></a>
+ <a data-l10n-name="privacy" href="https://accounts.firefox.com/legal/privacy" target="_blank" rel="noopener"></a>
+ </p>
+ <p id="sync-button-container"><button id="sync-button" type="submit" data-l10n-id="sync-button"></button></p>
+ <p id="sync-first" data-l10n-id="sync-first"></p>
+ <p id="sync-learn"><a href="https://support.mozilla.org/kb/how-do-i-set-sync-my-computer" target="_blank" rel="noopener" data-l10n-id="sync-learn"></a></p>
+ </form>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/newInstallPage.js b/browser/base/content/newInstallPage.js
new file mode 100644
index 0000000000..2b378a30d4
--- /dev/null
+++ b/browser/base/content/newInstallPage.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/. */
+
+/* global RPMGetUpdateChannel, RPMGetFxAccountsEndpoint */
+
+const PARAMS = new URL(location).searchParams;
+const ENTRYPOINT = "new-install-page";
+const SOURCE = `new-install-page-${RPMGetUpdateChannel()}`;
+const CAMPAIGN = "dedicated-profiles";
+const ENDPOINT = PARAMS.get("endpoint");
+
+function appendAccountsParams(url) {
+ url.searchParams.set("entrypoint", ENTRYPOINT);
+ url.searchParams.set("utm_source", SOURCE);
+ url.searchParams.set("utm_campaign", CAMPAIGN);
+}
+
+function appendParams(url, params) {
+ appendAccountsParams(url);
+
+ for (let [key, value] of Object.entries(params)) {
+ url.searchParams.set(key, value);
+ }
+}
+
+async function requestFlowMetrics() {
+ let requestURL = new URL(await endpoint);
+ requestURL.pathname = "metrics-flow";
+ appendParams(requestURL, {
+ form_type: "email",
+ });
+
+ let response = await fetch(requestURL, { credentials: "omit" });
+ if (response.status === 200) {
+ return response.json();
+ }
+
+ throw new Error(`Failed to retrieve metrics: ${response.status}`);
+}
+
+async function submitForm(event) {
+ // We never want to submit the form.
+ event.preventDefault();
+
+ let input = document.getElementById("sync-input");
+
+ let { flowId, flowBeginTime } = await metrics;
+
+ let requestURL = new URL(await endpoint);
+ appendParams(requestURL, {
+ action: "email",
+ utm_campaign: CAMPAIGN,
+ email: input.value,
+ flow_id: flowId,
+ flow_begin_time: flowBeginTime,
+ });
+
+ window.open(requestURL, "_blank", "noopener");
+ document.getElementById("sync").hidden = true;
+}
+
+const endpoint = RPMGetFxAccountsEndpoint(ENTRYPOINT);
+
+// This must come before the CSP is set or it will be blocked.
+const metrics = requestFlowMetrics();
+
+document.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ document.getElementById("sync").addEventListener("submit", submitForm);
+ },
+ { once: true }
+);
diff --git a/browser/base/content/nonbrowser-mac.js b/browser/base/content/nonbrowser-mac.js
new file mode 100644
index 0000000000..c00364025c
--- /dev/null
+++ b/browser/base/content/nonbrowser-mac.js
@@ -0,0 +1,151 @@
+/* -*- 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/. */
+
+/* eslint-env mozilla/browser-window */
+
+let delayedStartupTimeoutId = null;
+
+function OpenBrowserWindowFromDockMenu(options) {
+ let win = OpenBrowserWindow(options);
+ win.addEventListener(
+ "load",
+ function() {
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"].getService(
+ Ci.nsIMacDockSupport
+ );
+ dockSupport.activateApplication(true);
+ },
+ { once: true }
+ );
+
+ return win;
+}
+
+function nonBrowserWindowStartup() {
+ // Disable inappropriate commands / submenus
+ var disabledItems = [
+ "Browser:SavePage",
+ "Browser:SendLink",
+ "cmd_pageSetup",
+ "cmd_print",
+ "cmd_print_kb",
+ "cmd_find",
+ "cmd_findAgain",
+ "viewToolbarsMenu",
+ "viewSidebarMenuMenu",
+ "Browser:Reload",
+ "viewFullZoomMenu",
+ "pageStyleMenu",
+ "charsetMenu",
+ "View:PageSource",
+ "View:FullScreen",
+ "viewHistorySidebar",
+ "Browser:AddBookmarkAs",
+ "Browser:BookmarkAllTabs",
+ "View:PageInfo",
+ "History:UndoCloseTab",
+ ];
+ var element;
+
+ for (let disabledItem of disabledItems) {
+ element = document.getElementById(disabledItem);
+ if (element) {
+ element.setAttribute("disabled", "true");
+ }
+ }
+
+ // Show menus that are only visible in non-browser windows
+ let shownItems = ["menu_openLocation"];
+ for (let shownItem of shownItems) {
+ element = document.getElementById(shownItem);
+ if (element) {
+ element.removeAttribute("hidden");
+ }
+ }
+
+ if (
+ window.location.href == "chrome://browser/content/hiddenWindowMac.xhtml"
+ ) {
+ // If no windows are active (i.e. we're the hidden window), disable the
+ // close, minimize and zoom menu commands as well.
+ var hiddenWindowDisabledItems = [
+ "cmd_close",
+ "minimizeWindow",
+ "zoomWindow",
+ ];
+ for (let hiddenWindowDisabledItem of hiddenWindowDisabledItems) {
+ element = document.getElementById(hiddenWindowDisabledItem);
+ if (element) {
+ element.setAttribute("disabled", "true");
+ }
+ }
+
+ // Also hide the window-list separator.
+ element = document.getElementById("sep-window-list");
+ element.setAttribute("hidden", "true");
+
+ // Setup the dock menu.
+ let dockMenuElement = document.getElementById("menu_mac_dockmenu");
+ if (dockMenuElement != null) {
+ let nativeMenu = Cc[
+ "@mozilla.org/widget/standalonenativemenu;1"
+ ].createInstance(Ci.nsIStandaloneNativeMenu);
+
+ try {
+ nativeMenu.init(dockMenuElement);
+
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"].getService(
+ Ci.nsIMacDockSupport
+ );
+ dockSupport.dockMenu = nativeMenu;
+ } catch (e) {}
+ }
+
+ // Hide menuitems that don't apply to private contexts.
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ document.getElementById("macDockMenuNewWindow").hidden = true;
+ }
+ if (!PrivateBrowsingUtils.enabled) {
+ document.getElementById("macDockMenuNewPrivateWindow").hidden = true;
+ }
+ }
+
+ delayedStartupTimeoutId = setTimeout(nonBrowserWindowDelayedStartup, 0);
+}
+
+function nonBrowserWindowDelayedStartup() {
+ delayedStartupTimeoutId = null;
+
+ // initialise the offline listener
+ BrowserOffline.init();
+
+ // initialize the private browsing UI
+ gPrivateBrowsingUI.init();
+}
+
+function nonBrowserWindowShutdown() {
+ // If this is the hidden window being closed, release our reference to
+ // the dock menu element to prevent leaks on shutdown
+ if (
+ window.location.href == "chrome://browser/content/hiddenWindowMac.xhtml"
+ ) {
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"].getService(
+ Ci.nsIMacDockSupport
+ );
+ dockSupport.dockMenu = null;
+ }
+
+ // If nonBrowserWindowDelayedStartup hasn't run yet, we have no work to do -
+ // just cancel the pending timeout and return;
+ if (delayedStartupTimeoutId) {
+ clearTimeout(delayedStartupTimeoutId);
+ return;
+ }
+
+ BrowserOffline.uninit();
+}
+
+addEventListener("load", nonBrowserWindowStartup, false);
+addEventListener("unload", nonBrowserWindowShutdown, false);
diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js
new file mode 100644
index 0000000000..8599c52a2b
--- /dev/null
+++ b/browser/base/content/nsContextMenu.js
@@ -0,0 +1,2093 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 sw=2 sts=2 et 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/. */
+
+const PASSWORD_FIELDNAME_HINTS = ["current-password", "new-password"];
+
+function openContextMenu(aMessage, aBrowser, aActor) {
+ if (BrowserHandler.kiosk) {
+ // Don't display context menus in kiosk mode
+ return;
+ }
+ let data = aMessage.data;
+ let browser = aBrowser;
+ let actor = aActor;
+ let spellInfo = data.spellInfo;
+ let frameReferrerInfo = data.frameReferrerInfo;
+ let linkReferrerInfo = data.linkReferrerInfo;
+ let principal = data.principal;
+ let storagePrincipal = data.storagePrincipal;
+
+ let documentURIObject = makeURI(
+ data.docLocation,
+ data.charSet,
+ makeURI(data.baseURI)
+ );
+
+ if (frameReferrerInfo) {
+ frameReferrerInfo = E10SUtils.deserializeReferrerInfo(frameReferrerInfo);
+ }
+
+ if (linkReferrerInfo) {
+ linkReferrerInfo = E10SUtils.deserializeReferrerInfo(linkReferrerInfo);
+ }
+
+ nsContextMenu.contentData = {
+ context: data.context,
+ browser,
+ actor,
+ editFlags: data.editFlags,
+ spellInfo,
+ principal,
+ storagePrincipal,
+ customMenuItems: data.customMenuItems,
+ documentURIObject,
+ docLocation: data.docLocation,
+ charSet: data.charSet,
+ referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
+ frameReferrerInfo,
+ linkReferrerInfo,
+ contentType: data.contentType,
+ contentDisposition: data.contentDisposition,
+ frameID: data.frameID,
+ frameOuterWindowID: data.frameID,
+ frameBrowsingContext: BrowsingContext.get(data.frameBrowsingContextID),
+ selectionInfo: data.selectionInfo,
+ disableSetDesktopBackground: data.disableSetDesktopBackground,
+ loginFillInfo: data.loginFillInfo,
+ parentAllowsMixedContent: data.parentAllowsMixedContent,
+ userContextId: data.userContextId,
+ webExtContextData: data.webExtContextData,
+ cookieJarSettings: E10SUtils.deserializeCookieJarSettings(
+ data.cookieJarSettings
+ ),
+ };
+
+ let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
+ let context = nsContextMenu.contentData.context;
+
+ // We don't have access to the original event here, as that happened in
+ // another process. Therefore we synthesize a new MouseEvent to propagate the
+ // inputSource to the subsequently triggered popupshowing event.
+ var newEvent = document.createEvent("MouseEvent");
+ newEvent.initNSMouseEvent(
+ "contextmenu",
+ true,
+ true,
+ null,
+ 0,
+ context.screenX,
+ context.screenY,
+ 0,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ 0,
+ context.mozInputSource
+ );
+ popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
+}
+
+class nsContextMenu {
+ constructor(aXulMenu, aIsShift) {
+ // Get contextual info.
+ this.setContext();
+
+ if (!this.shouldDisplay) {
+ return;
+ }
+
+ this.hasPageMenu = false;
+ this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
+ if (!aIsShift) {
+ this.hasPageMenu = PageMenuParent.addToPopup(
+ this.contentData.customMenuItems,
+ this.browser,
+ aXulMenu
+ );
+
+ let tab =
+ gBrowser && gBrowser.getTabForBrowser
+ ? gBrowser.getTabForBrowser(this.browser)
+ : undefined;
+
+ let subject = {
+ menu: aXulMenu,
+ tab,
+ timeStamp: this.timeStamp,
+ isContentSelected: this.isContentSelected,
+ inFrame: this.inFrame,
+ isTextSelected: this.isTextSelected,
+ onTextInput: this.onTextInput,
+ onLink: this.onLink,
+ onImage: this.onImage,
+ onVideo: this.onVideo,
+ onAudio: this.onAudio,
+ onCanvas: this.onCanvas,
+ onEditable: this.onEditable,
+ onSpellcheckable: this.onSpellcheckable,
+ onPassword: this.onPassword,
+ srcUrl: this.mediaURL,
+ frameUrl: this.contentData ? this.contentData.docLocation : undefined,
+ pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
+ linkText: this.linkTextStr,
+ linkUrl: this.linkURL,
+ selectionText: this.isTextSelected
+ ? this.selectionInfo.fullText
+ : undefined,
+ frameId: this.frameID,
+ webExtBrowserType: this.webExtBrowserType,
+ webExtContextData: this.contentData
+ ? this.contentData.webExtContextData
+ : undefined,
+ };
+ subject.wrappedJSObject = subject;
+ Services.obs.notifyObservers(subject, "on-build-contextmenu");
+ }
+
+ this.viewFrameSourceElement = document.getElementById(
+ "context-viewframesource"
+ );
+ this.ellipsis = "\u2026";
+ try {
+ this.ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {}
+
+ // Reset after "on-build-contextmenu" notification in case selection was
+ // changed during the notification.
+ this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
+ this.onPlainTextLink = false;
+
+ // Initialize (disable/remove) menu items.
+ this.initItems();
+ }
+
+ setContext() {
+ let context = Object.create(null);
+
+ if (nsContextMenu.contentData) {
+ this.contentData = nsContextMenu.contentData;
+ context = this.contentData.context;
+ nsContextMenu.contentData = null;
+ }
+
+ this.shouldDisplay = context.shouldDisplay;
+ this.timeStamp = context.timeStamp;
+
+ // Assign what's _possibly_ needed from `context` sent by ContextMenuChild.jsm
+ // Keep this consistent with the similar code in ContextMenu's _setContext
+ this.bgImageURL = context.bgImageURL;
+ this.imageDescURL = context.imageDescURL;
+ this.imageInfo = context.imageInfo;
+ this.mediaURL = context.mediaURL;
+ this.webExtBrowserType = context.webExtBrowserType;
+
+ this.canSpellCheck = context.canSpellCheck;
+ this.hasBGImage = context.hasBGImage;
+ this.hasMultipleBGImages = context.hasMultipleBGImages;
+ this.isDesignMode = context.isDesignMode;
+ this.inFrame = context.inFrame;
+ this.inPDFViewer = context.inPDFViewer;
+ this.inSrcdocFrame = context.inSrcdocFrame;
+ this.inSyntheticDoc = context.inSyntheticDoc;
+ this.inTabBrowser = context.inTabBrowser;
+ this.inWebExtBrowser = context.inWebExtBrowser;
+
+ this.link = context.link;
+ this.linkDownload = context.linkDownload;
+ this.linkProtocol = context.linkProtocol;
+ this.linkTextStr = context.linkTextStr;
+ this.linkURL = context.linkURL;
+ this.linkURI = this.getLinkURI(); // can't send; regenerate
+
+ this.onAudio = context.onAudio;
+ this.onCanvas = context.onCanvas;
+ this.onCompletedImage = context.onCompletedImage;
+ this.onCTPPlugin = context.onCTPPlugin;
+ this.onDRMMedia = context.onDRMMedia;
+ this.onPiPVideo = context.onPiPVideo;
+ this.onEditable = context.onEditable;
+ this.onImage = context.onImage;
+ this.onKeywordField = context.onKeywordField;
+ this.onLink = context.onLink;
+ this.onLoadedImage = context.onLoadedImage;
+ this.onMailtoLink = context.onMailtoLink;
+ this.onMozExtLink = context.onMozExtLink;
+ this.onNumeric = context.onNumeric;
+ this.onPassword = context.onPassword;
+ this.onSaveableLink = context.onSaveableLink;
+ this.onSpellcheckable = context.onSpellcheckable;
+ this.onTextInput = context.onTextInput;
+ this.onVideo = context.onVideo;
+
+ this.target = context.target;
+ this.targetIdentifier = context.targetIdentifier;
+
+ this.principal = context.principal;
+ this.storagePrincipal = context.storagePrincipal;
+ this.frameID = context.frameID;
+ this.frameOuterWindowID = context.frameOuterWindowID;
+ this.frameBrowsingContext = BrowsingContext.get(
+ context.frameBrowsingContextID
+ );
+
+ this.inSyntheticDoc = context.inSyntheticDoc;
+ this.inAboutDevtoolsToolbox = context.inAboutDevtoolsToolbox;
+
+ // Everything after this isn't sent directly from ContextMenu
+ if (this.target) {
+ this.ownerDoc = this.target.ownerDocument;
+ }
+
+ this.csp = E10SUtils.deserializeCSP(context.csp);
+
+ if (this.contentData) {
+ this.browser = this.contentData.browser;
+ this.selectionInfo = this.contentData.selectionInfo;
+ this.actor = this.contentData.actor;
+ } else {
+ this.browser = this.ownerDoc.defaultView.docShell.chromeEventHandler;
+ this.selectionInfo = BrowserUtils.getSelectionDetails(window);
+ this.actor = this.browser.browsingContext.currentWindowGlobal.getActor(
+ "ContextMenu"
+ );
+ }
+
+ const { gBrowser } = this.browser.ownerGlobal;
+
+ this.textSelected = this.selectionInfo.text;
+ this.isTextSelected = !!this.textSelected.length;
+ this.webExtBrowserType = this.browser.getAttribute(
+ "webextension-view-type"
+ );
+ this.inWebExtBrowser = !!this.webExtBrowserType;
+ this.inTabBrowser =
+ gBrowser && gBrowser.getTabForBrowser
+ ? !!gBrowser.getTabForBrowser(this.browser)
+ : false;
+
+ if (context.shouldInitInlineSpellCheckerUINoChildren) {
+ InlineSpellCheckerUI.initFromRemote(
+ this.contentData.spellInfo,
+ this.actor.manager
+ );
+ }
+
+ if (context.shouldInitInlineSpellCheckerUIWithChildren) {
+ InlineSpellCheckerUI.initFromRemote(
+ this.contentData.spellInfo,
+ this.actor.manager
+ );
+ let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
+ this.showItem("spell-check-enabled", canSpell);
+ this.showItem("spell-separator", canSpell);
+ }
+ } // setContext
+
+ hiding() {
+ if (this.actor) {
+ this.actor.hiding();
+ }
+
+ this.contentData = null;
+ InlineSpellCheckerUI.clearSuggestionsFromMenu();
+ InlineSpellCheckerUI.clearDictionaryListFromMenu();
+ InlineSpellCheckerUI.uninit();
+ if (
+ Cu.isModuleLoaded("resource://gre/modules/LoginManagerContextMenu.jsm")
+ ) {
+ nsContextMenu.LoginManagerContextMenu.clearLoginsFromMenu(document);
+ }
+
+ // This handler self-deletes, only run it if it is still there:
+ if (this._onPopupHiding) {
+ this._onPopupHiding();
+ }
+ }
+
+ initItems() {
+ this.initPageMenuSeparator();
+ this.initOpenItems();
+ this.initNavigationItems();
+ this.initViewItems();
+ this.initMiscItems();
+ this.initSpellingItems();
+ this.initSaveItems();
+ this.initClipboardItems();
+ this.initMediaPlayerItems();
+ this.initLeaveDOMFullScreenItems();
+ this.initClickToPlayItems();
+ this.initPasswordManagerItems();
+ this.initSyncItems();
+ }
+
+ initPageMenuSeparator() {
+ this.showItem("page-menu-separator", this.hasPageMenu);
+ }
+
+ initOpenItems() {
+ var isMailtoInternal = false;
+ if (this.onMailtoLink) {
+ var mailtoHandler = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ]
+ .getService(Ci.nsIExternalProtocolService)
+ .getProtocolHandlerInfo("mailto");
+ isMailtoInternal =
+ !mailtoHandler.alwaysAskBeforeHandling &&
+ mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
+ mailtoHandler.preferredApplicationHandler instanceof
+ Ci.nsIWebHandlerApp;
+ }
+
+ if (
+ this.isTextSelected &&
+ !this.onLink &&
+ this.selectionInfo &&
+ this.selectionInfo.linkURL
+ ) {
+ this.linkURL = this.selectionInfo.linkURL;
+ try {
+ this.linkURI = makeURI(this.linkURL);
+ } catch (ex) {}
+
+ this.linkTextStr = this.selectionInfo.linkText;
+ this.onPlainTextLink = true;
+ }
+
+ var inContainer = false;
+ if (this.contentData.userContextId) {
+ inContainer = true;
+ var item = document.getElementById("context-openlinkincontainertab");
+
+ item.setAttribute("data-usercontextid", this.contentData.userContextId);
+
+ var label = ContextualIdentityService.getUserContextLabel(
+ this.contentData.userContextId
+ );
+ item.setAttribute(
+ "label",
+ gBrowserBundle.formatStringFromName("userContextOpenLink.label", [
+ label,
+ ])
+ );
+ }
+
+ var shouldShow =
+ this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
+ var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+ let showContainers =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ ContextualIdentityService.getPublicIdentities().length;
+ this.showItem("context-openlink", shouldShow && !isWindowPrivate);
+ this.showItem(
+ "context-openlinkprivate",
+ shouldShow && PrivateBrowsingUtils.enabled
+ );
+ this.showItem("context-openlinkintab", shouldShow && !inContainer);
+ this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
+ this.showItem(
+ "context-openlinkinusercontext-menu",
+ shouldShow && !isWindowPrivate && showContainers
+ );
+ this.showItem("context-openlinkincurrent", this.onPlainTextLink);
+ this.showItem("context-sep-open", shouldShow);
+ }
+
+ initNavigationItems() {
+ var shouldShow =
+ !(
+ this.isContentSelected ||
+ this.onLink ||
+ this.onImage ||
+ this.onCanvas ||
+ this.onVideo ||
+ this.onAudio ||
+ this.onTextInput
+ ) && this.inTabBrowser;
+ this.showItem("context-navigation", shouldShow);
+ this.showItem("context-sep-navigation", shouldShow);
+
+ let stopped =
+ XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";
+
+ let stopReloadItem = "";
+ if (shouldShow || !this.inTabBrowser) {
+ stopReloadItem = stopped || !this.inTabBrowser ? "reload" : "stop";
+ }
+
+ this.showItem("context-reload", stopReloadItem == "reload");
+ this.showItem("context-stop", stopReloadItem == "stop");
+ }
+
+ initLeaveDOMFullScreenItems() {
+ // only show the option if the user is in DOM fullscreen
+ var shouldShow = this.target.ownerDocument.fullscreen;
+ this.showItem("context-leave-dom-fullscreen", shouldShow);
+
+ // Explicitly show if in DOM fullscreen, but do not hide it has already been shown
+ if (shouldShow) {
+ this.showItem("context-media-sep-commands", true);
+ }
+ }
+
+ initSaveItems() {
+ var shouldShow = !(
+ this.onTextInput ||
+ this.onLink ||
+ this.isContentSelected ||
+ this.onImage ||
+ this.onCanvas ||
+ this.onVideo ||
+ this.onAudio
+ );
+ this.showItem("context-savepage", shouldShow);
+
+ // Save link depends on whether we're in a link, or selected text matches valid URL pattern.
+ this.showItem(
+ "context-savelink",
+ this.onSaveableLink || this.onPlainTextLink
+ );
+ if (
+ (this.onSaveableLink || this.onPlainTextLink) &&
+ Services.policies.status === Services.policies.ACTIVE
+ ) {
+ this.setItemAttr(
+ "context-savelink",
+ "disabled",
+ !WebsiteFilter.isAllowed(this.linkURL)
+ );
+ }
+
+ // Save image depends on having loaded its content, video and audio don't.
+ this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
+ this.showItem("context-savevideo", this.onVideo);
+ this.showItem("context-saveaudio", this.onAudio);
+ this.showItem("context-video-saveimage", this.onVideo);
+ this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
+ this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
+ // Send media URL (but not for canvas, since it's a big data: URL)
+ this.showItem("context-sendimage", this.onImage);
+ this.showItem("context-sendvideo", this.onVideo);
+ this.showItem("context-sendaudio", this.onAudio);
+ let mediaIsBlob = this.mediaURL.startsWith("blob:");
+ this.setItemAttr(
+ "context-sendvideo",
+ "disabled",
+ !this.mediaURL || mediaIsBlob
+ );
+ this.setItemAttr(
+ "context-sendaudio",
+ "disabled",
+ !this.mediaURL || mediaIsBlob
+ );
+ }
+
+ initViewItems() {
+ // View source is always OK, unless in directory listing.
+ this.showItem(
+ "context-viewpartialsource-selection",
+ !this.inAboutDevtoolsToolbox &&
+ this.isContentSelected &&
+ this.selectionInfo.isDocumentLevelSelection
+ );
+
+ this.showItem(
+ "context-print-selection",
+ !this.inAboutDevtoolsToolbox &&
+ this.isContentSelected &&
+ this.selectionInfo.isDocumentLevelSelection
+ );
+
+ var shouldShow = !(
+ this.isContentSelected ||
+ this.onImage ||
+ this.onCanvas ||
+ this.onVideo ||
+ this.onAudio ||
+ this.onLink ||
+ this.onTextInput
+ );
+
+ var showInspect =
+ this.inTabBrowser &&
+ !this.inAboutDevtoolsToolbox &&
+ Services.prefs.getBoolPref("devtools.inspector.enabled", true) &&
+ !Services.prefs.getBoolPref("devtools.policy.disabled", false);
+
+ var showInspectA11Y =
+ showInspect &&
+ Services.prefs.getBoolPref("devtools.accessibility.enabled", false) &&
+ this.inTabBrowser &&
+ Services.prefs.getBoolPref("devtools.enabled", true) &&
+ Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
+ !Services.prefs.getBoolPref("devtools.policy.disabled", false);
+
+ this.showItem("context-viewsource", shouldShow);
+ this.showItem("context-viewinfo", shouldShow);
+ // The page info is broken for WebExtension popups, as the browser is
+ // destroyed when the popup is closed.
+ this.setItemAttr(
+ "context-viewinfo",
+ "disabled",
+ this.webExtBrowserType === "popup"
+ );
+ this.showItem("inspect-separator", showInspect);
+ this.showItem("context-inspect", showInspect);
+
+ this.showItem("context-inspect-a11y", showInspectA11Y);
+
+ this.showItem("context-sep-viewsource", shouldShow);
+
+ // Set as Desktop background depends on whether an image was clicked on,
+ // and only works if we have a shell service.
+ var haveSetDesktopBackground = false;
+
+ if (
+ AppConstants.HAVE_SHELL_SERVICE &&
+ Services.policies.isAllowed("setDesktopBackground")
+ ) {
+ // Only enable Set as Desktop Background if we can get the shell service.
+ var shell = getShellService();
+ if (shell) {
+ haveSetDesktopBackground = shell.canSetDesktopBackground;
+ }
+ }
+
+ this.showItem(
+ "context-setDesktopBackground",
+ haveSetDesktopBackground && this.onLoadedImage
+ );
+
+ if (haveSetDesktopBackground && this.onLoadedImage) {
+ document.getElementById(
+ "context-setDesktopBackground"
+ ).disabled = this.contentData.disableSetDesktopBackground;
+ }
+
+ // Reload image depends on an image that's not fully loaded
+ this.showItem(
+ "context-reloadimage",
+ this.onImage && !this.onCompletedImage
+ );
+
+ // View image depends on having an image that's not standalone
+ // (or is in a frame), or a canvas.
+ this.showItem(
+ "context-viewimage",
+ (this.onImage && (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas
+ );
+
+ // View video depends on not having a standalone video.
+ this.showItem(
+ "context-viewvideo",
+ this.onVideo && (!this.inSyntheticDoc || this.inFrame)
+ );
+ this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
+
+ // View background image depends on whether there is one, but don't make
+ // background images of a stand-alone media document available.
+ this.showItem(
+ "context-viewbgimage",
+ shouldShow &&
+ !this.hasMultipleBGImages &&
+ !this.inSyntheticDoc &&
+ !this.inPDFViewer
+ );
+ this.showItem(
+ "context-sep-viewbgimage",
+ shouldShow &&
+ !this.hasMultipleBGImages &&
+ !this.inSyntheticDoc &&
+ !this.inPDFViewer
+ );
+ document.getElementById("context-viewbgimage").disabled = !this.hasBGImage;
+
+ this.showItem("context-viewimageinfo", this.onImage);
+ // The image info popup is broken for WebExtension popups, since the browser
+ // is destroyed when the popup is closed.
+ this.setItemAttr(
+ "context-viewimageinfo",
+ "disabled",
+ this.webExtBrowserType === "popup"
+ );
+ this.showItem(
+ "context-viewimagedesc",
+ this.onImage && this.imageDescURL !== ""
+ );
+ }
+
+ initMiscItems() {
+ // Use "Bookmark This Link" if on a link.
+ let bookmarkPage = document.getElementById("context-bookmarkpage");
+ this.showItem(
+ bookmarkPage,
+ !(
+ this.isContentSelected ||
+ this.onTextInput ||
+ this.onLink ||
+ this.onImage ||
+ this.onVideo ||
+ this.onAudio ||
+ this.onCanvas ||
+ this.inWebExtBrowser
+ )
+ );
+
+ this.showItem(
+ "context-bookmarklink",
+ (this.onLink && !this.onMailtoLink && !this.onMozExtLink) ||
+ this.onPlainTextLink
+ );
+ this.showItem(
+ "context-keywordfield",
+ this.onTextInput && this.onKeywordField
+ );
+ this.showItem("frame", this.inFrame);
+
+ if (this.inFrame) {
+ // To make it easier to debug the browser running with out-of-process iframes, we
+ // display the process PID of the iframe in the context menu for the subframe.
+ let frameOsPid = this.actor.manager.browsingContext.currentWindowGlobal
+ .osPid;
+ this.setItemAttr("context-frameOsPid", "label", "PID: " + frameOsPid);
+ }
+
+ this.showAndFormatSearchContextItem();
+
+ // srcdoc cannot be opened separately due to concerns about web
+ // content with about:srcdoc in location bar masquerading as trusted
+ // chrome/addon content.
+ // No need to also test for this.inFrame as this is checked in the parent
+ // submenu.
+ this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
+ this.showItem("context-openframeintab", !this.inSrcdocFrame);
+ this.showItem("context-openframe", !this.inSrcdocFrame);
+ this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
+ this.showItem("open-frame-sep", !this.inSrcdocFrame);
+
+ this.showItem("frame-sep", this.inFrame && this.isTextSelected);
+
+ // Hide menu entries for images, show otherwise
+ if (this.inFrame) {
+ if (
+ BrowserUtils.mimeTypeIsTextBased(this.target.ownerDocument.contentType)
+ ) {
+ this.viewFrameSourceElement.removeAttribute("hidden");
+ } else {
+ this.viewFrameSourceElement.setAttribute("hidden", "true");
+ }
+ }
+
+ // BiDi UI
+ this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI);
+ this.showItem(
+ "context-bidi-text-direction-toggle",
+ this.onTextInput && !this.onNumeric && top.gBidiUI
+ );
+ this.showItem(
+ "context-bidi-page-direction-toggle",
+ !this.onTextInput && top.gBidiUI
+ );
+ }
+
+ initSpellingItems() {
+ var canSpell =
+ InlineSpellCheckerUI.canSpellCheck &&
+ !InlineSpellCheckerUI.initialSpellCheckPending &&
+ this.canSpellCheck;
+ let showDictionaries = canSpell && InlineSpellCheckerUI.enabled;
+ var onMisspelling = InlineSpellCheckerUI.overMisspelling;
+ var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
+ this.showItem("spell-check-enabled", canSpell);
+ this.showItem("spell-separator", canSpell);
+ document
+ .getElementById("spell-check-enabled")
+ .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
+
+ this.showItem("spell-add-to-dictionary", onMisspelling);
+ this.showItem("spell-undo-add-to-dictionary", showUndo);
+
+ // suggestion list
+ this.showItem("spell-suggestions-separator", onMisspelling || showUndo);
+ if (onMisspelling) {
+ var suggestionsSeparator = document.getElementById(
+ "spell-add-to-dictionary"
+ );
+ var numsug = InlineSpellCheckerUI.addSuggestionsToMenu(
+ suggestionsSeparator.parentNode,
+ suggestionsSeparator,
+ 5
+ );
+ this.showItem("spell-no-suggestions", numsug == 0);
+ } else {
+ this.showItem("spell-no-suggestions", false);
+ }
+
+ // dictionary list
+ this.showItem("spell-dictionaries", showDictionaries);
+ if (canSpell) {
+ var dictMenu = document.getElementById("spell-dictionaries-menu");
+ var dictSep = document.getElementById("spell-language-separator");
+ let count = InlineSpellCheckerUI.addDictionaryListToMenu(
+ dictMenu,
+ dictSep
+ );
+ this.showItem(dictSep, count > 0);
+ this.showItem("spell-add-dictionaries-main", false);
+ } else if (this.onSpellcheckable) {
+ // when there is no spellchecker but we might be able to spellcheck
+ // add the add to dictionaries item. This will ensure that people
+ // with no dictionaries will be able to download them
+ this.showItem("spell-language-separator", showDictionaries);
+ this.showItem("spell-add-dictionaries-main", showDictionaries);
+ } else {
+ this.showItem("spell-add-dictionaries-main", false);
+ }
+ }
+
+ initClipboardItems() {
+ // Copy depends on whether there is selected text.
+ // Enabling this context menu item is now done through the global
+ // command updating system
+ // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
+ goUpdateGlobalEditMenuItems();
+
+ this.showItem("context-undo", this.onTextInput);
+ this.showItem("context-sep-undo", this.onTextInput);
+ this.showItem("context-cut", this.onTextInput);
+ this.showItem("context-copy", this.isContentSelected || this.onTextInput);
+ this.showItem("context-paste", this.onTextInput);
+ this.showItem("context-delete", this.onTextInput);
+ this.showItem("context-sep-paste", this.onTextInput);
+ this.showItem(
+ "context-selectall",
+ !(
+ this.onLink ||
+ this.onImage ||
+ this.onVideo ||
+ this.onAudio ||
+ this.inSyntheticDoc
+ ) || this.isDesignMode
+ );
+ this.showItem(
+ "context-sep-selectall",
+ !this.inAboutDevtoolsToolbox && this.isContentSelected
+ );
+
+ // XXX dr
+ // ------
+ // nsDocumentViewer.cpp has code to determine whether we're
+ // on a link or an image. we really ought to be using that...
+
+ // Copy email link depends on whether we're on an email link.
+ this.showItem("context-copyemail", this.onMailtoLink);
+
+ // Copy link location depends on whether we're on a non-mailto link.
+ this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
+ this.showItem(
+ "context-sep-copylink",
+ this.onLink && (this.onImage || this.onVideo || this.onAudio)
+ );
+
+ // Copy image contents depends on whether we're on an image.
+ // Note: the element doesn't exist on all platforms, but showItem() takes
+ // care of that by itself.
+ this.showItem("context-copyimage-contents", this.onImage);
+
+ // Copy image location depends on whether we're on an image.
+ this.showItem("context-copyimage", this.onImage);
+ this.showItem("context-copyvideourl", this.onVideo);
+ this.showItem("context-copyaudiourl", this.onAudio);
+ this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
+ this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
+ this.showItem(
+ "context-sep-copyimage",
+ this.onImage || this.onVideo || this.onAudio
+ );
+ }
+
+ initMediaPlayerItems() {
+ var onMedia = this.onVideo || this.onAudio;
+ // Several mutually exclusive items... play/pause, mute/unmute, show/hide
+ this.showItem(
+ "context-media-play",
+ onMedia && (this.target.paused || this.target.ended)
+ );
+ this.showItem(
+ "context-media-pause",
+ onMedia && !this.target.paused && !this.target.ended
+ );
+ this.showItem("context-media-mute", onMedia && !this.target.muted);
+ this.showItem("context-media-unmute", onMedia && this.target.muted);
+ this.showItem(
+ "context-media-playbackrate",
+ onMedia && this.target.duration != Number.POSITIVE_INFINITY
+ );
+ this.showItem("context-media-loop", onMedia);
+ this.showItem(
+ "context-media-showcontrols",
+ onMedia && !this.target.controls
+ );
+ this.showItem(
+ "context-media-hidecontrols",
+ this.target.controls &&
+ (this.onVideo || (this.onAudio && !this.inSyntheticDoc))
+ );
+ this.showItem(
+ "context-video-fullscreen",
+ this.onVideo && !this.target.ownerDocument.fullscreen
+ );
+ {
+ let shouldDisplay =
+ Services.prefs.getBoolPref(
+ "media.videocontrols.picture-in-picture.enabled"
+ ) &&
+ this.onVideo &&
+ !this.target.ownerDocument.fullscreen &&
+ this.target.readyState > 0;
+ this.showItem("context-video-pictureinpicture", shouldDisplay);
+ }
+ this.showItem("context-media-eme-learnmore", this.onDRMMedia);
+ this.showItem("context-media-eme-separator", this.onDRMMedia);
+
+ // Disable them when there isn't a valid media source loaded.
+ if (onMedia) {
+ this.setItemAttr(
+ "context-media-playbackrate-050x",
+ "checked",
+ this.target.playbackRate == 0.5
+ );
+ this.setItemAttr(
+ "context-media-playbackrate-100x",
+ "checked",
+ this.target.playbackRate == 1.0
+ );
+ this.setItemAttr(
+ "context-media-playbackrate-125x",
+ "checked",
+ this.target.playbackRate == 1.25
+ );
+ this.setItemAttr(
+ "context-media-playbackrate-150x",
+ "checked",
+ this.target.playbackRate == 1.5
+ );
+ this.setItemAttr(
+ "context-media-playbackrate-200x",
+ "checked",
+ this.target.playbackRate == 2.0
+ );
+ this.setItemAttr("context-media-loop", "checked", this.target.loop);
+ var hasError =
+ this.target.error != null ||
+ this.target.networkState == this.target.NETWORK_NO_SOURCE;
+ this.setItemAttr("context-media-play", "disabled", hasError);
+ this.setItemAttr("context-media-pause", "disabled", hasError);
+ this.setItemAttr("context-media-mute", "disabled", hasError);
+ this.setItemAttr("context-media-unmute", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError);
+ this.setItemAttr("context-media-showcontrols", "disabled", hasError);
+ this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
+ if (this.onVideo) {
+ let canSaveSnapshot =
+ !this.onDRMMedia &&
+ this.target.readyState >= this.target.HAVE_CURRENT_DATA;
+ this.setItemAttr(
+ "context-video-saveimage",
+ "disabled",
+ !canSaveSnapshot
+ );
+ this.setItemAttr("context-video-fullscreen", "disabled", hasError);
+ this.setItemAttr(
+ "context-video-pictureinpicture",
+ "checked",
+ this.onPiPVideo
+ );
+ this.setItemAttr(
+ "context-video-pictureinpicture",
+ "disabled",
+ !this.onPiPVideo && hasError
+ );
+ }
+ }
+ this.showItem("context-media-sep-commands", onMedia);
+ }
+
+ initClickToPlayItems() {
+ this.showItem("context-ctp-play", this.onCTPPlugin);
+ this.showItem("context-ctp-hide", this.onCTPPlugin);
+ this.showItem("context-sep-ctp", this.onCTPPlugin);
+ }
+
+ initPasswordManagerItems() {
+ let showFill = false;
+ let showGenerate = false;
+ let enableGeneration = Services.logins.isLoggedIn;
+ try {
+ let loginFillInfo = this.contentData && this.contentData.loginFillInfo;
+ let documentURI = this.contentData.documentURIObject;
+
+ // If we could not find a password field we
+ // don't want to show the form fill option.
+ if (
+ !loginFillInfo ||
+ !loginFillInfo.passwordField.found ||
+ documentURI.schemeIs("about") ||
+ this.browser.contentPrincipal.spec ==
+ "resource://pdf.js/web/viewer.html"
+ ) {
+ // Both generation and fill will default to disabled.
+ return;
+ }
+ showFill = true;
+
+ // Disable the fill option if the user hasn't unlocked with their master password
+ // or if the password field or target field are disabled.
+ // XXX: Bug 1529025 to maybe respect signon.rememberSignons.
+ let disableFill =
+ !Services.logins.isLoggedIn ||
+ loginFillInfo.passwordField.disabled ||
+ loginFillInfo.activeField.disabled;
+ this.setItemAttr("fill-login", "disabled", disableFill);
+
+ let onPasswordLikeField = PASSWORD_FIELDNAME_HINTS.includes(
+ loginFillInfo.activeField.fieldNameHint
+ );
+ // Set the correct label for the fill menu
+ let fillMenu = document.getElementById("fill-login");
+ if (onPasswordLikeField) {
+ fillMenu.setAttribute("label", fillMenu.getAttribute("label-password"));
+ fillMenu.setAttribute(
+ "accesskey",
+ fillMenu.getAttribute("accesskey-password")
+ );
+ } else {
+ // On a username field
+ fillMenu.setAttribute("label", fillMenu.getAttribute("label-login"));
+ fillMenu.setAttribute(
+ "accesskey",
+ fillMenu.getAttribute("accesskey-login")
+ );
+ }
+
+ let formOrigin = LoginHelper.getLoginOrigin(documentURI.spec);
+ let isGeneratedPasswordEnabled =
+ LoginHelper.generationAvailable && LoginHelper.generationEnabled;
+ showGenerate =
+ onPasswordLikeField &&
+ isGeneratedPasswordEnabled &&
+ Services.logins.getLoginSavingEnabled(formOrigin);
+
+ if (disableFill) {
+ // No need to update the submenu if the fill item is disabled.
+ return;
+ }
+
+ // Update sub-menu items.
+ let fragment = nsContextMenu.LoginManagerContextMenu.addLoginsToMenu(
+ this.targetIdentifier,
+ this.browser,
+ formOrigin
+ );
+
+ this.showItem("fill-login-no-logins", !fragment);
+
+ if (!fragment) {
+ return;
+ }
+ let popup = document.getElementById("fill-login-popup");
+ let insertBeforeElement = document.getElementById("fill-login-no-logins");
+ popup.insertBefore(fragment, insertBeforeElement);
+ } finally {
+ this.showItem("fill-login", showFill);
+ this.showItem("fill-login-generated-password", showGenerate);
+ this.setItemAttr(
+ "fill-login-generated-password",
+ "disabled",
+ !enableGeneration
+ );
+ this.showItem(
+ "fill-login-and-generated-password-separator",
+ showFill || showGenerate
+ );
+ }
+ }
+
+ initSyncItems() {
+ gSync.updateContentContextMenu(this);
+ }
+
+ openPasswordManager() {
+ LoginHelper.openPasswordManager(window, {
+ entryPoint: "contextmenu",
+ });
+ }
+
+ useGeneratedPassword() {
+ nsContextMenu.LoginManagerContextMenu.useGeneratedPassword(
+ this.targetIdentifier,
+ this.contentData.documentURIObject,
+ this.browser
+ );
+ }
+
+ inspectNode() {
+ return nsContextMenu.DevToolsShim.inspectNode(
+ gBrowser.selectedTab,
+ this.targetIdentifier
+ );
+ }
+
+ inspectA11Y() {
+ return nsContextMenu.DevToolsShim.inspectA11Y(
+ gBrowser.selectedTab,
+ this.targetIdentifier
+ );
+ }
+
+ _openLinkInParameters(extra) {
+ let params = {
+ charset: this.contentData.charSet,
+ originPrincipal: this.principal,
+ originStoragePrincipal: this.storagePrincipal,
+ triggeringPrincipal: this.principal,
+ csp: this.csp,
+ frameID: this.contentData.frameID,
+ };
+ for (let p in extra) {
+ params[p] = extra[p];
+ }
+
+ let referrerInfo = this.onLink
+ ? this.contentData.linkReferrerInfo
+ : this.contentData.referrerInfo;
+ // If we want to change userContextId, we must be sure that we don't
+ // propagate the referrer.
+ if (
+ ("userContextId" in params &&
+ params.userContextId != this.contentData.userContextId) ||
+ this.onPlainTextLink
+ ) {
+ referrerInfo = new ReferrerInfo(
+ referrerInfo.referrerPolicy,
+ false,
+ referrerInfo.originalReferrer
+ );
+ }
+
+ params.referrerInfo = referrerInfo;
+ return params;
+ }
+
+ // Open linked-to URL in a new window.
+ openLink() {
+ openLinkIn(this.linkURL, "window", this._openLinkInParameters());
+ }
+
+ // Open linked-to URL in a new private window.
+ openLinkInPrivateWindow() {
+ openLinkIn(
+ this.linkURL,
+ "window",
+ this._openLinkInParameters({ private: true })
+ );
+ }
+
+ // Open linked-to URL in a new tab.
+ openLinkInTab(event) {
+ let referrerURI = this.contentData.documentURIObject;
+
+ // if its parent allows mixed content and the referring URI passes
+ // a same origin check with the target URI, we can preserve the users
+ // decision of disabling MCB on a page for it's child tabs.
+ let persistAllowMixedContentInChildTab = false;
+
+ if (this.contentData.parentAllowsMixedContent) {
+ const sm = Services.scriptSecurityManager;
+ try {
+ let targetURI = this.linkURI;
+ let isPrivateWin =
+ this.browser.contentPrincipal.originAttributes.privateBrowsingId > 0;
+ sm.checkSameOriginURI(referrerURI, targetURI, false, isPrivateWin);
+ persistAllowMixedContentInChildTab = true;
+ } catch (e) {}
+ }
+
+ let params = {
+ allowMixedContent: persistAllowMixedContentInChildTab,
+ userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
+ };
+
+ openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params));
+ }
+
+ // open URL in current tab
+ openLinkInCurrent() {
+ openLinkIn(this.linkURL, "current", this._openLinkInParameters());
+ }
+
+ // Open frame in a new tab.
+ openFrameInTab() {
+ openLinkIn(this.contentData.docLocation, "tab", {
+ charset: this.contentData.charSet,
+ triggeringPrincipal: this.browser.contentPrincipal,
+ csp: this.browser.csp,
+ referrerInfo: this.contentData.frameReferrerInfo,
+ });
+ }
+
+ // Reload clicked-in frame.
+ reloadFrame(aEvent) {
+ let forceReload = aEvent.shiftKey;
+ this.actor.reloadFrame(this.targetIdentifier, forceReload);
+ }
+
+ // Open clicked-in frame in its own window.
+ openFrame() {
+ openLinkIn(this.contentData.docLocation, "window", {
+ charset: this.contentData.charSet,
+ triggeringPrincipal: this.browser.contentPrincipal,
+ csp: this.browser.csp,
+ referrerInfo: this.contentData.frameReferrerInfo,
+ });
+ }
+
+ // Open clicked-in frame in the same window.
+ showOnlyThisFrame() {
+ urlSecurityCheck(
+ this.contentData.docLocation,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ openWebLinkIn(this.contentData.docLocation, "current", {
+ referrerInfo: this.contentData.frameReferrerInfo,
+ triggeringPrincipal: this.browser.contentPrincipal,
+ });
+ }
+
+ // View Partial Source
+ viewPartialSource() {
+ let { browser } = this;
+ let openSelectionFn = function() {
+ let tabBrowser = gBrowser;
+ const inNewWindow = !Services.prefs.getBoolPref("view_source.tab");
+ // In the case of popups, we need to find a non-popup browser window.
+ // We might also not have a tabBrowser reference (if this isn't in a
+ // a tabbrowser scope) or might have a fake/stub tabbrowser reference
+ // (in the sidebar). Deal with those cases:
+ if (!tabBrowser || !tabBrowser.loadOneTab || !window.toolbar.visible) {
+ // This returns only non-popup browser windows by default.
+ let browserWindow = BrowserWindowTracker.getTopWindow();
+ tabBrowser = browserWindow.gBrowser;
+ }
+ let relatedToCurrent = gBrowser && gBrowser.selectedBrowser == browser;
+ let tab = tabBrowser.loadOneTab("about:blank", {
+ relatedToCurrent,
+ inBackground: inNewWindow,
+ skipAnimation: inNewWindow,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ const viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
+ if (inNewWindow) {
+ tabBrowser.hideTab(tab);
+ tabBrowser.replaceTabsWithWindow(tab);
+ }
+ return viewSourceBrowser;
+ };
+
+ top.gViewSourceUtils.viewPartialSourceInBrowser(
+ this.actor.browsingContext,
+ openSelectionFn
+ );
+ }
+
+ // Open new "view source" window with the frame's URL.
+ viewFrameSource() {
+ BrowserViewSourceOfDocument({
+ browser: this.browser,
+ URL: this.contentData.docLocation,
+ outerWindowID: this.frameOuterWindowID,
+ });
+ }
+
+ viewInfo() {
+ BrowserPageInfo(
+ this.contentData.docLocation,
+ null,
+ null,
+ null,
+ this.browser
+ );
+ }
+
+ viewImageInfo() {
+ BrowserPageInfo(
+ this.contentData.docLocation,
+ "mediaTab",
+ this.imageInfo,
+ null,
+ this.browser
+ );
+ }
+
+ viewImageDesc(e) {
+ urlSecurityCheck(
+ this.imageDescURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ openUILink(this.imageDescURL, e, {
+ referrerInfo: this.contentData.referrerInfo,
+ triggeringPrincipal: this.principal,
+ csp: this.csp,
+ });
+ }
+
+ viewFrameInfo() {
+ BrowserPageInfo(
+ this.contentData.docLocation,
+ null,
+ null,
+ this.actor.browsingContext,
+ this.browser
+ );
+ }
+
+ reloadImage() {
+ urlSecurityCheck(
+ this.mediaURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ this.actor.reloadImage(this.targetIdentifier);
+ }
+
+ _canvasToBlobURL(targetIdentifier) {
+ return this.actor.canvasToBlobURL(targetIdentifier);
+ }
+
+ // Change current window to the URL of the image, video, or audio.
+ viewMedia(e) {
+ let referrerInfo = this.contentData.referrerInfo;
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ if (this.onCanvas) {
+ this._canvasToBlobURL(this.targetIdentifier).then(function(blobURL) {
+ openUILink(blobURL, e, {
+ referrerInfo,
+ triggeringPrincipal: systemPrincipal,
+ });
+ }, Cu.reportError);
+ } else {
+ urlSecurityCheck(
+ this.mediaURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ openUILink(this.mediaURL, e, {
+ referrerInfo,
+ forceAllowDataURI: true,
+ triggeringPrincipal: this.principal,
+ csp: this.csp,
+ });
+ }
+ }
+
+ saveVideoFrameAsImage() {
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+
+ let name = "";
+ if (this.mediaURL) {
+ try {
+ let uri = makeURI(this.mediaURL);
+ let url = uri.QueryInterface(Ci.nsIURL);
+ if (url.fileBaseName) {
+ name = decodeURI(url.fileBaseName) + ".jpg";
+ }
+ } catch (e) {}
+ }
+ if (!name) {
+ name = "snapshot.jpg";
+ }
+
+ // Cache this because we fetch the data async
+ let referrerInfo = this.contentData.referrerInfo;
+ let cookieJarSettings = this.contentData.cookieJarSettings;
+
+ this.actor.saveVideoFrameAsImage(this.targetIdentifier).then(dataURL => {
+ // FIXME can we switch this to a blob URL?
+ internalSave(
+ dataURL,
+ null, // document
+ name,
+ null, // content disposition
+ "image/jpeg", // content type - keep in sync with ContextMenuChild!
+ true, // bypass cache
+ "SaveImageTitle",
+ null, // chosen data
+ referrerInfo,
+ cookieJarSettings,
+ null, // initiating doc
+ false, // don't skip prompt for where to save
+ null, // cache key
+ isPrivate,
+ this.principal
+ );
+ });
+ }
+
+ leaveDOMFullScreen() {
+ document.exitFullscreen();
+ }
+
+ // Change current window to the URL of the background image.
+ viewBGImage(e) {
+ urlSecurityCheck(
+ this.bgImageURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+
+ openUILink(this.bgImageURL, e, {
+ referrerInfo: this.contentData.referrerInfo,
+ forceAllowDataURI: true,
+ triggeringPrincipal: this.principal,
+ csp: this.csp,
+ });
+ }
+
+ setDesktopBackground() {
+ if (!Services.policies.isAllowed("setDesktopBackground")) {
+ return;
+ }
+
+ this.actor
+ .setAsDesktopBackground(this.targetIdentifier)
+ .then(({ failed, dataURL, imageName }) => {
+ if (failed) {
+ return;
+ }
+
+ let image = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "img"
+ );
+ image.src = dataURL;
+
+ // Confirm since it's annoying if you hit this accidentally.
+ const kDesktopBackgroundURL =
+ "chrome://browser/content/setDesktopBackground.xhtml";
+
+ if (AppConstants.platform == "macosx") {
+ // On Mac, the Set Desktop Background window is not modal.
+ // Don't open more than one Set Desktop Background window.
+ let dbWin = Services.wm.getMostRecentWindow(
+ "Shell:SetDesktopBackground"
+ );
+ if (dbWin) {
+ dbWin.gSetBackground.init(image, imageName);
+ dbWin.focus();
+ } else {
+ openDialog(
+ kDesktopBackgroundURL,
+ "",
+ "centerscreen,chrome,dialog=no,dependent,resizable=no",
+ image,
+ imageName
+ );
+ }
+ } else {
+ // On non-Mac platforms, the Set Wallpaper dialog is modal.
+ openDialog(
+ kDesktopBackgroundURL,
+ "",
+ "centerscreen,chrome,dialog,modal,dependent",
+ image,
+ imageName
+ );
+ }
+ });
+ }
+
+ // Save URL of clicked-on frame.
+ saveFrame() {
+ saveBrowser(this.browser, false, this.frameBrowsingContext);
+ }
+
+ // Helper function to wait for appropriate MIME-type headers and
+ // then prompt the user with a file picker
+ saveHelper(
+ linkURL,
+ linkText,
+ dialogTitle,
+ bypassCache,
+ doc,
+ referrerInfo,
+ cookieJarSettings,
+ windowID,
+ linkDownload,
+ isContentWindowPrivate
+ ) {
+ // canonical def in nsURILoader.h
+ const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
+
+ // an object to proxy the data through to
+ // nsIExternalHelperAppService.doContent, which will wait for the
+ // appropriate MIME-type headers and then prompt the user with a
+ // file picker
+ function saveAsListener(principal) {
+ this._triggeringPrincipal = principal;
+ }
+ saveAsListener.prototype = {
+ extListener: null,
+
+ onStartRequest: function saveLinkAs_onStartRequest(aRequest) {
+ // if the timer fired, the error status will have been caused by that,
+ // and we'll be restarting in onStopRequest, so no reason to notify
+ // the user
+ if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
+ return;
+ }
+
+ timer.cancel();
+
+ // some other error occured; notify the user...
+ if (!Components.isSuccessCode(aRequest.status)) {
+ try {
+ const bundle = Services.strings.createBundle(
+ "chrome://mozapps/locale/downloads/downloads.properties"
+ );
+
+ const title = bundle.GetStringFromName("downloadErrorAlertTitle");
+ let msg = bundle.GetStringFromName("downloadErrorGeneric");
+
+ try {
+ const channel = aRequest.QueryInterface(Ci.nsIChannel);
+ const reason = channel.loadInfo.requestBlockingReason;
+ if (
+ reason == Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST
+ ) {
+ try {
+ const properties = channel.QueryInterface(Ci.nsIPropertyBag);
+ const id = properties.getProperty("cancelledByExtension");
+ msg = bundle.formatStringFromName("downloadErrorBlockedBy", [
+ WebExtensionPolicy.getByID(id).name,
+ ]);
+ } catch (err) {
+ // "cancelledByExtension" doesn't have to be available.
+ msg = bundle.GetStringFromName("downloadErrorExtension");
+ }
+ }
+ } catch (ex) {}
+
+ let window = Services.wm.getOuterWindowWithId(windowID);
+ Services.prompt.alert(window, title, msg);
+ } catch (ex) {}
+ return;
+ }
+
+ let extHelperAppSvc = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsIExternalHelperAppService);
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ this.extListener = extHelperAppSvc.doContent(
+ channel.contentType,
+ aRequest,
+ null,
+ true,
+ window
+ );
+ this.extListener.onStartRequest(aRequest);
+ },
+
+ onStopRequest: function saveLinkAs_onStopRequest(aRequest, aStatusCode) {
+ if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
+ // do it the old fashioned way, which will pick the best filename
+ // it can without waiting.
+ saveURL(
+ linkURL,
+ linkText,
+ dialogTitle,
+ bypassCache,
+ false,
+ referrerInfo,
+ cookieJarSettings,
+ doc,
+ isContentWindowPrivate,
+ this._triggeringPrincipal
+ );
+ }
+ if (this.extListener) {
+ this.extListener.onStopRequest(aRequest, aStatusCode);
+ }
+ },
+
+ onDataAvailable: function saveLinkAs_onDataAvailable(
+ aRequest,
+ aInputStream,
+ aOffset,
+ aCount
+ ) {
+ this.extListener.onDataAvailable(
+ aRequest,
+ aInputStream,
+ aOffset,
+ aCount
+ );
+ },
+ };
+
+ function callbacks() {}
+ callbacks.prototype = {
+ getInterface: function sLA_callbacks_getInterface(aIID) {
+ if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
+ // If the channel demands authentication prompt, we must cancel it
+ // because the save-as-timer would expire and cancel the channel
+ // before we get credentials from user. Both authentication dialog
+ // and save as dialog would appear on the screen as we fall back to
+ // the old fashioned way after the timeout.
+ timer.cancel();
+ channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+ };
+
+ // if it we don't have the headers after a short time, the user
+ // won't have received any feedback from their click. that's bad. so
+ // we give up waiting for the filename.
+ function timerCallback() {}
+ timerCallback.prototype = {
+ notify: function sLA_timer_notify(aTimer) {
+ channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
+ },
+ };
+
+ // setting up a new channel for 'right click - save link as ...'
+ var channel = NetUtil.newChannel({
+ uri: makeURI(linkURL),
+ loadingPrincipal: this.principal,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ });
+
+ if (linkDownload) {
+ channel.contentDispositionFilename = linkDownload;
+ }
+ if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
+ let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ channel.setPrivate(docIsPrivate);
+ }
+ channel.notificationCallbacks = new callbacks();
+
+ let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
+
+ if (bypassCache) {
+ flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ }
+
+ if (channel instanceof Ci.nsICachingChannel) {
+ flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
+ }
+
+ channel.loadFlags |= flags;
+
+ if (channel instanceof Ci.nsIHttpChannel) {
+ channel.referrerInfo = referrerInfo;
+ if (channel instanceof Ci.nsIHttpChannelInternal) {
+ channel.forceAllowThirdPartyCookie = true;
+ }
+
+ channel.loadInfo.cookieJarSettings = cookieJarSettings;
+ }
+
+ // fallback to the old way if we don't see the headers quickly
+ var timeToWait = Services.prefs.getIntPref(
+ "browser.download.saveLinkAsFilenameTimeout"
+ );
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(
+ new timerCallback(),
+ timeToWait,
+ timer.TYPE_ONE_SHOT
+ );
+
+ // kick off the channel with our proxy object as the listener
+ channel.asyncOpen(new saveAsListener(this.principal));
+ }
+
+ // Save URL of clicked-on link.
+ saveLink() {
+ let referrerInfo = this.onLink
+ ? this.contentData.linkReferrerInfo
+ : this.contentData.referrerInfo;
+
+ let isContentWindowPrivate = this.ownerDoc.isPrivate;
+ this.saveHelper(
+ this.linkURL,
+ this.linkTextStr,
+ null,
+ true,
+ this.ownerDoc,
+ referrerInfo,
+ this.contentData.cookieJarSettings,
+ this.frameOuterWindowID,
+ this.linkDownload,
+ isContentWindowPrivate
+ );
+ }
+
+ // Backwards-compatibility wrapper
+ saveImage() {
+ if (this.onCanvas || this.onImage) {
+ this.saveMedia();
+ }
+ }
+
+ // Save URL of the clicked upon image, video, or audio.
+ saveMedia() {
+ let doc = this.ownerDoc;
+ let isContentWindowPrivate = this.ownerDoc.isPrivate;
+ let referrerInfo = this.contentData.referrerInfo;
+ let cookieJarSettings = this.contentData.cookieJarSettings;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ if (this.onCanvas) {
+ // Bypass cache, since it's a data: URL.
+ this._canvasToBlobURL(this.targetIdentifier).then(function(blobURL) {
+ internalSave(
+ blobURL,
+ null, // document
+ "canvas.png",
+ null, // content disposition
+ "image/png", // _canvasToBlobURL uses image/png by default.
+ true, // bypass cache
+ "SaveImageTitle",
+ null, // chosen data
+ referrerInfo,
+ cookieJarSettings,
+ null, // initiating doc
+ false, // don't skip prompt for where to save
+ null, // cache key
+ isPrivate,
+ document.nodePrincipal /* system, because blob: */
+ );
+ }, Cu.reportError);
+ } else if (this.onImage) {
+ urlSecurityCheck(this.mediaURL, this.principal);
+ internalSave(
+ this.mediaURL,
+ null, // document
+ null, // file name; we'll take it from the URL
+ this.contentData.contentDisposition,
+ this.contentData.contentType,
+ false, // do not bypass the cache
+ "SaveImageTitle",
+ null, // chosen data
+ referrerInfo,
+ cookieJarSettings,
+ null, // initiating doc
+ false, // don't skip prompt for where to save
+ null, // cache key
+ isPrivate,
+ this.principal
+ );
+ } else if (this.onVideo || this.onAudio) {
+ var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
+ this.saveHelper(
+ this.mediaURL,
+ null,
+ dialogTitle,
+ false,
+ doc,
+ referrerInfo,
+ cookieJarSettings,
+ this.frameOuterWindowID,
+ "",
+ isContentWindowPrivate
+ );
+ }
+ }
+
+ // Backwards-compatibility wrapper
+ sendImage() {
+ if (this.onCanvas || this.onImage) {
+ this.sendMedia();
+ }
+ }
+
+ sendMedia() {
+ MailIntegration.sendMessage(this.mediaURL, "");
+ }
+
+ playPlugin() {
+ this.actor.pluginCommand("play", this.targetIdentifier);
+ }
+
+ hidePlugin() {
+ this.actor.pluginCommand("hide", this.targetIdentifier);
+ }
+
+ // Generate email address and put it on clipboard.
+ copyEmail() {
+ // Copy the comma-separated list of email addresses only.
+ // There are other ways of embedding email addresses in a mailto:
+ // link, but such complex parsing is beyond us.
+ var url = this.linkURL;
+ var qmark = url.indexOf("?");
+ var addresses;
+
+ // 7 == length of "mailto:"
+ addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7);
+
+ // Let's try to unescape it using a character set
+ // in case the address is not ASCII.
+ try {
+ addresses = Services.textToSubURI.unEscapeURIForUI(addresses);
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(addresses);
+ }
+
+ copyLink() {
+ // If we're in a view source tab, remove the view-source: prefix
+ let linkURL = this.linkURL.replace(/^view-source:/, "");
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(linkURL);
+ }
+
+ addKeywordForSearchField() {
+ this.actor.getSearchFieldBookmarkData(this.targetIdentifier).then(data => {
+ let title = gNavigatorBundle.getFormattedString(
+ "addKeywordTitleAutoFill",
+ [data.title]
+ );
+ PlacesUIUtils.showBookmarkDialog(
+ {
+ action: "add",
+ type: "bookmark",
+ uri: makeURI(data.spec),
+ title,
+ keyword: "",
+ postData: data.postData,
+ charSet: data.charset,
+ hiddenRows: ["location", "tags"],
+ },
+ window
+ );
+ });
+ }
+
+ /**
+ * Utilities
+ */
+
+ /**
+ * Show/hide one item (specified via name or the item element itself).
+ * If the element is not found, then this function finishes silently.
+ *
+ * @param {Element|String} aItemOrId The item element or the name of the element
+ * to show.
+ * @param {Boolean} aShow Set to true to show the item, false to hide it.
+ */
+ showItem(aItemOrId, aShow) {
+ var item =
+ aItemOrId.constructor == String
+ ? document.getElementById(aItemOrId)
+ : aItemOrId;
+ if (item) {
+ item.hidden = !aShow;
+ }
+ }
+
+ // Set given attribute of specified context-menu item. If the
+ // value is null, then it removes the attribute (which works
+ // nicely for the disabled attribute).
+ setItemAttr(aID, aAttr, aVal) {
+ var elem = document.getElementById(aID);
+ if (elem) {
+ if (aVal == null) {
+ // null indicates attr should be removed.
+ elem.removeAttribute(aAttr);
+ } else {
+ // Set attr=val.
+ elem.setAttribute(aAttr, aVal);
+ }
+ }
+ }
+
+ // Temporary workaround for DOM api not yet implemented by XUL nodes.
+ cloneNode(aItem) {
+ // Create another element like the one we're cloning.
+ var node = document.createElement(aItem.tagName);
+
+ // Copy attributes from argument item to the new one.
+ var attrs = aItem.attributes;
+ for (var i = 0; i < attrs.length; i++) {
+ var attr = attrs.item(i);
+ node.setAttribute(attr.nodeName, attr.nodeValue);
+ }
+
+ // Voila!
+ return node;
+ }
+
+ getLinkURI() {
+ try {
+ return makeURI(this.linkURL);
+ } catch (ex) {
+ // e.g. empty URL string
+ }
+
+ return null;
+ }
+
+ // Kept for addon compat
+ linkText() {
+ return this.linkTextStr;
+ }
+
+ // Determines whether or not the separator with the specified ID should be
+ // shown or not by determining if there are any non-hidden items between it
+ // and the previous separator.
+ shouldShowSeparator(aSeparatorID) {
+ var separator = document.getElementById(aSeparatorID);
+ if (separator) {
+ var sibling = separator.previousSibling;
+ while (sibling && sibling.localName != "menuseparator") {
+ if (!sibling.hidden) {
+ return true;
+ }
+ sibling = sibling.previousSibling;
+ }
+ }
+ return false;
+ }
+
+ addDictionaries() {
+ var uri = formatURL("browser.dictionaries.download.url", true);
+
+ var locale = "-";
+ try {
+ locale = Services.prefs.getComplexValue(
+ "intl.accept_languages",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {}
+
+ var version = "-";
+ try {
+ version = Services.appinfo.version;
+ } catch (e) {}
+
+ uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
+
+ var newWindowPref = Services.prefs.getIntPref(
+ "browser.link.open_newwindow"
+ );
+ var where = newWindowPref == 3 ? "tab" : "window";
+
+ openTrustedLinkIn(uri, where);
+ }
+
+ bookmarkThisPage() {
+ window.top.PlacesCommandHook.bookmarkPage().catch(Cu.reportError);
+ }
+
+ bookmarkLink() {
+ window.top.PlacesCommandHook.bookmarkLink(
+ this.linkURL,
+ this.linkTextStr
+ ).catch(Cu.reportError);
+ }
+
+ addBookmarkForFrame() {
+ let uri = this.contentData.documentURIObject;
+
+ this.actor.getFrameTitle(this.targetIdentifier).then(title => {
+ window.top.PlacesCommandHook.bookmarkLink(uri.spec, title).catch(
+ Cu.reportError
+ );
+ });
+ }
+
+ savePageAs() {
+ saveBrowser(this.browser);
+ }
+
+ printFrame() {
+ PrintUtils.startPrintWindow(
+ "context_print_frame",
+ this.actor.browsingContext,
+ { printFrameOnly: true }
+ );
+ }
+
+ printSelection() {
+ PrintUtils.startPrintWindow(
+ "context_print_selection",
+ this.actor.browsingContext,
+ { printSelectionOnly: true }
+ );
+ }
+
+ switchPageDirection() {
+ gBrowser.selectedBrowser.sendMessageToActor(
+ "SwitchDocumentDirection",
+ {},
+ "SwitchDocumentDirection",
+ "roots"
+ );
+ }
+
+ mediaCommand(command, data) {
+ this.actor.mediaCommand(this.targetIdentifier, command, data);
+ }
+
+ copyMediaLocation() {
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(this.mediaURL);
+ }
+
+ drmLearnMore(aEvent) {
+ let drmInfoURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "drm-content";
+ let dest = whereToOpenLink(aEvent);
+ // Don't ever want this to open in the same tab as it'll unload the
+ // DRM'd video, which is going to be a bad idea in most cases.
+ if (dest == "current") {
+ dest = "tab";
+ }
+ openTrustedLinkIn(drmInfoURL, dest);
+ }
+
+ get imageURL() {
+ if (this.onImage) {
+ return this.mediaURL;
+ }
+ return "";
+ }
+
+ // Formats the 'Search <engine> for "<selection or link text>"' context menu.
+ showAndFormatSearchContextItem() {
+ let menuItem = document.getElementById("context-searchselect");
+ let menuItemPrivate = document.getElementById(
+ "context-searchselect-private"
+ );
+ if (!Services.search.isInitialized) {
+ menuItem.hidden = true;
+ menuItemPrivate.hidden = true;
+ return;
+ }
+ const docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ const privatePref = "browser.search.separatePrivateDefault.ui.enabled";
+ let showSearchSelect =
+ !this.inAboutDevtoolsToolbox &&
+ (this.isTextSelected || this.onLink) &&
+ !this.onImage;
+ // Don't show the private search item when we're already in a private
+ // browsing window.
+ let showPrivateSearchSelect =
+ showSearchSelect &&
+ !docIsPrivate &&
+ Services.prefs.getBoolPref(privatePref);
+
+ menuItem.hidden = !showSearchSelect;
+ menuItemPrivate.hidden = !showPrivateSearchSelect;
+ // If we're not showing the menu items, we can skip formatting the labels.
+ if (!showSearchSelect) {
+ return;
+ }
+
+ let selectedText = this.isTextSelected
+ ? this.textSelected
+ : this.linkTextStr;
+
+ // Store searchTerms in context menu item so we know what to search onclick
+ menuItem.searchTerms = menuItemPrivate.searchTerms = selectedText;
+ menuItem.principal = menuItemPrivate.principal = this.principal;
+ menuItem.csp = menuItemPrivate.csp = this.csp;
+
+ // Copied to alert.js' prefillAlertInfo().
+ // If the JS character after our truncation point is a trail surrogate,
+ // include it in the truncated string to avoid splitting a surrogate pair.
+ if (selectedText.length > 15) {
+ let truncLength = 15;
+ let truncChar = selectedText[15].charCodeAt(0);
+ if (truncChar >= 0xdc00 && truncChar <= 0xdfff) {
+ truncLength++;
+ }
+ selectedText = selectedText.substr(0, truncLength) + this.ellipsis;
+ }
+
+ // format "Search <engine> for <selection>" string to show in menu
+ let engineName = Services.search.defaultEngine.name;
+ let privateEngineName = Services.search.defaultPrivateEngine.name;
+ menuItem.usePrivate = docIsPrivate;
+ let menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", [
+ docIsPrivate ? privateEngineName : engineName,
+ selectedText,
+ ]);
+ menuItem.label = menuLabel;
+ menuItem.accessKey = gNavigatorBundle.getString(
+ "contextMenuSearch.accesskey"
+ );
+
+ if (showPrivateSearchSelect) {
+ let otherEngine = engineName != privateEngineName;
+ let accessKey = "contextMenuPrivateSearch.accesskey";
+ if (otherEngine) {
+ menuItemPrivate.label = gNavigatorBundle.getFormattedString(
+ "contextMenuPrivateSearchOtherEngine",
+ [privateEngineName]
+ );
+ accessKey = "contextMenuPrivateSearchOtherEngine.accesskey";
+ } else {
+ menuItemPrivate.label = gNavigatorBundle.getString(
+ "contextMenuPrivateSearch"
+ );
+ }
+ menuItemPrivate.accessKey = gNavigatorBundle.getString(accessKey);
+ }
+ }
+
+ createContainerMenu(aEvent) {
+ let createMenuOptions = {
+ isContextMenu: true,
+ excludeUserContextId: this.contentData.userContextId,
+ };
+ return createUserContextMenu(aEvent, createMenuOptions);
+ }
+
+ doCustomCommand(generatedItemId, handlingUserInput) {
+ this.actor.doCustomCommand(generatedItemId, handlingUserInput);
+ }
+}
+
+XPCOMUtils.defineLazyModuleGetters(nsContextMenu, {
+ LoginManagerContextMenu: "resource://gre/modules/LoginManagerContextMenu.jsm",
+ DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
+});
diff --git a/browser/base/content/overrides/app-license.html b/browser/base/content/overrides/app-license.html
new file mode 100644
index 0000000000..e7a158c792
--- /dev/null
+++ b/browser/base/content/overrides/app-license.html
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ <p><b>Binaries</b> of this product have been made available to you by the
+ <a href="http://www.mozilla.org/">Mozilla Project</a> under the Mozilla
+ Public License 2.0 (MPL). <a href="about:rights">Know your rights</a>.</p>
diff --git a/browser/base/content/pageinfo/pageInfo.css b/browser/base/content/pageinfo/pageInfo.css
new file mode 100644
index 0000000000..44dd3d7c18
--- /dev/null
+++ b/browser/base/content/pageinfo/pageInfo.css
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#viewGroup > radio > .radio-label-box {
+ -moz-box-orient: vertical;
+ -moz-box-align: center;
+}
+
+/* Hide the radio button for the section headers */
+#viewGroup > radio > .radio-check {
+ display: none;
+}
+
+#thepreviewimage {
+ display: block;
+/* This following entry can be removed when Bug 522850 is fixed. */
+ min-width: 1px;
+}
+
+table {
+ border-spacing: 0;
+}
+
+.tableSeparator {
+ height: 6px;
+}
+
+th, td {
+ padding: 0;
+}
+
+th {
+ font: inherit;
+ text-align: start;
+ padding-inline-end: .5em;
+}
+
+/*
+ Make the first column shrink to its min-content, except for #securityTable
+ which has full sentences in its first column.
+*/
+table:not(#securityTable) th {
+ width: 0;
+}
+
+th > label,
+td > input,
+.table-split-column {
+ width: 100%;
+ margin-block: 1px 4px;
+}
+
+.table-split-column {
+ display: flex;
+ align-items: center;
+}
+
+.table-split-column > label,
+.table-split-column > input {
+ flex-grow: 1;
+}
+
+.table-split-column > button {
+ flex-shrink: 0;
+}
+
+#hostText {
+ -moz-box-flex: 1;
+ margin-top: 1px; /* same margin as adjacent label */
+}
diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js
new file mode 100644
index 0000000000..74a5d28a31
--- /dev/null
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -0,0 +1,1135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ../../../../toolkit/content/globalOverlay.js */
+/* import-globals-from ../../../../toolkit/content/contentAreaUtils.js */
+/* import-globals-from ../../../../toolkit/content/treeUtils.js */
+/* import-globals-from ../utilityOverlay.js */
+/* import-globals-from permissions.js */
+/* import-globals-from security.js */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ E10SUtils: "resource://gre/modules/E10SUtils.jsm",
+});
+
+// define a js object to implement nsITreeView
+function pageInfoTreeView(treeid, copycol) {
+ // copycol is the index number for the column that we want to add to
+ // the copy-n-paste buffer when the user hits accel-c
+ this.treeid = treeid;
+ this.copycol = copycol;
+ this.rows = 0;
+ this.tree = null;
+ this.data = [];
+ this.selection = null;
+ this.sortcol = -1;
+ this.sortdir = false;
+}
+
+pageInfoTreeView.prototype = {
+ set rowCount(c) {
+ throw new Error("rowCount is a readonly property");
+ },
+ get rowCount() {
+ return this.rows;
+ },
+
+ setTree(tree) {
+ this.tree = tree;
+ },
+
+ getCellText(row, column) {
+ // row can be null, but js arrays are 0-indexed.
+ // colidx cannot be null, but can be larger than the number
+ // of columns in the array. In this case it's the fault of
+ // whoever typoed while calling this function.
+ return this.data[row][column.index] || "";
+ },
+
+ setCellValue(row, column, value) {},
+
+ setCellText(row, column, value) {
+ this.data[row][column.index] = value;
+ },
+
+ addRow(row) {
+ this.rows = this.data.push(row);
+ this.rowCountChanged(this.rows - 1, 1);
+ if (this.selection.count == 0 && this.rowCount && !gImageElement) {
+ this.selection.select(0);
+ }
+ },
+
+ addRows(rows) {
+ for (let row of rows) {
+ this.addRow(row);
+ }
+ },
+
+ rowCountChanged(index, count) {
+ this.tree.rowCountChanged(index, count);
+ },
+
+ invalidate() {
+ this.tree.invalidate();
+ },
+
+ clear() {
+ if (this.tree) {
+ this.tree.rowCountChanged(0, -this.rows);
+ }
+ this.rows = 0;
+ this.data = [];
+ },
+
+ onPageMediaSort(columnname) {
+ var tree = document.getElementById(this.treeid);
+ var treecol = tree.columns.getNamedColumn(columnname);
+
+ this.sortdir = gTreeUtils.sort(
+ tree,
+ this,
+ this.data,
+ treecol.index,
+ function textComparator(a, b) {
+ return (a || "").toLowerCase().localeCompare((b || "").toLowerCase());
+ },
+ this.sortcol,
+ this.sortdir
+ );
+
+ for (let col of tree.columns) {
+ col.element.removeAttribute("sortActive");
+ col.element.removeAttribute("sortDirection");
+ }
+ treecol.element.setAttribute("sortActive", "true");
+ treecol.element.setAttribute(
+ "sortDirection",
+ this.sortdir ? "ascending" : "descending"
+ );
+
+ this.sortcol = treecol.index;
+ },
+
+ getRowProperties(row) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ isContainer(index) {
+ return false;
+ },
+ isContainerOpen(index) {
+ return false;
+ },
+ isSeparator(index) {
+ return false;
+ },
+ isSorted() {
+ return this.sortcol > -1;
+ },
+ canDrop(index, orientation) {
+ return false;
+ },
+ drop(row, orientation) {
+ return false;
+ },
+ getParentIndex(index) {
+ return 0;
+ },
+ hasNextSibling(index, after) {
+ return false;
+ },
+ getLevel(index) {
+ return 0;
+ },
+ getImageSrc(row, column) {},
+ getCellValue(row, column) {
+ let col = column != null ? column : this.copycol;
+ return row < 0 || col < 0 ? "" : this.data[row][col] || "";
+ },
+ toggleOpenState(index) {},
+ cycleHeader(col) {},
+ selectionChanged() {},
+ cycleCell(row, column) {},
+ isEditable(row, column) {
+ return false;
+ },
+};
+
+// mmm, yummy. global variables.
+var gDocInfo = null;
+var gImageElement = null;
+
+// column number to help using the data array
+const COL_IMAGE_ADDRESS = 0;
+const COL_IMAGE_TYPE = 1;
+const COL_IMAGE_SIZE = 2;
+const COL_IMAGE_ALT = 3;
+const COL_IMAGE_COUNT = 4;
+const COL_IMAGE_NODE = 5;
+const COL_IMAGE_BG = 6;
+
+// column number to copy from, second argument to pageInfoTreeView's constructor
+const COPYCOL_NONE = -1;
+const COPYCOL_META_CONTENT = 1;
+const COPYCOL_IMAGE = COL_IMAGE_ADDRESS;
+
+// one nsITreeView for each tree in the window
+var gMetaView = new pageInfoTreeView("metatree", COPYCOL_META_CONTENT);
+var gImageView = new pageInfoTreeView("imagetree", COPYCOL_IMAGE);
+
+gImageView.getCellProperties = function(row, col) {
+ var data = gImageView.data[row];
+ var item = gImageView.data[row][COL_IMAGE_NODE];
+ var props = "";
+ if (
+ !checkProtocol(data) ||
+ item instanceof HTMLEmbedElement ||
+ (item instanceof HTMLObjectElement && !item.type.startsWith("image/"))
+ ) {
+ props += "broken";
+ }
+
+ if (col.element.id == "image-address") {
+ props += " ltr";
+ }
+
+ return props;
+};
+
+gImageView.onPageMediaSort = function(columnname) {
+ var tree = document.getElementById(this.treeid);
+ var treecol = tree.columns.getNamedColumn(columnname);
+
+ var comparator;
+ var index = treecol.index;
+ if (index == COL_IMAGE_SIZE || index == COL_IMAGE_COUNT) {
+ comparator = function numComparator(a, b) {
+ return a - b;
+ };
+ } else {
+ comparator = function textComparator(a, b) {
+ return (a || "").toLowerCase().localeCompare((b || "").toLowerCase());
+ };
+ }
+
+ this.sortdir = gTreeUtils.sort(
+ tree,
+ this,
+ this.data,
+ index,
+ comparator,
+ this.sortcol,
+ this.sortdir
+ );
+
+ for (let col of tree.columns) {
+ col.element.removeAttribute("sortActive");
+ col.element.removeAttribute("sortDirection");
+ }
+ treecol.element.setAttribute("sortActive", "true");
+ treecol.element.setAttribute(
+ "sortDirection",
+ this.sortdir ? "ascending" : "descending"
+ );
+
+ this.sortcol = index;
+};
+
+var gImageHash = {};
+
+// localized strings (will be filled in when the document is loaded)
+const MEDIA_STRINGS = {};
+let SIZE_UNKNOWN = "";
+let ALT_NOT_SET = "";
+
+// a number of services I'll need later
+// the cache services
+const nsICacheStorageService = Ci.nsICacheStorageService;
+const nsICacheStorage = Ci.nsICacheStorage;
+const cacheService = Cc[
+ "@mozilla.org/netwerk/cache-storage-service;1"
+].getService(nsICacheStorageService);
+
+var loadContextInfo = Services.loadContextInfo.fromLoadContext(
+ window.docShell.QueryInterface(Ci.nsILoadContext),
+ false
+);
+var diskStorage = cacheService.diskCacheStorage(loadContextInfo, false);
+
+const nsICookiePermission = Ci.nsICookiePermission;
+
+const nsICertificateDialogs = Ci.nsICertificateDialogs;
+const CERTIFICATEDIALOGS_CONTRACTID = "@mozilla.org/nsCertificateDialogs;1";
+
+// clipboard helper
+function getClipboardHelper() {
+ try {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ } catch (e) {
+ // do nothing, later code will handle the error
+ return null;
+ }
+}
+const gClipboardHelper = getClipboardHelper();
+
+/* Called when PageInfo window is loaded. Arguments are:
+ * window.arguments[0] - (optional) an object consisting of
+ * - doc: (optional) document to use for source. if not provided,
+ * the calling window's document will be used
+ * - initialTab: (optional) id of the inital tab to display
+ */
+async function onLoadPageInfo() {
+ [
+ SIZE_UNKNOWN,
+ ALT_NOT_SET,
+ MEDIA_STRINGS.img,
+ MEDIA_STRINGS["bg-img"],
+ MEDIA_STRINGS["border-img"],
+ MEDIA_STRINGS["list-img"],
+ MEDIA_STRINGS.cursor,
+ MEDIA_STRINGS.object,
+ MEDIA_STRINGS.embed,
+ MEDIA_STRINGS.link,
+ MEDIA_STRINGS.input,
+ MEDIA_STRINGS.video,
+ MEDIA_STRINGS.audio,
+ ] = await document.l10n.formatValues([
+ "image-size-unknown",
+ "not-set-alternative-text",
+ "media-img",
+ "media-bg-img",
+ "media-border-img",
+ "media-list-img",
+ "media-cursor",
+ "media-object",
+ "media-embed",
+ "media-link",
+ "media-input",
+ "media-video",
+ "media-audio",
+ ]);
+
+ const args =
+ "arguments" in window &&
+ window.arguments.length >= 1 &&
+ window.arguments[0];
+
+ // Init media view
+ document.getElementById("imagetree").view = gImageView;
+
+ // Select the requested tab, if the name is specified
+ await loadTab(args);
+
+ // Emit init event for tests
+ window.dispatchEvent(new Event("page-info-init"));
+}
+
+async function loadPageInfo(browsingContext, imageElement, browser) {
+ browser = browser || window.opener.gBrowser.selectedBrowser;
+ browsingContext = browsingContext || browser.browsingContext;
+
+ let actor = browsingContext.currentWindowGlobal.getActor("PageInfo");
+
+ let result = await actor.sendQuery("PageInfo:getData");
+ await onNonMediaPageInfoLoad(browser, result, imageElement);
+
+ // Here, we are walking the frame tree via BrowsingContexts to collect all of the
+ // media information for each frame
+ let contextsToVisit = [browsingContext];
+ while (contextsToVisit.length) {
+ let currContext = contextsToVisit.pop();
+ let global = currContext.currentWindowGlobal;
+
+ if (!global) {
+ continue;
+ }
+
+ let subframeActor = global.getActor("PageInfo");
+ let mediaResult = await subframeActor.sendQuery("PageInfo:getMediaData");
+ for (let item of mediaResult.mediaItems) {
+ addImage(item);
+ }
+ selectImage();
+ contextsToVisit.push(...currContext.children);
+ }
+}
+
+/**
+ * onNonMediaPageInfoLoad is responsible for populating the page info
+ * UI other than the media tab. This includes general, permissions, and security.
+ */
+async function onNonMediaPageInfoLoad(browser, pageInfoData, imageInfo) {
+ const { docInfo, windowInfo } = pageInfoData;
+ let uri = Services.io.newURI(docInfo.documentURIObject.spec);
+ let principal = docInfo.principal;
+ gDocInfo = docInfo;
+
+ gImageElement = imageInfo;
+ var titleFormat = windowInfo.isTopWindow
+ ? "page-info-page"
+ : "page-info-frame";
+ document.l10n.setAttributes(document.documentElement, titleFormat, {
+ website: docInfo.location,
+ });
+
+ document
+ .getElementById("main-window")
+ .setAttribute("relatedUrl", docInfo.location);
+
+ await makeGeneralTab(pageInfoData.metaViewRows, docInfo);
+ if (
+ uri.spec.startsWith("about:neterror") ||
+ uri.spec.startsWith("about:certerror") ||
+ uri.spec.startsWith("about:httpsonlyerror")
+ ) {
+ uri = browser.currentURI;
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ browser.contentPrincipal.originAttributes
+ );
+ }
+ onLoadPermission(uri, principal);
+ securityOnLoad(uri, windowInfo);
+}
+
+function resetPageInfo(args) {
+ /* Reset Meta tags part */
+ gMetaView.clear();
+
+ /* Reset Media tab */
+ var mediaTab = document.getElementById("mediaTab");
+ if (!mediaTab.hidden) {
+ mediaTab.hidden = true;
+ }
+ gImageView.clear();
+ gImageHash = {};
+
+ /* Rebuild the data */
+ loadTab(args);
+}
+
+function doHelpButton() {
+ const helpTopics = {
+ generalPanel: "pageinfo_general",
+ mediaPanel: "pageinfo_media",
+ permPanel: "pageinfo_permissions",
+ securityPanel: "pageinfo_security",
+ };
+
+ var deck = document.getElementById("mainDeck");
+ var helpdoc = helpTopics[deck.selectedPanel.id] || "pageinfo_general";
+ openHelpLink(helpdoc);
+}
+
+function showTab(id) {
+ var deck = document.getElementById("mainDeck");
+ var pagel = document.getElementById(id + "Panel");
+ deck.selectedPanel = pagel;
+}
+
+async function loadTab(args) {
+ // If the "View Image Info" context menu item was used, the related image
+ // element is provided as an argument. This can't be a background image.
+ let imageElement = args?.imageElement;
+ let browsingContext = args?.browsingContext;
+ let browser = args?.browser;
+
+ /* Load the page info */
+ await loadPageInfo(browsingContext, imageElement, browser);
+
+ var initialTab = args?.initialTab || "generalTab";
+ var radioGroup = document.getElementById("viewGroup");
+ initialTab =
+ document.getElementById(initialTab) ||
+ document.getElementById("generalTab");
+ radioGroup.selectedItem = initialTab;
+ radioGroup.selectedItem.doCommand();
+ radioGroup.focus();
+}
+
+function openCacheEntry(key, cb) {
+ var checkCacheListener = {
+ onCacheEntryCheck(entry, appCache) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+ onCacheEntryAvailable(entry, isNew, appCache, status) {
+ cb(entry);
+ },
+ };
+ diskStorage.asyncOpenURI(
+ Services.io.newURI(key),
+ "",
+ nsICacheStorage.OPEN_READONLY,
+ checkCacheListener
+ );
+}
+
+async function makeGeneralTab(metaViewRows, docInfo) {
+ // Sets Title in the General Tab, set to "Untitled Page" if no title found
+ if (docInfo.title) {
+ document.getElementById("titletext").value = docInfo.title;
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("titletext"),
+ "no-page-title"
+ );
+ }
+
+ var url = docInfo.location;
+ setItemValue("urltext", url);
+
+ var referrer = "referrer" in docInfo && docInfo.referrer;
+ setItemValue("refertext", referrer);
+
+ var mode =
+ "compatMode" in docInfo && docInfo.compatMode == "BackCompat"
+ ? "general-quirks-mode"
+ : "general-strict-mode";
+ document.l10n.setAttributes(document.getElementById("modetext"), mode);
+
+ // find out the mime type
+ setItemValue("typetext", docInfo.contentType);
+
+ // get the document characterset
+ var encoding = docInfo.characterSet;
+ document.getElementById("encodingtext").value = encoding;
+
+ let length = metaViewRows.length;
+
+ var metaGroup = document.getElementById("metaTags");
+ if (!length) {
+ metaGroup.style.visibility = "hidden";
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("metaTagsCaption"),
+ "general-meta-tags",
+ { tags: length }
+ );
+
+ document.getElementById("metatree").view = gMetaView;
+
+ // Add the metaViewRows onto the general tab's meta info tree.
+ gMetaView.addRows(metaViewRows);
+
+ metaGroup.style.removeProperty("visibility");
+ }
+
+ var modifiedText = formatDate(
+ docInfo.lastModified,
+ await document.l10n.formatValue("not-set-date")
+ );
+ document.getElementById("modifiedtext").value = modifiedText;
+
+ // get cache info
+ var cacheKey = url.replace(/#.*$/, "");
+ openCacheEntry(cacheKey, function(cacheEntry) {
+ if (cacheEntry) {
+ var pageSize = cacheEntry.dataSize;
+ var kbSize = formatNumber(Math.round((pageSize / 1024) * 100) / 100);
+ document.l10n.setAttributes(
+ document.getElementById("sizetext"),
+ "properties-general-size",
+ { kb: kbSize, bytes: formatNumber(pageSize) }
+ );
+ } else {
+ setItemValue("sizetext", null);
+ }
+ });
+}
+
+async function addImage({ url, type, alt, altNotProvided, element, isBg }) {
+ if (!url) {
+ return;
+ }
+
+ if (altNotProvided) {
+ alt = ALT_NOT_SET;
+ }
+
+ if (!gImageHash.hasOwnProperty(url)) {
+ gImageHash[url] = {};
+ }
+ if (!gImageHash[url].hasOwnProperty(type)) {
+ gImageHash[url][type] = {};
+ }
+ if (!gImageHash[url][type].hasOwnProperty(alt)) {
+ gImageHash[url][type][alt] = gImageView.data.length;
+ var row = [url, MEDIA_STRINGS[type], SIZE_UNKNOWN, alt, 1, element, isBg];
+ gImageView.addRow(row);
+
+ // Fill in cache data asynchronously
+ openCacheEntry(url, function(cacheEntry) {
+ // The data at row[2] corresponds to the data size.
+ if (cacheEntry) {
+ let value = cacheEntry.dataSize;
+ // If value is not -1 then replace with actual value, else keep as "unknown"
+ if (value != -1) {
+ let kbSize = Number(Math.round((value / 1024) * 100) / 100);
+ document.l10n
+ .formatValue("media-file-size", { size: kbSize })
+ .then(function(response) {
+ row[2] = response;
+ // Invalidate the row to trigger a repaint.
+ gImageView.tree.invalidateRow(gImageView.data.indexOf(row));
+ });
+ }
+ }
+ });
+
+ if (gImageView.data.length == 1) {
+ document.getElementById("mediaTab").hidden = false;
+ }
+ } else {
+ var i = gImageHash[url][type][alt];
+ gImageView.data[i][COL_IMAGE_COUNT]++;
+ // The same image can occur several times on the page at different sizes.
+ // If the "View Image Info" context menu item was used, ensure we select
+ // the correct element.
+ if (
+ !gImageView.data[i][COL_IMAGE_BG] &&
+ gImageElement &&
+ url == gImageElement.currentSrc &&
+ gImageElement.width == element.width &&
+ gImageElement.height == element.height &&
+ gImageElement.imageText == element.imageText
+ ) {
+ gImageView.data[i][COL_IMAGE_NODE] = element;
+ }
+ }
+}
+
+// Link Stuff
+function onBeginLinkDrag(event, urlField, descField) {
+ if (event.originalTarget.localName != "treechildren") {
+ return;
+ }
+
+ var tree = event.target;
+ if (tree.localName != "tree") {
+ tree = tree.parentNode;
+ }
+
+ var row = tree.getRowAt(event.clientX, event.clientY);
+ if (row == -1) {
+ return;
+ }
+
+ // Adding URL flavor
+ var col = tree.columns[urlField];
+ var url = tree.view.getCellText(row, col);
+ col = tree.columns[descField];
+ var desc = tree.view.getCellText(row, col);
+
+ var dt = event.dataTransfer;
+ dt.setData("text/x-moz-url", url + "\n" + desc);
+ dt.setData("text/url-list", url);
+ dt.setData("text/plain", url);
+}
+
+// Image Stuff
+function getSelectedRows(tree) {
+ var start = {};
+ var end = {};
+ var numRanges = tree.view.selection.getRangeCount();
+
+ var rowArray = [];
+ for (var t = 0; t < numRanges; t++) {
+ tree.view.selection.getRangeAt(t, start, end);
+ for (var v = start.value; v <= end.value; v++) {
+ rowArray.push(v);
+ }
+ }
+
+ return rowArray;
+}
+
+function getSelectedRow(tree) {
+ var rows = getSelectedRows(tree);
+ return rows.length == 1 ? rows[0] : -1;
+}
+
+async function selectSaveFolder(aCallback) {
+ const { nsIFile, nsIFilePicker } = Ci;
+ let titleText = await document.l10n.formatValue("media-select-folder");
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult == nsIFilePicker.returnOK) {
+ aCallback(fp.file.QueryInterface(nsIFile));
+ } else {
+ aCallback(null);
+ }
+ };
+
+ fp.init(window, titleText, nsIFilePicker.modeGetFolder);
+ fp.appendFilters(nsIFilePicker.filterAll);
+ try {
+ let initialDir = Services.prefs.getComplexValue(
+ "browser.download.dir",
+ nsIFile
+ );
+ if (initialDir) {
+ fp.displayDirectory = initialDir;
+ }
+ } catch (ex) {}
+ fp.open(fpCallback);
+}
+
+function saveMedia() {
+ var tree = document.getElementById("imagetree");
+ var rowArray = getSelectedRows(tree);
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+
+ if (rowArray.length == 1) {
+ let row = rowArray[0];
+ let item = gImageView.data[row][COL_IMAGE_NODE];
+ let url = gImageView.data[row][COL_IMAGE_ADDRESS];
+
+ if (url) {
+ var titleKey = "SaveImageTitle";
+
+ if (item instanceof HTMLVideoElement) {
+ titleKey = "SaveVideoTitle";
+ } else if (item instanceof HTMLAudioElement) {
+ titleKey = "SaveAudioTitle";
+ }
+
+ // Bug 1565216 to evaluate passing referrer as item.baseURL
+ let referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI(item.baseURI)
+ );
+ let cookieJarSettings = E10SUtils.deserializeCookieJarSettings(
+ gDocInfo.cookieJarSettings
+ );
+ saveURL(
+ url,
+ null,
+ titleKey,
+ false,
+ false,
+ referrerInfo,
+ cookieJarSettings,
+ null,
+ gDocInfo.isContentWindowPrivate,
+ gDocInfo.principal
+ );
+ }
+ } else {
+ selectSaveFolder(function(aDirectory) {
+ if (aDirectory) {
+ var saveAnImage = function(aURIString, aChosenData, aBaseURI) {
+ uniqueFile(aChosenData.file);
+
+ let referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ aBaseURI
+ );
+ let cookieJarSettings = E10SUtils.deserializeCookieJarSettings(
+ gDocInfo.cookieJarSettings
+ );
+ internalSave(
+ aURIString,
+ null,
+ null,
+ null,
+ null,
+ false,
+ "SaveImageTitle",
+ aChosenData,
+ referrerInfo,
+ cookieJarSettings,
+ null,
+ false,
+ null,
+ gDocInfo.isContentWindowPrivate,
+ gDocInfo.principal
+ );
+ };
+
+ for (var i = 0; i < rowArray.length; i++) {
+ let v = rowArray[i];
+ let dir = aDirectory.clone();
+ let item = gImageView.data[v][COL_IMAGE_NODE];
+ let uriString = gImageView.data[v][COL_IMAGE_ADDRESS];
+ let uri = Services.io.newURI(uriString);
+
+ try {
+ uri.QueryInterface(Ci.nsIURL);
+ dir.append(decodeURIComponent(uri.fileName));
+ } catch (ex) {
+ // data:/blob: uris
+ // Supply a dummy filename, otherwise Download Manager
+ // will try to delete the base directory on failure.
+ dir.append(gImageView.data[v][COL_IMAGE_TYPE]);
+ }
+
+ if (i == 0) {
+ saveAnImage(
+ uriString,
+ new AutoChosen(dir, uri),
+ Services.io.newURI(item.baseURI)
+ );
+ } else {
+ // This delay is a hack which prevents the download manager
+ // from opening many times. See bug 377339.
+ setTimeout(
+ saveAnImage,
+ 200,
+ uriString,
+ new AutoChosen(dir, uri),
+ Services.io.newURI(item.baseURI)
+ );
+ }
+ }
+ }
+ });
+ }
+}
+
+function onImageSelect() {
+ var previewBox = document.getElementById("mediaPreviewBox");
+ var mediaSaveBox = document.getElementById("mediaSaveBox");
+ var splitter = document.getElementById("mediaSplitter");
+ var tree = document.getElementById("imagetree");
+ var count = tree.view.selection.count;
+ if (count == 0) {
+ previewBox.collapsed = true;
+ mediaSaveBox.collapsed = true;
+ splitter.collapsed = true;
+ tree.flex = 1;
+ } else if (count > 1) {
+ splitter.collapsed = true;
+ previewBox.collapsed = true;
+ mediaSaveBox.collapsed = false;
+ tree.flex = 1;
+ } else {
+ mediaSaveBox.collapsed = true;
+ splitter.collapsed = false;
+ previewBox.collapsed = false;
+ tree.flex = 0;
+ makePreview(getSelectedRows(tree)[0]);
+ }
+}
+
+// Makes the media preview (image, video, etc) for the selected row on the media tab.
+function makePreview(row) {
+ var item = gImageView.data[row][COL_IMAGE_NODE];
+ var url = gImageView.data[row][COL_IMAGE_ADDRESS];
+ var isBG = gImageView.data[row][COL_IMAGE_BG];
+ var isAudio = false;
+
+ setItemValue("imageurltext", url);
+ setItemValue("imagetext", item.imageText);
+ setItemValue("imagelongdesctext", item.longDesc);
+
+ // get cache info
+ var cacheKey = url.replace(/#.*$/, "");
+ openCacheEntry(cacheKey, function(cacheEntry) {
+ // find out the file size
+ if (cacheEntry) {
+ let imageSize = cacheEntry.dataSize;
+ var kbSize = Math.round((imageSize / 1024) * 100) / 100;
+ document.l10n.setAttributes(
+ document.getElementById("imagesizetext"),
+ "properties-general-size",
+ { kb: formatNumber(kbSize), bytes: formatNumber(imageSize) }
+ );
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("imagesizetext"),
+ "media-unknown-not-cached"
+ );
+ }
+
+ var mimeType = item.mimeType || this.getContentTypeFromHeaders(cacheEntry);
+ var numFrames = item.numFrames;
+
+ let element = document.getElementById("imagetypetext");
+ var imageType;
+ if (mimeType) {
+ // We found the type, try to display it nicely
+ let imageMimeType = /^image\/(.*)/i.exec(mimeType);
+ if (imageMimeType) {
+ imageType = imageMimeType[1].toUpperCase();
+ if (numFrames > 1) {
+ document.l10n.setAttributes(element, "media-animated-image-type", {
+ type: imageType,
+ frames: numFrames,
+ });
+ } else {
+ document.l10n.setAttributes(element, "media-image-type", {
+ type: imageType,
+ });
+ }
+ } else {
+ // the MIME type doesn't begin with image/, display the raw type
+ element.setAttribute("value", mimeType);
+ element.removeAttribute("data-l10n-id");
+ }
+ } else {
+ // We couldn't find the type, fall back to the value in the treeview
+ element.setAttribute("value", gImageView.data[row][COL_IMAGE_TYPE]);
+ element.removeAttribute("data-l10n-id");
+ }
+
+ var imageContainer = document.getElementById("theimagecontainer");
+ var oldImage = document.getElementById("thepreviewimage");
+
+ var isProtocolAllowed = checkProtocol(gImageView.data[row]);
+
+ var newImage = new Image();
+ newImage.id = "thepreviewimage";
+ var physWidth = 0,
+ physHeight = 0;
+ var width = 0,
+ height = 0;
+
+ let triggeringPrinStr = E10SUtils.serializePrincipal(gDocInfo.principal);
+ if (
+ (item.HTMLLinkElement ||
+ item.HTMLInputElement ||
+ item.HTMLImageElement ||
+ item.SVGImageElement ||
+ (item.HTMLObjectElement && mimeType && mimeType.startsWith("image/")) ||
+ isBG) &&
+ isProtocolAllowed
+ ) {
+ // We need to wait for the image to finish loading before using width & height
+ newImage.addEventListener(
+ "loadend",
+ function() {
+ physWidth = newImage.width || 0;
+ physHeight = newImage.height || 0;
+
+ // "width" and "height" attributes must be set to newImage,
+ // even if there is no "width" or "height attribute in item;
+ // otherwise, the preview image cannot be displayed correctly.
+ // Since the image might have been loaded out-of-process, we expect
+ // the item to tell us its width / height dimensions. Failing that
+ // the item should tell us the natural dimensions of the image. Finally
+ // failing that, we'll assume that the image was never loaded in the
+ // other process (this can be true for favicons, for example), and so
+ // we'll assume that we can use the natural dimensions of the newImage
+ // we just created. If the natural dimensions of newImage are not known
+ // then the image is probably broken.
+ if (!isBG) {
+ newImage.width =
+ ("width" in item && item.width) || newImage.naturalWidth;
+ newImage.height =
+ ("height" in item && item.height) || newImage.naturalHeight;
+ } else {
+ // the Width and Height of an HTML tag should not be used for its background image
+ // (for example, "table" can have "width" or "height" attributes)
+ newImage.width = item.naturalWidth || newImage.naturalWidth;
+ newImage.height = item.naturalHeight || newImage.naturalHeight;
+ }
+
+ if (item.SVGImageElement) {
+ newImage.width = item.SVGImageElementWidth;
+ newImage.height = item.SVGImageElementHeight;
+ }
+
+ width = newImage.width;
+ height = newImage.height;
+
+ document.getElementById("theimagecontainer").collapsed = false;
+ document.getElementById("brokenimagecontainer").collapsed = true;
+
+ if (url) {
+ if (width != physWidth || height != physHeight) {
+ document.l10n.setAttributes(
+ document.getElementById("imagedimensiontext"),
+ "media-dimensions-scaled",
+ {
+ dimx: formatNumber(physWidth),
+ dimy: formatNumber(physHeight),
+ scaledx: formatNumber(width),
+ scaledy: formatNumber(height),
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("imagedimensiontext"),
+ "media-dimensions",
+ { dimx: formatNumber(width), dimy: formatNumber(height) }
+ );
+ }
+ }
+ },
+ { once: true }
+ );
+
+ newImage.setAttribute("triggeringprincipal", triggeringPrinStr);
+ newImage.setAttribute("src", url);
+ } else {
+ // Handle the case where newImage is not used for width & height
+ if (item.HTMLVideoElement && isProtocolAllowed) {
+ newImage = document.createElement("video");
+ newImage.id = "thepreviewimage";
+ newImage.setAttribute("triggeringprincipal", triggeringPrinStr);
+ newImage.src = url;
+ newImage.controls = true;
+ width = physWidth = item.videoWidth;
+ height = physHeight = item.videoHeight;
+
+ document.getElementById("theimagecontainer").collapsed = false;
+ document.getElementById("brokenimagecontainer").collapsed = true;
+ } else if (item.HTMLAudioElement && isProtocolAllowed) {
+ newImage = new Audio();
+ newImage.id = "thepreviewimage";
+ newImage.setAttribute("triggeringprincipal", triggeringPrinStr);
+ newImage.src = url;
+ newImage.controls = true;
+ isAudio = true;
+
+ document.getElementById("theimagecontainer").collapsed = false;
+ document.getElementById("brokenimagecontainer").collapsed = true;
+ } else {
+ // fallback image for protocols not allowed (e.g., javascript:)
+ // or elements not [yet] handled (e.g., object, embed).
+ document.getElementById("brokenimagecontainer").collapsed = false;
+ document.getElementById("theimagecontainer").collapsed = true;
+ }
+
+ if (url && !isAudio) {
+ document.l10n.setAttributes(
+ document.getElementById("imagedimensiontext"),
+ "media-dimensions",
+ { dimx: formatNumber(width), dimy: formatNumber(height) }
+ );
+ }
+ }
+
+ imageContainer.removeChild(oldImage);
+ imageContainer.appendChild(newImage);
+ });
+}
+
+function getContentTypeFromHeaders(cacheEntryDescriptor) {
+ if (!cacheEntryDescriptor) {
+ return null;
+ }
+
+ let headers = cacheEntryDescriptor.getMetaDataElement("response-head");
+ let type = /^Content-Type:\s*(.*?)\s*(?:\;|$)/im.exec(headers);
+ return type && type[1];
+}
+
+function setItemValue(id, value) {
+ var item = document.getElementById(id);
+ item.closest("tr").hidden = !value;
+ if (value) {
+ item.value = value;
+ }
+}
+
+function formatNumber(number) {
+ return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString()
+}
+
+function formatDate(datestr, unknown) {
+ var date = new Date(datestr);
+ if (!date.valueOf()) {
+ return unknown;
+ }
+
+ const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "long",
+ timeStyle: "long",
+ });
+ return dateTimeFormatter.format(date);
+}
+
+function doCopy() {
+ if (!gClipboardHelper) {
+ return;
+ }
+
+ var elem = document.commandDispatcher.focusedElement;
+
+ if (elem && elem.localName == "tree") {
+ var view = elem.view;
+ var selection = view.selection;
+ var text = [],
+ tmp = "";
+ var min = {},
+ max = {};
+
+ var count = selection.getRangeCount();
+
+ for (var i = 0; i < count; i++) {
+ selection.getRangeAt(i, min, max);
+
+ for (var row = min.value; row <= max.value; row++) {
+ tmp = view.getCellValue(row, null);
+ if (tmp) {
+ text.push(tmp);
+ }
+ }
+ }
+ gClipboardHelper.copyString(text.join("\n"));
+ }
+}
+
+function doSelectAllMedia() {
+ var tree = document.getElementById("imagetree");
+
+ if (tree) {
+ tree.view.selection.selectAll();
+ }
+}
+
+function doSelectAll() {
+ var elem = document.commandDispatcher.focusedElement;
+
+ if (elem && elem.localName == "tree") {
+ elem.view.selection.selectAll();
+ }
+}
+
+function selectImage() {
+ if (!gImageElement) {
+ return;
+ }
+
+ var tree = document.getElementById("imagetree");
+ for (var i = 0; i < tree.view.rowCount; i++) {
+ // If the image row element is the image selected from the "View Image Info" context menu item.
+ let image = gImageView.data[i][COL_IMAGE_NODE];
+ if (
+ !gImageView.data[i][COL_IMAGE_BG] &&
+ gImageElement.currentSrc == gImageView.data[i][COL_IMAGE_ADDRESS] &&
+ gImageElement.width == image.width &&
+ gImageElement.height == image.height &&
+ gImageElement.imageText == image.imageText
+ ) {
+ tree.view.selection.select(i);
+ tree.ensureRowIsVisible(i);
+ tree.focus();
+ return;
+ }
+ }
+}
+
+function checkProtocol(img) {
+ var url = img[COL_IMAGE_ADDRESS];
+ return (
+ /^data:image\//i.test(url) ||
+ /^(https?|ftp|file|about|chrome|resource):/.test(url)
+ );
+}
diff --git a/browser/base/content/pageinfo/pageInfo.xhtml b/browser/base/content/pageinfo/pageInfo.xhtml
new file mode 100644
index 0000000000..f40ffd3778
--- /dev/null
+++ b/browser/base/content/pageinfo/pageInfo.xhtml
@@ -0,0 +1,419 @@
+<?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://browser/content/pageinfo/pageInfo.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/pageInfo.css" type="text/css"?>
+
+<!DOCTYPE window [
+#ifdef XP_MACOSX
+#include ../browser-doctype.inc
+#endif
+]>
+
+<window id="main-window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ data-l10n-id="page-info-window"
+ data-l10n-attrs="style"
+ windowtype="Browser:page-info"
+ onload="onLoadPageInfo()"
+ align="stretch"
+ screenX="10" screenY="10"
+ persist="screenX screenY width height sizemode">
+
+ <linkset>
+ <html:link rel="localization" href="browser/pageInfo.ftl"/>
+ </linkset>
+ #ifdef XP_MACOSX
+ #include ../macWindow.inc.xhtml
+ #else
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+ <script src="chrome://browser/content/utilityOverlay.js"/>
+ #endif
+ <script src="chrome://global/content/contentAreaUtils.js"/>
+ <script src="chrome://global/content/treeUtils.js"/>
+ <script src="chrome://browser/content/pageinfo/pageInfo.js"/>
+ <script src="chrome://browser/content/pageinfo/permissions.js"/>
+ <script src="chrome://browser/content/pageinfo/security.js"/>
+
+ <stringbundleset id="pageinfobundleset">
+ <stringbundle id="pkiBundle" src="chrome://pippki/locale/pippki.properties"/>
+ <stringbundle id="browserBundle" src="chrome://browser/locale/browser.properties"/>
+ </stringbundleset>
+
+ <commandset id="pageInfoCommandSet">
+ <command id="cmd_close" oncommand="window.close();"/>
+ <command id="cmd_help" oncommand="doHelpButton();"/>
+ <command id="cmd_copy_tree" oncommand="doCopy();"/>
+ <command id="cmd_selectall_tree" oncommand="doSelectAll();"/>
+ </commandset>
+
+ <keyset id="pageInfoKeySet">
+ <key data-l10n-id="close-dialog" data-l10n-attrs="key" modifiers="accel" command="cmd_close"/>
+ <key keycode="VK_ESCAPE" command="cmd_close"/>
+#ifdef XP_MACOSX
+ <key key="." modifiers="meta" command="cmd_close"/>
+#else
+ <key keycode="VK_F1" command="cmd_help"/>
+#endif
+ <key data-l10n-id="copy" data-l10n-attrs="key" modifiers="accel" command="cmd_copy_tree"/>
+ <key data-l10n-id="select-all" data-l10n-attrs="key" modifiers="accel" command="cmd_selectall_tree"/>
+ <key data-l10n-id="select-all" data-l10n-attrs="key" modifiers="alt" command="cmd_selectall_tree"/>
+ </keyset>
+
+ <menupopup id="picontext">
+ <menuitem id="menu_selectall" data-l10n-id="menu-select-all" command="cmd_selectall_tree"/>
+ <menuitem id="menu_copy" data-l10n-id="menu-copy" command="cmd_copy_tree"/>
+ </menupopup>
+
+ <vbox id="topBar">
+ <radiogroup id="viewGroup" class="chromeclass-toolbar" orient="horizontal">
+ <radio id="generalTab" data-l10n-id="general-tab"
+ oncommand="showTab('general');"/>
+ <radio id="mediaTab" data-l10n-id="media-tab"
+ oncommand="showTab('media');" hidden="true"/>
+ <radio id="permTab" data-l10n-id="perm-tab"
+ oncommand="showTab('perm');"/>
+ <radio id="securityTab" data-l10n-id="security-tab"
+ oncommand="showTab('security');"/>
+ </radiogroup>
+ </vbox>
+
+ <deck id="mainDeck" flex="1">
+ <!-- General page information -->
+ <vbox id="generalPanel">
+ <table id="generalTable" xmlns="http://www.w3.org/1999/xhtml">
+ <tr id="generalTitle">
+ <th>
+ <xul:label control="titletext" data-l10n-id="general-title"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="titletext" data-l10n-attrs="value"/>
+ </td>
+ </tr>
+ <tr id="generalURLRow">
+ <th>
+ <xul:label control="urltext" data-l10n-id="general-url"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="urltext"/>
+ </td>
+ </tr>
+ <tr class="tableSeparator"/>
+ <tr id="generalTypeRow">
+ <th>
+ <xul:label control="typetext" data-l10n-id="general-type"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="typetext"/>
+ </td>
+ </tr>
+ <tr id="generalModeRow">
+ <th>
+ <xul:label control="modetext" data-l10n-id="general-mode"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="modetext" data-l10n-attrs="value"/>
+ </td>
+ </tr>
+ <tr id="generalEncodingRow">
+ <th>
+ <xul:label control="encodingtext" data-l10n-id="general-encoding"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="encodingtext"/>
+ </td>
+ </tr>
+ <tr id="generalSizeRow">
+ <th>
+ <xul:label control="sizetext" data-l10n-id="general-size"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="sizetext" data-l10n-attrs="value"/>
+ </td>
+ </tr>
+ <tr id="generalReferrerRow">
+ <th>
+ <xul:label control="refertext" data-l10n-id="general-referrer"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="refertext"/>
+ </td>
+ </tr>
+ <tr class="tableSeparator"/>
+ <tr id="generalModifiedRow">
+ <th>
+ <xul:label control="modifiedtext" data-l10n-id="general-modified"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="modifiedtext"/>
+ </td>
+ </tr>
+ </table>
+ <separator class="thin"/>
+ <vbox id="metaTags" flex="1">
+ <label control="metatree" id="metaTagsCaption" class="header"/>
+ <tree id="metatree" flex="1" hidecolumnpicker="true" contextmenu="picontext">
+ <treecols>
+ <treecol id="meta-name" data-l10n-id="general-meta-name"
+ persist="width" flex="1"
+ onclick="gMetaView.onPageMediaSort('meta-name');"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="meta-content" data-l10n-id="general-meta-content"
+ persist="width" flex="4"
+ onclick="gMetaView.onPageMediaSort('meta-content');"/>
+ </treecols>
+ <treechildren id="metatreechildren" flex="1"/>
+ </tree>
+ </vbox>
+ <hbox pack="end">
+ <button command="cmd_help" data-l10n-id="help-button" dlgtype="help"/>
+ </hbox>
+ </vbox>
+
+ <!-- Media information -->
+ <vbox id="mediaPanel">
+ <tree id="imagetree" onselect="onImageSelect();" contextmenu="picontext"
+ ondragstart="onBeginLinkDrag(event, 'image-address', 'image-alt')">
+ <treecols>
+ <treecol primary="true" persist="width" flex="10"
+ width="10" id="image-address" data-l10n-id="media-address"
+ onclick="gImageView.onPageMediaSort('image-address');"/>
+ <splitter class="tree-splitter"/>
+ <treecol persist="hidden width" flex="2"
+ width="2" id="image-type" data-l10n-id="media-type"
+ onclick="gImageView.onPageMediaSort('image-type');"/>
+ <splitter class="tree-splitter"/>
+ <treecol hidden="true" persist="hidden width" flex="2"
+ width="2" id="image-size" data-l10n-id="media-size" value="size"
+ onclick="gImageView.onPageMediaSort('image-size');"/>
+ <splitter class="tree-splitter"/>
+ <treecol hidden="true" persist="hidden width" flex="4"
+ width="4" id="image-alt" data-l10n-id="media-alt-header"
+ onclick="gImageView.onPageMediaSort('image-alt');"/>
+ <splitter class="tree-splitter"/>
+ <treecol hidden="true" persist="hidden width" flex="1"
+ width="1" id="image-count" data-l10n-id="media-count"
+ onclick="gImageView.onPageMediaSort('image-count');"/>
+ </treecols>
+ <treechildren id="imagetreechildren" flex="1"/>
+ </tree>
+ <splitter orient="vertical" id="mediaSplitter"/>
+ <vbox flex="1" id="mediaPreviewBox" collapsed="true">
+ <table id="mediaTable" xmlns="http://www.w3.org/1999/xhtml">
+ <tr id="mediaLocationRow">
+ <th>
+ <xul:label control="imageurltext" data-l10n-id="media-location"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="imageurltext"/>
+ </td>
+ </tr>
+ <tr id="mediaTypeRow">
+ <th>
+ <xul:label control="imagetypetext" data-l10n-id="general-type"/>
+ </th>
+ <td>
+ <input id="imagetypetext" data-l10n-attrs="value"/>
+ </td>
+ </tr>
+ <tr id="mediaSizeRow">
+ <th>
+ <xul:label control="imagesizetext" data-l10n-id="general-size"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="imagesizetext" data-l10n-attrs="value"/>
+ </td>
+ </tr>
+ <tr id="mediaDimensionRow">
+ <th>
+ <xul:label control="imagedimensiontext" data-l10n-id="media-dimension"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="imagedimensiontext" data-l10n-attrs="value"/>
+ </td>
+ </tr>
+ <tr id="mediaTextRow">
+ <th>
+ <xul:label control="imagetext" data-l10n-id="media-text"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="imagetext"/>
+ </td>
+ </tr>
+ <tr id="mediaLongdescRow">
+ <th>
+ <xul:label control="imagelongdesctext" data-l10n-id="media-long-desc"/>
+ </th>
+ <td>
+ <input readonly="readonly" id="imagelongdesctext"/>
+ </td>
+ </tr>
+ </table>
+ <hbox id="imageSaveBox" align="end">
+ <spacer id="imageSaveBoxSpacer" flex="1"/>
+ <button data-l10n-id="menu-select-all"
+ id="selectallbutton"
+ oncommand="doSelectAllMedia();"/>
+ <button data-l10n-id="media-save-as"
+ id="imagesaveasbutton"
+ oncommand="saveMedia();"/>
+ </hbox>
+ <vbox id="imagecontainerbox" flex="1" pack="center">
+ <hbox id="theimagecontainer" pack="center">
+ <image id="thepreviewimage"/>
+ </hbox>
+ <hbox id="brokenimagecontainer" pack="center" collapsed="true">
+ <image id="brokenimage" src="resource://gre-resources/broken-image.png"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ <hbox id="mediaSaveBox" collapsed="true">
+ <spacer id="mediaSaveBoxSpacer" flex="1"/>
+ <button data-l10n-id="media-save-image-as"
+ id="mediasaveasbutton"
+ oncommand="saveMedia();"/>
+ </hbox>
+ <hbox pack="end">
+ <button command="cmd_help" data-l10n-id="help-button" dlgtype="help"/>
+ </hbox>
+ </vbox>
+
+ <!-- Permissions -->
+ <vbox id="permPanel">
+ <hbox id="permHostBox">
+ <label data-l10n-id="permissions-for" control="hostText" />
+ <html:input id="hostText" class="header" readonly="readonly"/>
+ </hbox>
+
+ <vbox id="permList" flex="1"/>
+ <hbox pack="end">
+ <button command="cmd_help" data-l10n-id="help-button" dlgtype="help"/>
+ </hbox>
+ </vbox>
+
+ <!-- Security & Privacy -->
+ <vbox id="securityPanel">
+ <!-- Identity Section -->
+ <groupbox>
+ <label class="header" data-l10n-id="security-view-identity"/>
+ <table xmlns="http://www.w3.org/1999/xhtml">
+ <!-- Domain -->
+ <tr>
+ <th>
+ <xul:label data-l10n-id="security-view-identity-domain"
+ control="security-identity-domain-value"/>
+ </th>
+ <td>
+ <input id="security-identity-domain-value" readonly="readonly"/>
+ </td>
+ </tr>
+ <!-- Owner -->
+ <tr>
+ <th>
+ <xul:label id="security-identity-owner-label"
+ class="fieldLabel"
+ data-l10n-id="security-view-identity-owner"
+ control="security-identity-owner-value"/>
+ </th>
+ <td>
+ <input id="security-identity-owner-value" readonly="readonly" data-l10n-attrs="value"/>
+ </td>
+ </tr>
+ <!-- Verifier -->
+ <tr>
+ <th>
+ <xul:label data-l10n-id="security-view-identity-verifier"
+ control="security-identity-verifier-value"/>
+ </th>
+ <td>
+ <div class="table-split-column">
+ <input id="security-identity-verifier-value" readonly="readonly"
+ data-l10n-attrs="value"/>
+ <xul:button id="security-view-cert" data-l10n-id="security-view"
+ collapsed="true"
+ oncommand="security.viewCert();"/>
+ </div>
+ </td>
+ </tr>
+ <!-- Certificate Validity -->
+ <tr id="security-identity-validity-row">
+ <th>
+ <xul:label data-l10n-id="security-view-identity-validity"
+ control="security-identity-validity-value"/>
+ </th>
+ <td>
+ <input id="security-identity-validity-value" readonly="readonly"/>
+ </td>
+ </tr>
+ </table>
+ </groupbox>
+
+ <!-- Privacy & History section -->
+ <groupbox>
+ <label class="header" data-l10n-id="security-view-privacy"/>
+ <table id="securityTable" xmlns="http://www.w3.org/1999/xhtml">
+ <!-- History -->
+ <tr>
+ <th>
+ <xul:label control="security-privacy-history-value" data-l10n-id="security-view-privacy-history-value"/>
+ </th>
+ <td>
+ <xul:label id="security-privacy-history-value"
+ data-l10n-id="security-view-unknown"/>
+ </td>
+ </tr>
+ <!-- Site Data & Cookies -->
+ <tr id="security-privacy-sitedata-row">
+ <th>
+ <xul:label control="security-privacy-sitedata-value" data-l10n-id="security-view-privacy-sitedata-value"/>
+ </th>
+ <td>
+ <div class="table-split-column">
+ <xul:label id="security-privacy-sitedata-value" data-l10n-id="security-view-unknown"/>
+ <xul:button id="security-clear-sitedata"
+ disabled="true"
+ data-l10n-id="security-view-privacy-clearsitedata"
+ oncommand="security.clearSiteData();"/>
+ </div>
+ </td>
+ </tr>
+ <!-- Passwords -->
+ <tr>
+ <th>
+ <xul:label control="security-privacy-passwords-value" data-l10n-id="security-view-privacy-passwords-value"/>
+ </th>
+ <td>
+ <div class="table-split-column">
+ <xul:label id="security-privacy-passwords-value"
+ data-l10n-id="security-view-unknown"/>
+ <xul:button id="security-view-password"
+ data-l10n-id="security-view-privacy-viewpasswords"
+ oncommand="security.viewPasswords();"/>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </groupbox>
+
+ <!-- Technical Details section -->
+ <groupbox>
+ <label class="header" data-l10n-id="security-view-technical"/>
+ <label id="security-technical-shortform"/>
+ <description id="security-technical-longform1"/>
+ <description id="security-technical-longform2"/>
+ <description id="security-technical-certificate-transparency"/>
+ </groupbox>
+
+ <hbox pack="end">
+ <button command="cmd_help" data-l10n-id="help-button" dlgtype="help"/>
+ </hbox>
+ </vbox>
+ <!-- Others added by overlay -->
+ </deck>
+
+</window>
diff --git a/browser/base/content/pageinfo/permissions.js b/browser/base/content/pageinfo/permissions.js
new file mode 100644
index 0000000000..257a01e9e5
--- /dev/null
+++ b/browser/base/content/pageinfo/permissions.js
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 pageInfo.js */
+
+const { SitePermissions } = ChromeUtils.import(
+ "resource:///modules/SitePermissions.jsm"
+);
+
+var gPermPrincipal;
+
+// List of ids of permissions to hide.
+const EXCLUDE_PERMS = ["open-protocol-handler"];
+
+// Array of permissionIDs sorted alphabetically by label.
+let gPermissions = SitePermissions.listPermissions()
+ .filter(permissionID => {
+ if (!SitePermissions.getPermissionLabel(permissionID)) {
+ return false;
+ }
+ return !EXCLUDE_PERMS.includes(permissionID);
+ })
+ .sort((a, b) => {
+ let firstLabel = SitePermissions.getPermissionLabel(a);
+ let secondLabel = SitePermissions.getPermissionLabel(b);
+ return firstLabel.localeCompare(secondLabel);
+ });
+
+var permissionObserver = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "perm-changed") {
+ var permission = aSubject.QueryInterface(Ci.nsIPermission);
+ if (
+ permission.matches(gPermPrincipal, true) &&
+ gPermissions.includes(permission.type)
+ ) {
+ initRow(permission.type);
+ }
+ }
+ },
+};
+
+function getExcludedPermissions() {
+ return EXCLUDE_PERMS;
+}
+
+function onLoadPermission(uri, principal) {
+ var permTab = document.getElementById("permTab");
+ if (SitePermissions.isSupportedPrincipal(principal)) {
+ gPermPrincipal = principal;
+ var hostText = document.getElementById("hostText");
+ hostText.value = uri.displayPrePath;
+
+ for (var i of gPermissions) {
+ initRow(i);
+ }
+ Services.obs.addObserver(permissionObserver, "perm-changed");
+ window.addEventListener("unload", onUnloadPermission);
+ permTab.hidden = false;
+ } else {
+ permTab.hidden = true;
+ }
+}
+
+function onUnloadPermission() {
+ Services.obs.removeObserver(permissionObserver, "perm-changed");
+}
+
+function initRow(aPartId) {
+ createRow(aPartId);
+
+ var checkbox = document.getElementById(aPartId + "Def");
+ var command = document.getElementById("cmd_" + aPartId + "Toggle");
+ var { state, scope } = SitePermissions.getForPrincipal(
+ gPermPrincipal,
+ aPartId
+ );
+ let defaultState = SitePermissions.getDefault(aPartId);
+
+ // Since cookies preferences have many different possible configuration states
+ // we don't consider any permission except "no permission" to be default.
+ if (aPartId == "cookie") {
+ state = Services.perms.testPermissionFromPrincipal(
+ gPermPrincipal,
+ "cookie"
+ );
+
+ if (state == SitePermissions.UNKNOWN) {
+ checkbox.checked = true;
+ command.setAttribute("disabled", "true");
+ // Don't select any item in the radio group, as we can't
+ // confidently say that all cookies on the site will be allowed.
+ let radioGroup = document.getElementById("cookieRadioGroup");
+ radioGroup.selectedItem = null;
+ } else {
+ checkbox.checked = false;
+ command.removeAttribute("disabled");
+ }
+
+ setRadioState(aPartId, state);
+
+ checkbox.disabled = Services.prefs.prefIsLocked(
+ "network.cookie.cookieBehavior"
+ );
+
+ return;
+ }
+
+ if (state != defaultState) {
+ checkbox.checked = false;
+ command.removeAttribute("disabled");
+ } else {
+ checkbox.checked = true;
+ command.setAttribute("disabled", "true");
+ }
+
+ if (
+ [SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL].includes(scope)
+ ) {
+ checkbox.setAttribute("disabled", "true");
+ command.setAttribute("disabled", "true");
+ }
+
+ setRadioState(aPartId, state);
+
+ switch (aPartId) {
+ case "install":
+ checkbox.disabled = !Services.policies.isAllowed("xpinstall");
+ break;
+ case "popup":
+ checkbox.disabled = Services.prefs.prefIsLocked(
+ "dom.disable_open_during_load"
+ );
+ break;
+ case "autoplay-media":
+ checkbox.disabled = Services.prefs.prefIsLocked("media.autoplay.default");
+ break;
+ case "geo":
+ case "desktop-notification":
+ case "camera":
+ case "microphone":
+ case "xr":
+ checkbox.disabled = Services.prefs.prefIsLocked(
+ "permissions.default." + aPartId
+ );
+ break;
+ }
+}
+
+function createRow(aPartId) {
+ let rowId = "perm-" + aPartId + "-row";
+ if (document.getElementById(rowId)) {
+ return;
+ }
+
+ let commandId = "cmd_" + aPartId + "Toggle";
+ let labelId = "perm-" + aPartId + "-label";
+ let radiogroupId = aPartId + "RadioGroup";
+
+ let command = document.createXULElement("command");
+ command.setAttribute("id", commandId);
+ command.setAttribute("oncommand", "onRadioClick('" + aPartId + "');");
+ document.getElementById("pageInfoCommandSet").appendChild(command);
+
+ let row = document.createXULElement("vbox");
+ row.setAttribute("id", rowId);
+ row.setAttribute("class", "permission");
+
+ let label = document.createXULElement("label");
+ label.setAttribute("id", labelId);
+ label.setAttribute("control", radiogroupId);
+ label.setAttribute("value", SitePermissions.getPermissionLabel(aPartId));
+ label.setAttribute("class", "permissionLabel");
+ row.appendChild(label);
+
+ let controls = document.createXULElement("hbox");
+ controls.setAttribute("role", "group");
+ controls.setAttribute("aria-labelledby", labelId);
+
+ let checkbox = document.createXULElement("checkbox");
+ checkbox.setAttribute("id", aPartId + "Def");
+ checkbox.setAttribute("oncommand", "onCheckboxClick('" + aPartId + "');");
+ document.l10n.setAttributes(checkbox, "permissions-use-default");
+ controls.appendChild(checkbox);
+
+ let spacer = document.createXULElement("spacer");
+ spacer.setAttribute("flex", "1");
+ controls.appendChild(spacer);
+
+ let radiogroup = document.createXULElement("radiogroup");
+ radiogroup.setAttribute("id", radiogroupId);
+ radiogroup.setAttribute("orient", "horizontal");
+ for (let state of SitePermissions.getAvailableStates(aPartId)) {
+ let radio = document.createXULElement("radio");
+ radio.setAttribute("id", aPartId + "#" + state);
+ radio.setAttribute(
+ "label",
+ SitePermissions.getMultichoiceStateLabel(aPartId, state)
+ );
+ radio.setAttribute("command", commandId);
+ radiogroup.appendChild(radio);
+ }
+ controls.appendChild(radiogroup);
+
+ row.appendChild(controls);
+
+ document.getElementById("permList").appendChild(row);
+}
+
+function onCheckboxClick(aPartId) {
+ var command = document.getElementById("cmd_" + aPartId + "Toggle");
+ var checkbox = document.getElementById(aPartId + "Def");
+ if (checkbox.checked) {
+ SitePermissions.removeFromPrincipal(gPermPrincipal, aPartId);
+ command.setAttribute("disabled", "true");
+ } else {
+ onRadioClick(aPartId);
+ command.removeAttribute("disabled");
+ }
+}
+
+function onRadioClick(aPartId) {
+ var radioGroup = document.getElementById(aPartId + "RadioGroup");
+ let permission;
+ if (radioGroup.selectedItem) {
+ permission = parseInt(radioGroup.selectedItem.id.split("#")[1]);
+ } else {
+ permission = SitePermissions.getDefault(aPartId);
+ }
+ SitePermissions.setForPrincipal(gPermPrincipal, aPartId, permission);
+}
+
+function setRadioState(aPartId, aValue) {
+ var radio = document.getElementById(aPartId + "#" + aValue);
+ if (radio) {
+ radio.radioGroup.selectedItem = radio;
+ }
+}
diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js
new file mode 100644
index 0000000000..6a2d09ec84
--- /dev/null
+++ b/browser/base/content/pageinfo/security.js
@@ -0,0 +1,434 @@
+/* -*- 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/. */
+
+const { SiteDataManager } = ChromeUtils.import(
+ "resource:///modules/SiteDataManager.jsm"
+);
+const { DownloadUtils } = ChromeUtils.import(
+ "resource://gre/modules/DownloadUtils.jsm"
+);
+
+/* import-globals-from pageInfo.js */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PluralForm",
+ "resource://gre/modules/PluralForm.jsm"
+);
+
+var security = {
+ async init(uri, windowInfo) {
+ this.uri = uri;
+ this.windowInfo = windowInfo;
+ this.securityInfo = await this._getSecurityInfo();
+ },
+
+ viewCert() {
+ let certChain = this.securityInfo.certChain;
+ let certs = certChain.map(elem =>
+ encodeURIComponent(elem.getBase64DERString())
+ );
+ let certsStringURL = certs.map(elem => `cert=${elem}`);
+ certsStringURL = certsStringURL.join("&");
+ let url = `about:certificate?${certsStringURL}`;
+ let win = BrowserWindowTracker.getTopWindow();
+ win.switchToTabHavingURI(url, true, {});
+ },
+
+ async _getSecurityInfo() {
+ // We don't have separate info for a frame, return null until further notice
+ // (see bug 138479)
+ if (!this.windowInfo.isTopWindow) {
+ return null;
+ }
+
+ var ui = security._getSecurityUI();
+ if (!ui) {
+ return null;
+ }
+
+ var isBroken = ui.state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
+ var isMixed =
+ ui.state &
+ (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT |
+ Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ var isEV = ui.state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
+
+ let retval = {
+ cAName: "",
+ encryptionAlgorithm: "",
+ encryptionStrength: 0,
+ version: "",
+ isBroken,
+ isMixed,
+ isEV,
+ cert: null,
+ certificateTransparency: null,
+ };
+
+ // Only show certificate info for secure contexts. This prevents us from
+ // showing certificate data for http origins when using a proxy.
+ // https://searchfox.org/mozilla-central/rev/9c72508fcf2bba709a5b5b9eae9da35e0c707baa/security/manager/ssl/nsSecureBrowserUI.cpp#62-64
+ if (!ui.isSecureContext) {
+ return retval;
+ }
+
+ let secInfo = await window.opener.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getSecurityInfo();
+ if (!secInfo) {
+ return retval;
+ }
+
+ secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ let cert = secInfo.serverCert;
+ let issuerName = null;
+ if (cert) {
+ issuerName = cert.issuerOrganization || cert.issuerName;
+ }
+
+ let certChainArray = [];
+ if (secInfo.succeededCertChain.length) {
+ certChainArray = secInfo.succeededCertChain;
+ } else {
+ certChainArray = secInfo.failedCertChain;
+ }
+
+ retval = {
+ cAName: issuerName,
+ encryptionAlgorithm: undefined,
+ encryptionStrength: undefined,
+ version: undefined,
+ isBroken,
+ isMixed,
+ isEV,
+ cert,
+ certChain: certChainArray,
+ certificateTransparency: undefined,
+ };
+
+ var version;
+ try {
+ retval.encryptionAlgorithm = secInfo.cipherName;
+ retval.encryptionStrength = secInfo.secretKeyLength;
+ version = secInfo.protocolVersion;
+ } catch (e) {}
+
+ switch (version) {
+ case Ci.nsITransportSecurityInfo.SSL_VERSION_3:
+ retval.version = "SSL 3";
+ break;
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1:
+ retval.version = "TLS 1.0";
+ break;
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1:
+ retval.version = "TLS 1.1";
+ break;
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2:
+ retval.version = "TLS 1.2";
+ break;
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3:
+ retval.version = "TLS 1.3";
+ break;
+ }
+
+ // Select the status text to display for Certificate Transparency.
+ // Since we do not yet enforce the CT Policy on secure connections,
+ // we must not complain on policy discompliance (it might be viewed
+ // as a security issue by the user).
+ switch (secInfo.certificateTransparencyStatus) {
+ case Ci.nsITransportSecurityInfo.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE:
+ case Ci.nsITransportSecurityInfo
+ .CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS:
+ case Ci.nsITransportSecurityInfo
+ .CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS:
+ retval.certificateTransparency = null;
+ break;
+ case Ci.nsITransportSecurityInfo
+ .CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT:
+ retval.certificateTransparency = "Compliant";
+ break;
+ }
+
+ return retval;
+ },
+
+ // Find the secureBrowserUI object (if present)
+ _getSecurityUI() {
+ if (window.opener.gBrowser) {
+ return window.opener.gBrowser.securityUI;
+ }
+ return null;
+ },
+
+ async _updateSiteDataInfo() {
+ // Save site data info for deleting.
+ this.siteData = await SiteDataManager.getSites(
+ SiteDataManager.getBaseDomainFromHost(this.uri.host)
+ );
+
+ let clearSiteDataButton = document.getElementById(
+ "security-clear-sitedata"
+ );
+ let siteDataLabel = document.getElementById(
+ "security-privacy-sitedata-value"
+ );
+
+ if (!this.siteData.length) {
+ document.l10n.setAttributes(siteDataLabel, "security-site-data-no");
+ clearSiteDataButton.setAttribute("disabled", "true");
+ return;
+ }
+
+ let usage = this.siteData.reduce((acc, site) => acc + site.usage, 0);
+ if (usage > 0) {
+ let size = DownloadUtils.convertByteUnits(usage);
+ let hasCookies = this.siteData.some(site => !!site.cookies.length);
+ if (hasCookies) {
+ document.l10n.setAttributes(
+ siteDataLabel,
+ "security-site-data-cookies",
+ { value: size[0], unit: size[1] }
+ );
+ } else {
+ document.l10n.setAttributes(siteDataLabel, "security-site-data-only", {
+ value: size[0],
+ unit: size[1],
+ });
+ }
+ } else {
+ // We're storing cookies, else the list would have been empty.
+ document.l10n.setAttributes(
+ siteDataLabel,
+ "security-site-data-cookies-only"
+ );
+ }
+
+ clearSiteDataButton.removeAttribute("disabled");
+ },
+
+ /**
+ * Clear Site Data and Cookies
+ */
+ clearSiteData() {
+ if (this.siteData && this.siteData.length) {
+ let hosts = this.siteData.map(site => site.host);
+ if (SiteDataManager.promptSiteDataRemoval(window, hosts)) {
+ SiteDataManager.remove(hosts).then(() => this._updateSiteDataInfo());
+ }
+ }
+ },
+
+ /**
+ * Open the login manager window
+ */
+ viewPasswords() {
+ LoginHelper.openPasswordManager(window, {
+ filterString: this.windowInfo.hostName,
+ entryPoint: "pageinfo",
+ });
+ },
+};
+
+async function securityOnLoad(uri, windowInfo) {
+ await security.init(uri, windowInfo);
+
+ let info = security.securityInfo;
+ if (
+ !info ||
+ (uri.scheme === "about" && !uri.spec.startsWith("about:certerror"))
+ ) {
+ document.getElementById("securityTab").hidden = true;
+ return;
+ }
+ document.getElementById("securityTab").hidden = false;
+
+ /* Set Identity section text */
+ setText("security-identity-domain-value", windowInfo.hostName);
+
+ var validity;
+ if (info.cert && !info.isBroken) {
+ validity = info.cert.validity.notAfterLocalDay;
+
+ // Try to pull out meaningful values. Technically these fields are optional
+ // so we'll employ fallbacks where appropriate. The EV spec states that Org
+ // fields must be specified for subject and issuer so that case is simpler.
+ if (info.isEV) {
+ setText("security-identity-owner-value", info.cert.organization);
+ setText("security-identity-verifier-value", info.cAName);
+ } else {
+ // Technically, a non-EV cert might specify an owner in the O field or not,
+ // depending on the CA's issuing policies. However we don't have any programmatic
+ // way to tell those apart, and no policy way to establish which organization
+ // vetting standards are good enough (that's what EV is for) so we default to
+ // treating these certs as domain-validated only.
+ document.l10n.setAttributes(
+ document.getElementById("security-identity-owner-value"),
+ "page-info-security-no-owner"
+ );
+ setText(
+ "security-identity-verifier-value",
+ info.cAName || info.cert.issuerCommonName || info.cert.issuerName
+ );
+ }
+ } else {
+ // We don't have valid identity credentials.
+ document.l10n.setAttributes(
+ document.getElementById("security-identity-owner-value"),
+ "page-info-security-no-owner"
+ );
+ document.l10n.setAttributes(
+ document.getElementById("security-identity-verifier-value"),
+ "page-info-not-specified"
+ );
+ }
+
+ if (validity) {
+ setText("security-identity-validity-value", validity);
+ } else {
+ document.getElementById("security-identity-validity-row").hidden = true;
+ }
+
+ /* Manage the View Cert button*/
+ var viewCert = document.getElementById("security-view-cert");
+ if (info.cert) {
+ viewCert.collapsed = false;
+ } else {
+ viewCert.collapsed = true;
+ }
+
+ /* Set Privacy & History section text */
+
+ // Only show quota usage data for websites, not internal sites.
+ if (uri.scheme == "http" || uri.scheme == "https") {
+ SiteDataManager.updateSites().then(() => security._updateSiteDataInfo());
+ } else {
+ document.getElementById("security-privacy-sitedata-row").hidden = true;
+ }
+
+ if (realmHasPasswords(uri)) {
+ document.l10n.setAttributes(
+ document.getElementById("security-privacy-passwords-value"),
+ "saved-passwords-yes"
+ );
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("security-privacy-passwords-value"),
+ "saved-passwords-no"
+ );
+ }
+
+ document.l10n.setAttributes(
+ document.getElementById("security-privacy-history-value"),
+ "security-visits-number",
+ { visits: previousVisitCount(windowInfo.hostName) }
+ );
+
+ /* Set the Technical Detail section messages */
+ const pkiBundle = document.getElementById("pkiBundle");
+ var hdr;
+ var msg1;
+ var msg2;
+
+ if (info.isBroken) {
+ if (info.isMixed) {
+ hdr = pkiBundle.getString("pageInfo_MixedContent");
+ msg1 = pkiBundle.getString("pageInfo_MixedContent2");
+ } else {
+ hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption", [
+ info.encryptionAlgorithm,
+ info.encryptionStrength + "",
+ info.version,
+ ]);
+ msg1 = pkiBundle.getString("pageInfo_WeakCipher");
+ }
+ msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
+ } else if (info.encryptionStrength > 0) {
+ hdr = pkiBundle.getFormattedString(
+ "pageInfo_EncryptionWithBitsAndProtocol",
+ [info.encryptionAlgorithm, info.encryptionStrength + "", info.version]
+ );
+ msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
+ msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
+ } else {
+ hdr = pkiBundle.getString("pageInfo_NoEncryption");
+ if (windowInfo.hostName != null) {
+ msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [
+ windowInfo.hostName,
+ ]);
+ } else {
+ msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
+ }
+ msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
+ }
+ setText("security-technical-shortform", hdr);
+ setText("security-technical-longform1", msg1);
+ setText("security-technical-longform2", msg2);
+
+ const ctStatus = document.getElementById(
+ "security-technical-certificate-transparency"
+ );
+ if (info.certificateTransparency) {
+ ctStatus.hidden = false;
+ ctStatus.value = pkiBundle.getString(
+ "pageInfo_CertificateTransparency_" + info.certificateTransparency
+ );
+ } else {
+ ctStatus.hidden = true;
+ }
+}
+
+function setText(id, value) {
+ var element = document.getElementById(id);
+ if (!element) {
+ return;
+ }
+ if (element.localName == "input" || element.localName == "label") {
+ element.value = value;
+ } else {
+ element.textContent = value;
+ }
+}
+
+/**
+ * Return true iff realm (proto://host:port) (extracted from uri) has
+ * saved passwords
+ */
+function realmHasPasswords(uri) {
+ return Services.logins.countLogins(uri.prePath, "", "") > 0;
+}
+
+/**
+ * Return the number of previous visits recorded for host before today.
+ *
+ * @param host - the domain name to look for in history
+ */
+function previousVisitCount(host, endTimeReference) {
+ if (!host) {
+ return 0;
+ }
+
+ var historyService = Cc[
+ "@mozilla.org/browser/nav-history-service;1"
+ ].getService(Ci.nsINavHistoryService);
+
+ var options = historyService.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ // Search for visits to this host before today
+ var query = historyService.getNewQuery();
+ query.endTimeReference = query.TIME_RELATIVE_TODAY;
+ query.endTime = 0;
+ query.domain = host;
+
+ var result = historyService.executeQuery(query, options);
+ result.root.containerOpen = true;
+ var cc = result.root.childCount;
+ result.root.containerOpen = false;
+ return cc;
+}
diff --git a/browser/base/content/popup-notifications.inc b/browser/base/content/popup-notifications.inc
new file mode 100644
index 0000000000..5b7c9ba751
--- /dev/null
+++ b/browser/base/content/popup-notifications.inc
@@ -0,0 +1,131 @@
+# to be included inside a popupset element
+
+ <panel id="notification-popup"
+ type="arrow"
+ position="after_start"
+ hidden="true"
+ orient="vertical"
+ noautofocus="true"
+ role="alert"/>
+
+ <popupnotification id="webRTC-shareDevices-notification" hidden="true">
+ <popupnotificationcontent id="webRTC-selectCamera" orient="vertical">
+ <label data-l10n-id="popup-select-camera"
+ control="webRTC-selectCamera-menulist"/>
+ <menulist id="webRTC-selectCamera-menulist">
+ <menupopup id="webRTC-selectCamera-menupopup"/>
+ </menulist>
+ </popupnotificationcontent>
+
+ <popupnotificationcontent id="webRTC-selectWindowOrScreen" orient="vertical">
+ <label id="webRTC-selectWindow-label"
+ control="webRTC-selectWindow-menulist"/>
+ <menulist id="webRTC-selectWindow-menulist"
+ oncommand="webrtcUI.updateWarningLabel(this);">
+ <menupopup id="webRTC-selectWindow-menupopup"/>
+ </menulist>
+ <description id="webRTC-all-windows-shared" hidden="true" data-l10n-id="popup-all-windows-shared"></description>
+ </popupnotificationcontent>
+
+ <popupnotificationcontent id="webRTC-preview" hidden="true">
+ <html:video id="webRTC-previewVideo" tabindex="-1"/>
+ <vbox id="webRTC-previewWarningBox">
+ <spacer flex="1"/>
+ <description id="webRTC-previewWarning"/>
+ </vbox>
+ </popupnotificationcontent>
+
+ <popupnotificationcontent id="webRTC-selectMicrophone" orient="vertical">
+ <label data-l10n-id="popup-select-microphone"
+ control="webRTC-selectMicrophone-menulist"/>
+ <menulist id="webRTC-selectMicrophone-menulist">
+ <menupopup id="webRTC-selectMicrophone-menupopup"/>
+ </menulist>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="servicesInstall-notification" hidden="true">
+ <popupnotificationcontent orient="vertical" align="start">
+ <!-- XXX bug 974146, tests are looking for this, can't remove yet. -->
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="password-notification" hidden="true">
+ <popupnotificationcontent orient="vertical">
+ <stack>
+ <html:input id="password-notification-username"
+ class="ac-has-end-icon"
+ autocompletesearch="login-doorhanger-username"
+ autocompletepopup="PopupAutoComplete"
+ is="autocomplete-input"
+ maxrows="10"
+ maxdropmarkerrows="10"/>
+ <dropmarker id="password-notification-username-dropmarker"
+ class="ac-dropmarker"/>
+ </stack>
+ <stack>
+ <html:input id="password-notification-password" type="password"/>
+ <dropmarker id="password-notification-password-dropmarker"
+ class="ac-dropmarker"
+ hidden="true"/>
+ </stack>
+ <checkbox id="password-notification-visibilityToggle" hidden="true"/>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="addon-progress-notification" is="addon-progress-notification" hidden="true">
+ <popupnotificationcontent orient="vertical">
+ <html:progress id="addon-progress-notification-progressmeter" max="100"/>
+ <label id="addon-progress-notification-progresstext" crop="end"/>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="addon-install-confirmation-notification" hidden="true">
+ <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
+ </popupnotification>
+
+ <popupnotification id="addon-webext-permissions-notification" hidden="true">
+ <popupnotificationcontent class="addon-webext-perm-notification-content" orient="vertical">
+ <description id="addon-webext-perm-text" class="addon-webext-perm-text"/>
+ <label id="addon-webext-perm-intro" class="addon-webext-perm-text"/>
+ <html:ul id="addon-webext-perm-list" class="addon-webext-perm-list"/>
+ <hbox>
+ <label id="addon-webext-perm-info" is="text-link" class="popup-notification-learnmore-link"/>
+ </hbox>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="addon-install-blocked-notification" hidden="true">
+ <popupnotificationcontent id="addon-install-blocked-content" orient="vertical">
+ <description id="addon-install-blocked-message" class="popup-notification-description"></description>
+ <hbox>
+ <label id="addon-install-blocked-info" class="popup-notification-learnmore-link" is="text-link"/>
+ </hbox>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="contextual-feature-recommendation-notification" hidden="true">
+ <popupnotificationheader id="cfr-notification-header">
+ <stack id="cfr-notification-header-stack">
+ <description id="cfr-notification-header-label"></description>
+ <label id="cfr-notification-header-link" is="text-link">
+ <xul:image id="cfr-notification-header-image"/>
+ </label>
+ </stack>
+ </popupnotificationheader>
+ <popupnotificationcontent>
+ <description id="cfr-notification-author"></description>
+ </popupnotificationcontent>
+ <popupnotificationfooter id="cfr-notification-footer" orient="vertical">
+ <vbox id="cfr-notification-footer-text-and-addon-info">
+ <description id="cfr-notification-footer-text"/>
+ <hbox id="cfr-notification-footer-addon-info">
+ <hbox id="cfr-notification-footer-filled-stars"/>
+ <hbox id="cfr-notification-footer-empty-stars"/>
+ <label id="cfr-notification-footer-users"/>
+ <spacer id="cfr-notification-footer-spacer" hidden="true"/>
+ <label id="cfr-notification-footer-learn-more-link" is="text-link"/>
+ </hbox>
+ </vbox>
+ </popupnotificationfooter>
+ </popupnotification>
diff --git a/browser/base/content/robot.ico b/browser/base/content/robot.ico
new file mode 100644
index 0000000000..8913387fc9
--- /dev/null
+++ b/browser/base/content/robot.ico
Binary files differ
diff --git a/browser/base/content/safeMode.css b/browser/base/content/safeMode.css
new file mode 100644
index 0000000000..7a2cfff8b1
--- /dev/null
+++ b/browser/base/content/safeMode.css
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#resetProfileFooter {
+ font-weight: bold;
+}
diff --git a/browser/base/content/safeMode.js b/browser/base/content/safeMode.js
new file mode 100644
index 0000000000..5c810b031a
--- /dev/null
+++ b/browser/base/content/safeMode.js
@@ -0,0 +1,90 @@
+/* -*- 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/. */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const appStartup = Services.startup;
+
+const { ResetProfile } = ChromeUtils.import(
+ "resource://gre/modules/ResetProfile.jsm"
+);
+
+var defaultToReset = false;
+
+function restartApp() {
+ appStartup.quit(appStartup.eForceQuit | appStartup.eRestart);
+}
+
+function resetProfile() {
+ // Set the reset profile environment variable.
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ env.set("MOZ_RESET_PROFILE_RESTART", "1");
+}
+
+function showResetDialog() {
+ // Prompt the user to confirm the reset.
+ let retVals = {
+ reset: false,
+ };
+ window.openDialog(
+ "chrome://global/content/resetProfile.xhtml",
+ null,
+ "chrome,modal,centerscreen,titlebar,dialog=yes",
+ retVals
+ );
+ if (!retVals.reset) {
+ return;
+ }
+ resetProfile();
+ restartApp();
+}
+
+function onDefaultButton(event) {
+ if (defaultToReset) {
+ // Prevent starting into safe mode while restarting.
+ event.preventDefault();
+ // Restart to reset the profile.
+ resetProfile();
+ restartApp();
+ }
+ // Dialog will be closed by default Event handler.
+ // Continue in safe mode. No restart needed.
+}
+
+function onCancel() {
+ appStartup.quit(appStartup.eForceQuit);
+}
+
+function onExtra1() {
+ if (defaultToReset) {
+ // Continue in safe mode
+ window.close();
+ }
+ // The reset dialog will handle starting the reset process if the user confirms.
+ showResetDialog();
+}
+
+function onLoad() {
+ const dialog = document.getElementById("safeModeDialog");
+ if (appStartup.automaticSafeModeNecessary) {
+ document.getElementById("autoSafeMode").hidden = false;
+ document.getElementById("safeMode").hidden = true;
+ if (ResetProfile.resetSupported()) {
+ document.getElementById("resetProfile").hidden = false;
+ } else {
+ // Hide the reset button is it's not supported.
+ dialog.getButton("extra1").hidden = true;
+ }
+ } else if (!ResetProfile.resetSupported()) {
+ // Hide the reset button and text if it's not supported.
+ dialog.getButton("extra1").hidden = true;
+ document.getElementById("resetProfileInstead").hidden = true;
+ }
+ document.addEventListener("dialogaccept", onDefaultButton);
+ document.addEventListener("dialogcancel", onCancel);
+ document.addEventListener("dialogextra1", onExtra1);
+}
diff --git a/browser/base/content/safeMode.xhtml b/browser/base/content/safeMode.xhtml
new file mode 100644
index 0000000000..5c95e19137
--- /dev/null
+++ b/browser/base/content/safeMode.xhtml
@@ -0,0 +1,46 @@
+<?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"?>
+<?xml-stylesheet href="chrome://browser/content/safeMode.css"?>
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ data-l10n-id="safe-mode-window"
+ data-l10n-attrs="title,style"
+ onload="onLoad()">
+<dialog id="safeModeDialog"
+ buttons="accept,extra1"
+ buttonidaccept="start-safe-mode"
+ buttonidextra1="refresh-profile">
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl"/>
+ <html:link rel="localization" href="browser/safeMode.ftl"/>
+ </linkset>
+
+ <script src="chrome://browser/content/safeMode.js"/>
+
+
+ <vbox id="autoSafeMode" hidden="true">
+ <description data-l10n-id="auto-safe-mode-description"/>
+ </vbox>
+
+ <vbox id="safeMode">
+ <label data-l10n-id="safe-mode-description" />
+ <separator class="thin"/>
+ <label data-l10n-id="safe-mode-description-details" />
+ <separator class="thin"/>
+ <label id="resetProfileInstead" data-l10n-id="refresh-profile-instead"/>
+ </vbox>
+
+ <vbox id="resetProfile" hidden="true">
+ <label data-l10n-id="refresh-profile-instead" />
+ </vbox>
+
+ <separator class="thin"/>
+</dialog>
+</window>
diff --git a/browser/base/content/sanitize.xhtml b/browser/base/content/sanitize.xhtml
new file mode 100644
index 0000000000..a4ec3b5ef9
--- /dev/null
+++ b/browser/base/content/sanitize.xhtml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/skin/sanitizeDialog.css"?>
+
+
+<?xml-stylesheet href="chrome://browser/content/sanitizeDialog.css"?>
+
+<!DOCTYPE window>
+
+<window id="SanitizeDialog"
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="lastSelected screenX screenY"
+ data-l10n-id="dialog-title"
+ data-l10n-attrs="style"
+ onload="gSanitizePromptDialog.init();">
+<dialog buttons="accept,cancel">
+
+ <linkset>
+ <html:link rel="localization" href="browser/sanitize.ftl"/>
+ </linkset>
+
+ <script src="chrome://global/content/preferencesBindings.js"/>
+ <script src="chrome://browser/content/sanitizeDialog.js"/>
+
+ <hbox id="SanitizeDurationBox" align="center">
+ <label data-l10n-id="clear-time-duration-prefix"
+ control="sanitizeDurationChoice"
+ id="sanitizeDurationLabel"/>
+ <menulist id="sanitizeDurationChoice"
+ preference="privacy.sanitize.timeSpan"
+ onselect="gSanitizePromptDialog.selectByTimespan();"
+ flex="1">
+ <menupopup id="sanitizeDurationPopup">
+ <menuitem data-l10n-id="clear-time-duration-value-last-hour" value="1"/>
+ <menuitem data-l10n-id="clear-time-duration-value-last-2-hours" value="2"/>
+ <menuitem data-l10n-id="clear-time-duration-value-last-4-hours" value="3"/>
+ <menuitem data-l10n-id="clear-time-duration-value-today" value="4"/>
+ <menuseparator/>
+ <menuitem data-l10n-id="clear-time-duration-value-everything" value="0"/>
+ </menupopup>
+ </menulist>
+ <label id="sanitizeDurationSuffixLabel"
+ data-l10n-id="clear-time-duration-suffix"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <vbox id="sanitizeEverythingWarningBox">
+ <spacer flex="1"/>
+ <hbox align="center">
+ <image id="sanitizeEverythingWarningIcon"/>
+ <vbox id="sanitizeEverythingWarningDescBox" flex="1">
+ <description id="sanitizeEverythingWarning"/>
+ <description id="sanitizeEverythingUndoWarning" data-l10n-id="sanitize-everything-undo-warning"></description>
+ </vbox>
+ </hbox>
+ <spacer flex="1"/>
+ </vbox>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <label><html:h2 data-l10n-id="history-section-label"/></label>
+ <hbox>
+ <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+ <checkbox data-l10n-id="item-history-and-downloads"
+ preference="privacy.cpd.history"/>
+ <checkbox data-l10n-id="item-active-logins"
+ preference="privacy.cpd.sessions"/>
+ <checkbox data-l10n-id="item-form-search-history"
+ preference="privacy.cpd.formdata"/>
+ </vbox>
+ <vbox flex="1">
+ <checkbox data-l10n-id="item-cookies"
+ preference="privacy.cpd.cookies"/>
+ <checkbox data-l10n-id="item-cache"
+ preference="privacy.cpd.cache"/>
+ </vbox>
+ </hbox>
+ </groupbox>
+ <groupbox>
+ <label><html:h2 data-l10n-id="data-section-label"/></label>
+ <hbox>
+ <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+ <checkbox data-l10n-id="item-site-preferences"
+ preference="privacy.cpd.siteSettings"/>
+ </vbox>
+ <vbox flex="1">
+ <checkbox data-l10n-id="item-offline-apps"
+ preference="privacy.cpd.offlineApps"/>
+ </vbox>
+ </hbox>
+ </groupbox>
+</dialog>
+</window>
diff --git a/browser/base/content/sanitizeDialog.css b/browser/base/content/sanitizeDialog.css
new file mode 100644
index 0000000000..6d0711a5e6
--- /dev/null
+++ b/browser/base/content/sanitizeDialog.css
@@ -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/. */
+
+/* Sanitize everything warnings */
+
+#sanitizeEverythingWarning,
+#sanitizeEverythingUndoWarning {
+ white-space: pre-wrap;
+}
+
+/* Hide the duration dropdown suffix label if it's empty. Otherwise it
+ takes up a little space, causing the end of the dropdown to not be aligned
+ with the warning box. */
+#sanitizeDurationSuffixLabel[value=""] {
+ display: none;
+}
+
+/* Sanitize everything warning box */
+#sanitizeEverythingWarningBox {
+ /* Fallback colors are used when the dialog is open outside of in-content prefs */
+ background-color: var(--in-content-box-background, Window);
+ border: 1px solid var(--in-content-box-border-color, ThreeDDarkShadow);
+ border-radius: 5px;
+ padding: 16px;
+}
+
+#sanitizeEverythingWarningIcon {
+ padding: 0;
+ margin: 0;
+}
+
+#sanitizeEverythingWarningDescBox {
+ padding: 0 16px;
+ margin: 0;
+}
diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js
new file mode 100644
index 0000000000..ad51ea6efb
--- /dev/null
+++ b/browser/base/content/sanitizeDialog.js
@@ -0,0 +1,232 @@
+/* -*- 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/. */
+
+/* import-globals-from ../../../toolkit/content/preferencesBindings.js */
+
+var { Sanitizer } = ChromeUtils.import("resource:///modules/Sanitizer.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+Preferences.addAll([
+ { id: "privacy.cpd.history", type: "bool" },
+ { id: "privacy.cpd.formdata", type: "bool" },
+ { id: "privacy.cpd.downloads", type: "bool", disabled: true },
+ { id: "privacy.cpd.cookies", type: "bool" },
+ { id: "privacy.cpd.cache", type: "bool" },
+ { id: "privacy.cpd.sessions", type: "bool" },
+ { id: "privacy.cpd.offlineApps", type: "bool" },
+ { id: "privacy.cpd.siteSettings", type: "bool" },
+ { id: "privacy.sanitize.timeSpan", type: "int" },
+]);
+
+var gSanitizePromptDialog = {
+ get selectedTimespan() {
+ var durList = document.getElementById("sanitizeDurationChoice");
+ return parseInt(durList.value);
+ },
+
+ get warningBox() {
+ return document.getElementById("sanitizeEverythingWarningBox");
+ },
+
+ init() {
+ // This is used by selectByTimespan() to determine if the window has loaded.
+ this._inited = true;
+ this._dialog = document.querySelector("dialog");
+
+ let OKButton = this._dialog.getButton("accept");
+ document.l10n.setAttributes(OKButton, "sanitize-button-ok");
+
+ document.addEventListener("dialogaccept", function(e) {
+ gSanitizePromptDialog.sanitize(e);
+ });
+
+ this.registerSyncFromPrefListeners();
+
+ if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
+ this.prepareWarning();
+ this.warningBox.hidden = false;
+ document.l10n.setAttributes(
+ document.documentElement,
+ "dialog-title-everything"
+ );
+ let warningDesc = document.getElementById("sanitizeEverythingWarning");
+ // Ensure we've translated and sized the warning.
+ document.mozSubdialogReady = document.l10n
+ .translateFragment(warningDesc)
+ .then(() => {
+ // And then ensure we've run layout.
+ let rootWin = window.browsingContext.topChromeWindow;
+ return rootWin.promiseDocumentFlushed(() => {});
+ });
+ } else {
+ this.warningBox.hidden = true;
+ }
+
+ // Only apply the following if the dialog is opened outside of the Preferences.
+ if (!this._dialog.hasAttribute("subdialog")) {
+ // The style attribute on the dialog may get set after the dialog has been sized.
+ // Force the dialog to size again after the style attribute has been applied.
+ document.l10n.translateElements([document.documentElement]).then(() => {
+ window.sizeToContent();
+ });
+ }
+ },
+
+ selectByTimespan() {
+ // This method is the onselect handler for the duration dropdown. As a
+ // result it's called a couple of times before onload calls init().
+ if (!this._inited) {
+ return;
+ }
+
+ var warningBox = this.warningBox;
+
+ // If clearing everything
+ if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
+ this.prepareWarning();
+ if (warningBox.hidden) {
+ warningBox.hidden = false;
+ window.resizeBy(0, warningBox.getBoundingClientRect().height);
+ }
+ document.l10n.setAttributes(
+ document.documentElement,
+ "dialog-title-everything"
+ );
+ return;
+ }
+
+ // If clearing a specific time range
+ if (!warningBox.hidden) {
+ window.resizeBy(0, -warningBox.getBoundingClientRect().height);
+ warningBox.hidden = true;
+ }
+ document.l10n.setAttributes(document.documentElement, "dialog-title");
+ },
+
+ sanitize(event) {
+ // Update pref values before handing off to the sanitizer (bug 453440)
+ this.updatePrefs();
+
+ // As the sanitize is async, we disable the buttons, update the label on
+ // the 'accept' button to indicate things are happening and return false -
+ // once the async operation completes (either with or without errors)
+ // we close the window.
+ let acceptButton = this._dialog.getButton("accept");
+ acceptButton.disabled = true;
+ document.l10n.setAttributes(acceptButton, "sanitize-button-clearing");
+ this._dialog.getButton("cancel").disabled = true;
+
+ try {
+ let range = Sanitizer.getClearRange(this.selectedTimespan);
+ let options = {
+ ignoreTimespan: !range,
+ range,
+ };
+ Sanitizer.sanitize(null, options)
+ .catch(Cu.reportError)
+ .then(() => window.close())
+ .catch(Cu.reportError);
+ event.preventDefault();
+ } catch (er) {
+ Cu.reportError("Exception during sanitize: " + er);
+ }
+ },
+
+ /**
+ * If the panel that displays a warning when the duration is "Everything" is
+ * not set up, sets it up. Otherwise does nothing.
+ */
+ prepareWarning() {
+ // If the date and time-aware locale warning string is ever used again,
+ // initialize it here. Currently we use the no-visits warning string,
+ // which does not include date and time. See bug 480169 comment 48.
+
+ var warningDesc = document.getElementById("sanitizeEverythingWarning");
+ if (this.hasNonSelectedItems()) {
+ document.l10n.setAttributes(warningDesc, "sanitize-selected-warning");
+ } else {
+ document.l10n.setAttributes(warningDesc, "sanitize-everything-warning");
+ }
+ },
+
+ /**
+ * Return the boolean prefs that enable/disable clearing of various kinds
+ * of history. The only pref this excludes is privacy.sanitize.timeSpan.
+ */
+ _getItemPrefs() {
+ return Preferences.getAll().filter(
+ p => p.id !== "privacy.sanitize.timeSpan"
+ );
+ },
+
+ /**
+ * Called when the value of a preference element is synced from the actual
+ * pref. Enables or disables the OK button appropriately.
+ */
+ onReadGeneric() {
+ // Find any other pref that's checked and enabled (except for
+ // privacy.sanitize.timeSpan, which doesn't affect the button's status).
+ var found = this._getItemPrefs().some(
+ pref => !!pref.value && !pref.disabled
+ );
+
+ try {
+ this._dialog.getButton("accept").disabled = !found;
+ } catch (e) {}
+
+ // Update the warning prompt if needed
+ this.prepareWarning();
+
+ return undefined;
+ },
+
+ /**
+ * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
+ * Because the type of this prefwindow is "child" -- and that's needed because
+ * without it the dialog has no OK and Cancel buttons -- the prefs are not
+ * updated on dialogaccept. We must therefore manually set the prefs
+ * from their corresponding preference elements.
+ */
+ updatePrefs() {
+ Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, this.selectedTimespan);
+
+ // Keep the pref for the download history in sync with the history pref.
+ Preferences.get("privacy.cpd.downloads").value = Preferences.get(
+ "privacy.cpd.history"
+ ).value;
+
+ // Now manually set the prefs from their corresponding preference
+ // elements.
+ var prefs = this._getItemPrefs();
+ for (let i = 0; i < prefs.length; ++i) {
+ var p = prefs[i];
+ Services.prefs.setBoolPref(p.id, p.value);
+ }
+ },
+
+ /**
+ * Check if all of the history items have been selected like the default status.
+ */
+ hasNonSelectedItems() {
+ let checkboxes = document.querySelectorAll("checkbox[preference]");
+ for (let i = 0; i < checkboxes.length; ++i) {
+ let pref = Preferences.get(checkboxes[i].getAttribute("preference"));
+ if (!pref.value) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Register syncFromPref listener functions.
+ */
+ registerSyncFromPrefListeners() {
+ let checkboxes = document.querySelectorAll("checkbox[preference]");
+ for (let checkbox of checkboxes) {
+ Preferences.addSyncFromPrefListener(checkbox, () => this.onReadGeneric());
+ }
+ },
+};
diff --git a/browser/base/content/static-robot.png b/browser/base/content/static-robot.png
new file mode 100644
index 0000000000..52338ff81e
--- /dev/null
+++ b/browser/base/content/static-robot.png
Binary files differ
diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js
new file mode 100644
index 0000000000..e45449d080
--- /dev/null
+++ b/browser/base/content/tab-content.js
@@ -0,0 +1,58 @@
+/* -*- 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/. */
+
+/* This content script contains code that requires a tab browser. */
+
+/* eslint-env mozilla/frame-script */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm"
+);
+
+// BrowserChildGlobal
+var global = this;
+
+var WebBrowserChrome = {
+ onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
+ return BrowserUtils.onBeforeLinkTraversal(
+ originalTarget,
+ linkURI,
+ linkNode,
+ isAppTab
+ );
+ },
+
+ // Check whether this URI should load in the current process
+ shouldLoadURI(
+ aDocShell,
+ aURI,
+ aReferrerInfo,
+ aHasPostData,
+ aTriggeringPrincipal,
+ aCsp
+ ) {
+ return true;
+ },
+
+ shouldLoadURIInThisProcess(aURI) {
+ return true;
+ },
+};
+
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ let tabchild = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIBrowserChild);
+ tabchild.webBrowserChrome = WebBrowserChrome;
+}
+
+Services.obs.notifyObservers(this, "tab-content-frameloader-created");
+
+// This is a temporary hack to prevent regressions (bug 1471327).
+void content;
diff --git a/browser/base/content/tabbrowser-tab.js b/browser/base/content/tabbrowser-tab.js
new file mode 100644
index 0000000000..8668a24611
--- /dev/null
+++ b/browser/base/content/tabbrowser-tab.js
@@ -0,0 +1,676 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 is loaded into chrome windows with the subscript loader. Wrap in
+// a block to prevent accidentally leaking globals onto `window`.
+{
+ class MozTabbrowserTab extends MozElements.MozTab {
+ static get markup() {
+ return `
+ <stack class="tab-stack" flex="1">
+ <vbox class="tab-background">
+ <hbox class="tab-line"/>
+ <spacer flex="1" class="tab-background-inner"/>
+ <hbox class="tab-bottom-line"/>
+ </vbox>
+ <hbox class="tab-loading-burst"/>
+ <hbox class="tab-content" align="center">
+ <hbox class="tab-throbber" layer="true"/>
+ <hbox class="tab-icon-pending"/>
+ <image class="tab-icon-image" validate="never" role="presentation"/>
+ <image class="tab-sharing-icon-overlay" role="presentation"/>
+ <image class="tab-icon-overlay" role="presentation"/>
+ <hbox class="tab-label-container"
+ onoverflow="this.setAttribute('textoverflow', 'true');"
+ onunderflow="this.removeAttribute('textoverflow');"
+ flex="1">
+ <label class="tab-text tab-label" role="presentation"/>
+ </hbox>
+ <image class="tab-icon-sound" role="presentation"/>
+ <vbox class="tab-label-container proton"
+ onoverflow="this.setAttribute('textoverflow', 'true');"
+ onunderflow="this.removeAttribute('textoverflow');"
+ flex="1">
+ <label class="tab-text tab-label" role="presentation"/>
+ <hbox class="tab-icon-sound">
+ <image class="tab-icon-sound-image" role="presentation"/>
+ <label class="tab-icon-sound-playing-label" data-l10n-id="browser-tab-audio-playing" role="presentation"/>
+ <label class="tab-icon-sound-muted-label" data-l10n-id="browser-tab-audio-muted" role="presentation"/>
+ </hbox>
+ </vbox>
+ <image class="tab-close-button close-icon" role="presentation"/>
+ </hbox>
+ </stack>
+ `;
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("mouseover", this);
+ this.addEventListener("mouseout", this);
+ this.addEventListener("dragstart", this, true);
+ this.addEventListener("dragstart", this);
+ this.addEventListener("mousedown", this);
+ this.addEventListener("mouseup", this);
+ this.addEventListener("click", this);
+ this.addEventListener("dblclick", this, true);
+ this.addEventListener("animationend", this);
+ this.addEventListener("focus", this);
+ this.addEventListener("AriaFocus", this);
+
+ this._selectedOnFirstMouseDown = false;
+
+ /**
+ * Describes how the tab ended up in this mute state. May be any of:
+ *
+ * - undefined: The tabs mute state has never changed.
+ * - null: The mute state was last changed through the UI.
+ * - Any string: The ID was changed through an extension API. The string
+ * must be the ID of the extension which changed it.
+ */
+ this.muteReason = undefined;
+
+ this.mOverCloseButton = false;
+
+ this.mCorrespondingMenuitem = null;
+
+ this.closing = false;
+ }
+
+ static get inheritedAttributes() {
+ return {
+ ".tab-background": "selected=visuallyselected,fadein,multiselected",
+ ".tab-line":
+ "selected=visuallyselected,multiselected,before-multiselected",
+ ".tab-loading-burst": "pinned,bursting,notselectedsinceload",
+ ".tab-content":
+ "pinned,selected=visuallyselected,titlechanged,attention",
+ ".tab-throbber":
+ "fadein,pinned,busy,progress,selected=visuallyselected",
+ ".tab-icon-pending":
+ "fadein,pinned,busy,progress,selected=visuallyselected,pendingicon",
+ ".tab-icon-image":
+ "src=image,triggeringprincipal=iconloadingprincipal,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture",
+ ".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
+ ".tab-icon-overlay":
+ "pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked",
+ ".tab-label-container":
+ "pinned,selected=visuallyselected,labeldirection",
+ ".tab-label-container.proton":
+ "pinned,selected=visuallyselected,labeldirection",
+ ".tab-label":
+ "text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
+ ".tab-label-container.proton .tab-label":
+ "text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
+ ".tab-icon-sound":
+ "soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,pictureinpicture",
+ ".tab-label-container.proton .tab-icon-sound":
+ "soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,pictureinpicture",
+ ".tab-close-button": "fadein,pinned,selected=visuallyselected",
+ };
+ }
+
+ connectedCallback() {
+ this.initialize();
+ }
+
+ initialize() {
+ if (this._initialized) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+ this.initializeAttributeInheritance();
+ this.setAttribute("context", "tabContextMenu");
+ this._initialized = true;
+
+ if (!("_lastAccessed" in this)) {
+ this.updateLastAccessed();
+ }
+ }
+
+ get container() {
+ return gBrowser.tabContainer;
+ }
+
+ set _visuallySelected(val) {
+ if (val == (this.getAttribute("visuallyselected") == "true")) {
+ return val;
+ }
+
+ if (val) {
+ this.setAttribute("visuallyselected", "true");
+ } else {
+ this.removeAttribute("visuallyselected");
+ }
+ gBrowser._tabAttrModified(this, ["visuallyselected"]);
+
+ return val;
+ }
+
+ set _selected(val) {
+ // in e10s we want to only pseudo-select a tab before its rendering is done, so that
+ // the rest of the system knows that the tab is selected, but we don't want to update its
+ // visual status to selected until after we receive confirmation that its content has painted.
+ if (val) {
+ this.setAttribute("selected", "true");
+ } else {
+ this.removeAttribute("selected");
+ }
+
+ // If we're non-e10s we should update the visual selection as well at the same time,
+ // *or* if we're e10s and the visually selected tab isn't changing, in which case the
+ // tab switcher code won't run and update anything else (like the before- and after-
+ // selected attributes).
+ if (
+ !gMultiProcessBrowser ||
+ (val && this.hasAttribute("visuallyselected"))
+ ) {
+ this._visuallySelected = val;
+ }
+
+ return val;
+ }
+
+ get pinned() {
+ return this.getAttribute("pinned") == "true";
+ }
+
+ get hidden() {
+ return this.getAttribute("hidden") == "true";
+ }
+
+ get muted() {
+ return this.getAttribute("muted") == "true";
+ }
+
+ get multiselected() {
+ return this.getAttribute("multiselected") == "true";
+ }
+
+ get beforeMultiselected() {
+ return this.getAttribute("before-multiselected") == "true";
+ }
+
+ get userContextId() {
+ return this.hasAttribute("usercontextid")
+ ? parseInt(this.getAttribute("usercontextid"))
+ : 0;
+ }
+
+ get soundPlaying() {
+ return this.getAttribute("soundplaying") == "true";
+ }
+
+ get pictureinpicture() {
+ return this.getAttribute("pictureinpicture") == "true";
+ }
+
+ get activeMediaBlocked() {
+ return this.getAttribute("activemedia-blocked") == "true";
+ }
+
+ get isEmpty() {
+ // Determines if a tab is "empty", usually used in the context of determining
+ // if it's ok to close the tab.
+ if (this.hasAttribute("busy")) {
+ return false;
+ }
+
+ if (this.hasAttribute("customizemode")) {
+ return false;
+ }
+
+ let browser = this.linkedBrowser;
+ if (!isBlankPageURL(browser.currentURI.spec)) {
+ return false;
+ }
+
+ if (!BrowserUtils.checkEmptyPageOrigin(browser)) {
+ return false;
+ }
+
+ if (browser.canGoForward || browser.canGoBack) {
+ return false;
+ }
+
+ return true;
+ }
+
+ get lastAccessed() {
+ return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
+ }
+
+ get _overPlayingIcon() {
+ let iconVisible =
+ this.soundPlaying || this.muted || this.activeMediaBlocked;
+
+ let soundPlayingIcon = this.soundPlayingIcon;
+ let overlayIcon = this.overlayIcon;
+ return (
+ (soundPlayingIcon && soundPlayingIcon.matches(":hover")) ||
+ (overlayIcon && overlayIcon.matches(":hover") && iconVisible)
+ );
+ }
+
+ get soundPlayingIcon() {
+ return gProtonTabs
+ ? this.querySelector(".tab-label-container.proton > .tab-icon-sound")
+ : this.querySelector(".tab-icon-sound");
+ }
+
+ get overlayIcon() {
+ return this.querySelector(".tab-icon-overlay");
+ }
+
+ get throbber() {
+ return this.querySelector(".tab-throbber");
+ }
+
+ get iconImage() {
+ return this.querySelector(".tab-icon-image");
+ }
+
+ get sharingIcon() {
+ return this.querySelector(".tab-sharing-icon-overlay");
+ }
+
+ get textLabel() {
+ return this.querySelector(".tab-label");
+ }
+
+ get closeButton() {
+ return this.querySelector(".tab-close-button");
+ }
+
+ _isEventForTabSoundIcon(event) {
+ return gProtonTabs
+ ? event.target.closest(".tab-icon-sound")
+ : event.target.classList.contains("tab-icon-sound");
+ }
+
+ updateLastAccessed(aDate) {
+ this._lastAccessed = this.selected ? Infinity : aDate || Date.now();
+ }
+
+ on_mouseover(event) {
+ if (event.target.classList.contains("tab-close-button")) {
+ this.mOverCloseButton = true;
+ }
+ this._mouseenter();
+ }
+
+ on_mouseout(event) {
+ if (event.target.classList.contains("tab-close-button")) {
+ this.mOverCloseButton = false;
+ }
+ this._mouseleave();
+ }
+
+ on_dragstart(event) {
+ if (event.eventPhase == Event.CAPTURING_PHASE) {
+ this.style.MozUserFocus = "";
+ } else if (
+ this.mOverCloseButton ||
+ gSharedTabWarning.willShowSharedTabWarning(this)
+ ) {
+ event.stopPropagation();
+ }
+ }
+
+ on_mousedown(event) {
+ let eventMaySelectTab = true;
+ let tabContainer = this.container;
+
+ if (
+ tabContainer._closeTabByDblclick &&
+ event.button == 0 &&
+ event.detail == 1
+ ) {
+ this._selectedOnFirstMouseDown = this.selected;
+ }
+
+ if (this.selected) {
+ this.style.MozUserFocus = "ignore";
+ } else if (
+ event.target.classList.contains("tab-close-button") ||
+ this._isEventForTabSoundIcon(event) ||
+ event.target.classList.contains("tab-icon-overlay")
+ ) {
+ eventMaySelectTab = false;
+ }
+
+ if (event.button == 1) {
+ gBrowser.warmupTab(gBrowser._findTabToBlurTo(this));
+ }
+
+ if (event.button == 0) {
+ let shiftKey = event.shiftKey;
+ let accelKey = event.getModifierState("Accel");
+ if (shiftKey) {
+ eventMaySelectTab = false;
+ const lastSelectedTab = gBrowser.lastMultiSelectedTab;
+ if (!accelKey) {
+ gBrowser.selectedTab = lastSelectedTab;
+
+ // Make sure selection is cleared when tab-switch doesn't happen.
+ gBrowser.clearMultiSelectedTabs();
+ }
+ gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
+ } else if (accelKey) {
+ // Ctrl (Cmd for mac) key is pressed
+ eventMaySelectTab = false;
+ if (this.multiselected) {
+ gBrowser.removeFromMultiSelectedTabs(this);
+ } else if (this != gBrowser.selectedTab) {
+ gBrowser.addToMultiSelectedTabs(this);
+ gBrowser.lastMultiSelectedTab = this;
+ }
+ } else if (!this.selected && this.multiselected) {
+ gBrowser.lockClearMultiSelectionOnce();
+ }
+ }
+
+ if (gSharedTabWarning.willShowSharedTabWarning(this)) {
+ eventMaySelectTab = false;
+ }
+
+ if (eventMaySelectTab) {
+ super.on_mousedown(event);
+ }
+ }
+
+ on_mouseup(event) {
+ // Make sure that clear-selection is released.
+ // Otherwise selection using Shift key may be broken.
+ gBrowser.unlockClearMultiSelection();
+
+ this.style.MozUserFocus = "";
+ }
+
+ on_click(event) {
+ if (event.button != 0) {
+ return;
+ }
+
+ if (event.getModifierState("Accel") || event.shiftKey) {
+ return;
+ }
+
+ if (
+ gBrowser.multiSelectedTabsCount > 0 &&
+ !event.target.classList.contains("tab-close-button") &&
+ !this._isEventForTabSoundIcon(event) &&
+ !event.target.classList.contains("tab-icon-overlay")
+ ) {
+ // Tabs were previously multi-selected and user clicks on a tab
+ // without holding Ctrl/Cmd Key
+ gBrowser.clearMultiSelectedTabs();
+ }
+
+ if (
+ this._isEventForTabSoundIcon(event) ||
+ (event.target.classList.contains("tab-icon-overlay") &&
+ (this.soundPlaying || this.muted || this.activeMediaBlocked))
+ ) {
+ if (this.multiselected) {
+ gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
+ } else {
+ if (this._isEventForTabSoundIcon(event) && this.pictureinpicture) {
+ // When Picture-in-Picture is open, we repurpose '.tab-icon-sound' as
+ // an inert Picture-in-Picture indicator, and expose the '.tab-icon-overlay'
+ // as the mechanism for muting the tab, so we don't need to handle clicks on
+ // '.tab-icon-sound' in this case.
+ return;
+ }
+ this.toggleMuteAudio();
+ }
+ return;
+ }
+
+ if (event.target.classList.contains("tab-close-button")) {
+ if (this.multiselected) {
+ gBrowser.removeMultiSelectedTabs();
+ } else {
+ gBrowser.removeTab(this, {
+ animate: true,
+ byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
+ });
+ }
+ // This enables double-click protection for the tab container
+ // (see tabbrowser-tabs 'click' handler).
+ gBrowser.tabContainer._blockDblClick = true;
+ }
+ }
+
+ on_dblclick(event) {
+ if (event.button != 0) {
+ return;
+ }
+
+ // for the one-close-button case
+ if (event.target.classList.contains("tab-close-button")) {
+ event.stopPropagation();
+ }
+
+ let tabContainer = this.container;
+ if (
+ tabContainer._closeTabByDblclick &&
+ this._selectedOnFirstMouseDown &&
+ this.selected &&
+ !(
+ this._isEventForTabSoundIcon(event) ||
+ event.target.classList.contains("tab-icon-overlay")
+ )
+ ) {
+ gBrowser.removeTab(this, {
+ animate: true,
+ byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
+ });
+ }
+ }
+
+ on_animationend(event) {
+ if (event.target.classList.contains("tab-loading-burst")) {
+ this.removeAttribute("bursting");
+ }
+ }
+
+ _mouseenter() {
+ if (this.hidden || this.closing) {
+ return;
+ }
+
+ let tabContainer = this.container;
+ let visibleTabs = tabContainer._getVisibleTabs();
+ let tabIndex = visibleTabs.indexOf(this);
+
+ if (this.selected) {
+ tabContainer._handleTabSelect();
+ }
+
+ if (tabIndex == 0) {
+ tabContainer._beforeHoveredTab = null;
+ } else {
+ let candidate = visibleTabs[tabIndex - 1];
+ let separatedByScrollButton =
+ tabContainer.getAttribute("overflow") == "true" &&
+ candidate.pinned &&
+ !this.pinned;
+ if (!candidate.selected && !separatedByScrollButton) {
+ tabContainer._beforeHoveredTab = candidate;
+ candidate.setAttribute("beforehovered", "true");
+ }
+ }
+
+ if (tabIndex == visibleTabs.length - 1) {
+ tabContainer._afterHoveredTab = null;
+ } else {
+ let candidate = visibleTabs[tabIndex + 1];
+ if (!candidate.selected) {
+ tabContainer._afterHoveredTab = candidate;
+ candidate.setAttribute("afterhovered", "true");
+ }
+ }
+
+ tabContainer._hoveredTab = this;
+ if (this.linkedPanel && !this.selected) {
+ this.linkedBrowser.unselectedTabHover(true);
+ this.startUnselectedTabHoverTimer();
+ }
+
+ // Prepare connection to host beforehand.
+ SessionStore.speculativeConnectOnTabHover(this);
+
+ let tabToWarm = this;
+ if (this.mOverCloseButton) {
+ tabToWarm = gBrowser._findTabToBlurTo(this);
+ }
+ gBrowser.warmupTab(tabToWarm);
+ }
+
+ _mouseleave() {
+ let tabContainer = this.container;
+ if (tabContainer._beforeHoveredTab) {
+ tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
+ tabContainer._beforeHoveredTab = null;
+ }
+ if (tabContainer._afterHoveredTab) {
+ tabContainer._afterHoveredTab.removeAttribute("afterhovered");
+ tabContainer._afterHoveredTab = null;
+ }
+
+ tabContainer._hoveredTab = null;
+ if (this.linkedPanel && !this.selected) {
+ this.linkedBrowser.unselectedTabHover(false);
+ this.cancelUnselectedTabHoverTimer();
+ }
+ }
+
+ startUnselectedTabHoverTimer() {
+ // Only record data when we need to.
+ if (!this.linkedBrowser.shouldHandleUnselectedTabHover) {
+ return;
+ }
+
+ if (
+ !TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
+ ) {
+ TelemetryStopwatch.start("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
+ }
+
+ if (this._hoverTabTimer) {
+ clearTimeout(this._hoverTabTimer);
+ this._hoverTabTimer = null;
+ }
+ }
+
+ cancelUnselectedTabHoverTimer() {
+ // Since we're listening "mouseout" event, instead of "mouseleave".
+ // Every time the cursor is moving from the tab to its child node (icon),
+ // it would dispatch "mouseout"(for tab) first and then dispatch
+ // "mouseover" (for icon, eg: close button, speaker icon) soon.
+ // It causes we would cancel present TelemetryStopwatch immediately
+ // when cursor is moving on the icon, and then start a new one.
+ // In order to avoid this situation, we could delay cancellation and
+ // remove it if we get "mouseover" within very short period.
+ this._hoverTabTimer = setTimeout(() => {
+ if (
+ TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
+ ) {
+ TelemetryStopwatch.cancel("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
+ }
+ }, 100);
+ }
+
+ finishUnselectedTabHoverTimer() {
+ // Stop timer when the tab is opened.
+ if (
+ TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
+ ) {
+ TelemetryStopwatch.finish("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
+ }
+ }
+
+ toggleMuteAudio(aMuteReason) {
+ let browser = this.linkedBrowser;
+ let modifiedAttrs = [];
+ let hist = Services.telemetry.getHistogramById(
+ "TAB_AUDIO_INDICATOR_USED"
+ );
+
+ if (this.activeMediaBlocked) {
+ this.removeAttribute("activemedia-blocked");
+ modifiedAttrs.push("activemedia-blocked");
+
+ browser.resumeMedia();
+ hist.add(3 /* unblockByClickingIcon */);
+ } else {
+ if (browser.audioMuted) {
+ if (this.linkedPanel) {
+ // "Lazy Browser" should not invoke its unmute method
+ browser.unmute();
+ }
+ this.removeAttribute("muted");
+ hist.add(1 /* unmute */);
+ } else {
+ if (this.linkedPanel) {
+ // "Lazy Browser" should not invoke its mute method
+ browser.mute();
+ }
+ this.setAttribute("muted", "true");
+ hist.add(0 /* mute */);
+ }
+ this.muteReason = aMuteReason || null;
+ modifiedAttrs.push("muted");
+ }
+ gBrowser._tabAttrModified(this, modifiedAttrs);
+ }
+
+ setUserContextId(aUserContextId) {
+ if (aUserContextId) {
+ if (this.linkedBrowser) {
+ this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
+ }
+ this.setAttribute("usercontextid", aUserContextId);
+ } else {
+ if (this.linkedBrowser) {
+ this.linkedBrowser.removeAttribute("usercontextid");
+ }
+ this.removeAttribute("usercontextid");
+ }
+
+ ContextualIdentityService.setTabStyle(this);
+ }
+
+ updateA11yDescription() {
+ let prevDescTab = gBrowser.tabContainer.querySelector(
+ "tab[aria-describedby]"
+ );
+ if (prevDescTab) {
+ // We can only have a description for the focused tab.
+ prevDescTab.removeAttribute("aria-describedby");
+ }
+ let desc = document.getElementById("tabbrowser-tab-a11y-desc");
+ desc.textContent = gBrowser.getTabTooltip(this, false);
+ this.setAttribute("aria-describedby", "tabbrowser-tab-a11y-desc");
+ }
+
+ on_focus(event) {
+ this.updateA11yDescription();
+ }
+
+ on_AriaFocus(event) {
+ this.updateA11yDescription();
+ }
+ }
+
+ customElements.define("tabbrowser-tab", MozTabbrowserTab, {
+ extends: "tab",
+ });
+}
diff --git a/browser/base/content/tabbrowser-tabs.js b/browser/base/content/tabbrowser-tabs.js
new file mode 100644
index 0000000000..e0c2a6c73f
--- /dev/null
+++ b/browser/base/content/tabbrowser-tabs.js
@@ -0,0 +1,2040 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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-env mozilla/browser-window */
+
+"use strict";
+
+// This is loaded into all browser windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+ class MozTabbrowserTabs extends MozElements.TabsBase {
+ constructor() {
+ super();
+
+ this.addEventListener("TabSelect", this);
+ this.addEventListener("TabClose", this);
+ this.addEventListener("TabAttrModified", this);
+ this.addEventListener("TabHide", this);
+ this.addEventListener("TabShow", this);
+ this.addEventListener("transitionend", this);
+ this.addEventListener("dblclick", this);
+ this.addEventListener("click", this);
+ this.addEventListener("click", this, true);
+ this.addEventListener("keydown", this, { mozSystemGroup: true });
+ this.addEventListener("dragstart", this);
+ this.addEventListener("dragover", this);
+ this.addEventListener("drop", this);
+ this.addEventListener("dragend", this);
+ this.addEventListener("dragexit", this);
+ }
+
+ init() {
+ this.arrowScrollbox = this.querySelector("arrowscrollbox");
+
+ this.baseConnect();
+
+ this._firstTab = null;
+ this._lastTab = null;
+ this._beforeSelectedTab = null;
+ this._beforeHoveredTab = null;
+ this._afterHoveredTab = null;
+ this._hoveredTab = null;
+ this._blockDblClick = false;
+ this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
+ this._dragOverDelay = 350;
+ this._dragTime = 0;
+ this._closeButtonsUpdatePending = false;
+ this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
+ this._tabDefaultMaxWidth = NaN;
+ this._lastTabClosedByMouse = false;
+ this._hasTabTempMaxWidth = false;
+ this._scrollButtonWidth = 0;
+ this._lastNumPinned = 0;
+ this._pinnedTabsLayoutCache = null;
+ this._animateElement = this.arrowScrollbox;
+ this._tabClipWidth = Services.prefs.getIntPref(
+ "browser.tabs.tabClipWidth"
+ );
+ this._hiddenSoundPlayingTabs = new Set();
+
+ // Normal tab title is used also in the permanent private browsing mode.
+ let strId =
+ PrivateBrowsingUtils.isWindowPrivate(window) &&
+ !Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
+ ? "emptyPrivateTabTitle"
+ : "emptyTabTitle";
+ this.emptyTabTitle = gTabBrowserBundle.GetStringFromName("tabs." + strId);
+
+ var tab = this.allTabs[0];
+ tab.label = this.emptyTabTitle;
+
+ this.newTabButton.setAttribute(
+ "aria-label",
+ GetDynamicShortcutTooltipText("tabs-newtab-button")
+ );
+
+ window.addEventListener("resize", this);
+
+ this.boundObserve = (...args) => this.observe(...args);
+ Services.prefs.addObserver("privacy.userContext", this.boundObserve);
+ this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_tabMinWidthPref",
+ "browser.tabs.tabMinWidth",
+ null,
+ (pref, prevValue, newValue) => (this._tabMinWidth = newValue),
+ newValue => {
+ const LIMIT = 50;
+ return Math.max(newValue, LIMIT);
+ }
+ );
+
+ this._tabMinWidth = this._tabMinWidthPref;
+
+ this._setPositionalAttributes();
+
+ CustomizableUI.addListener(this);
+ this._updateNewTabVisibility();
+ this._initializeArrowScrollbox();
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_closeTabByDblclick",
+ "browser.tabs.closeTabByDblclick",
+ false
+ );
+
+ if (gMultiProcessBrowser) {
+ this.tabbox.tabpanels.setAttribute("async", "true");
+ }
+ }
+
+ on_TabSelect(event) {
+ this._handleTabSelect();
+ }
+
+ on_TabClose(event) {
+ this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
+ }
+
+ on_TabAttrModified(event) {
+ if (
+ event.detail.changed.includes("soundplaying") &&
+ event.target.hidden
+ ) {
+ this._hiddenSoundPlayingStatusChanged(event.target);
+ }
+ }
+
+ on_TabHide(event) {
+ if (event.target.soundPlaying) {
+ this._hiddenSoundPlayingStatusChanged(event.target);
+ }
+ }
+
+ on_TabShow(event) {
+ if (event.target.soundPlaying) {
+ this._hiddenSoundPlayingStatusChanged(event.target);
+ }
+ }
+
+ on_transitionend(event) {
+ if (event.propertyName != "max-width") {
+ return;
+ }
+
+ let tab = event.target ? event.target.closest("tab") : null;
+
+ if (tab.getAttribute("fadein") == "true") {
+ if (tab._fullyOpen) {
+ this._updateCloseButtons();
+ } else {
+ this._handleNewTab(tab);
+ }
+ } else if (tab.closing) {
+ gBrowser._endRemoveTab(tab);
+ }
+
+ let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
+ tab.dispatchEvent(evt);
+ }
+
+ on_dblclick(event) {
+ // When the tabbar has an unified appearance with the titlebar
+ // and menubar, a double-click in it should have the same behavior
+ // as double-clicking the titlebar
+ if (TabsInTitlebar.enabled) {
+ return;
+ }
+
+ if (event.button != 0 || event.originalTarget.localName != "scrollbox") {
+ return;
+ }
+
+ if (!this._blockDblClick) {
+ BrowserOpenTab();
+ }
+
+ event.preventDefault();
+ }
+
+ on_click(event) {
+ if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
+ /* Catches extra clicks meant for the in-tab close button.
+ * Placed here to avoid leaking (a temporary handler added from the
+ * in-tab close button binding would close over the tab and leak it
+ * until the handler itself was removed). (bug 897751)
+ *
+ * The only sequence in which a second click event (i.e. dblclik)
+ * can be dispatched on an in-tab close button is when it is shown
+ * after the first click (i.e. the first click event was dispatched
+ * on the tab). This happens when we show the close button only on
+ * the active tab. (bug 352021)
+ * The only sequence in which a third click event can be dispatched
+ * on an in-tab close button is when the tab was opened with a
+ * double click on the tabbar. (bug 378344)
+ * In both cases, it is most likely that the close button area has
+ * been accidentally clicked, therefore we do not close the tab.
+ *
+ * We don't want to ignore processing of more than one click event,
+ * though, since the user might actually be repeatedly clicking to
+ * close many tabs at once.
+ */
+ let target = event.originalTarget;
+ if (target.classList.contains("tab-close-button")) {
+ // We preemptively set this to allow the closing-multiple-tabs-
+ // in-a-row case.
+ if (this._blockDblClick) {
+ target._ignoredCloseButtonClicks = true;
+ } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
+ target._ignoredCloseButtonClicks = true;
+ event.stopPropagation();
+ return;
+ } else {
+ // Reset the "ignored click" flag
+ target._ignoredCloseButtonClicks = false;
+ }
+ }
+
+ /* Protects from close-tab-button errant doubleclick:
+ * Since we're removing the event target, if the user
+ * double-clicks the button, the dblclick event will be dispatched
+ * with the tabbar as its event target (and explicit/originalTarget),
+ * which treats that as a mouse gesture for opening a new tab.
+ * In this context, we're manually blocking the dblclick event.
+ */
+ if (this._blockDblClick) {
+ if (!("_clickedTabBarOnce" in this)) {
+ this._clickedTabBarOnce = true;
+ return;
+ }
+ delete this._clickedTabBarOnce;
+ this._blockDblClick = false;
+ }
+ } else if (
+ event.eventPhase == Event.BUBBLING_PHASE &&
+ event.button == 1
+ ) {
+ let tab = event.target ? event.target.closest("tab") : null;
+ if (tab) {
+ gBrowser.removeTab(tab, {
+ animate: true,
+ byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
+ });
+ } else if (event.originalTarget.localName == "scrollbox") {
+ // The user middleclicked on the tabstrip. Check whether the click
+ // was dispatched on the open space of it.
+ let visibleTabs = this._getVisibleTabs();
+ let lastTab = visibleTabs[visibleTabs.length - 1];
+ let winUtils = window.windowUtils;
+ let endOfTab = winUtils.getBoundsWithoutFlushing(lastTab)[
+ RTL_UI ? "left" : "right"
+ ];
+ if (
+ (!RTL_UI && event.clientX > endOfTab) ||
+ (RTL_UI && event.clientX < endOfTab)
+ ) {
+ BrowserOpenTab();
+ }
+ } else {
+ return;
+ }
+
+ event.stopPropagation();
+ }
+ }
+
+ on_keydown(event) {
+ let { altKey, shiftKey } = event;
+ let [accel, nonAccel] =
+ AppConstants.platform == "macosx"
+ ? [event.metaKey, event.ctrlKey]
+ : [event.ctrlKey, event.metaKey];
+
+ let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
+ let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
+
+ if (!keyComboForMove && !keyComboForFocus) {
+ return;
+ }
+
+ // Don't check if the event was already consumed because tab navigation
+ // should work always for better user experience.
+ let { visibleTabs, selectedTab } = gBrowser;
+ let { arrowKeysShouldWrap } = this;
+ let focusedTabIndex = this.ariaFocusedIndex;
+ if (focusedTabIndex == -1) {
+ focusedTabIndex = visibleTabs.indexOf(selectedTab);
+ }
+ let lastFocusedTabIndex = focusedTabIndex;
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_UP:
+ if (keyComboForMove) {
+ gBrowser.moveTabBackward();
+ } else {
+ focusedTabIndex--;
+ }
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ if (keyComboForMove) {
+ gBrowser.moveTabForward();
+ } else {
+ focusedTabIndex++;
+ }
+ break;
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_LEFT:
+ if (keyComboForMove) {
+ gBrowser.moveTabOver(event);
+ } else if (
+ (!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
+ (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)
+ ) {
+ focusedTabIndex++;
+ } else {
+ focusedTabIndex--;
+ }
+ break;
+ case KeyEvent.DOM_VK_HOME:
+ if (keyComboForMove) {
+ gBrowser.moveTabToStart();
+ } else {
+ focusedTabIndex = 0;
+ }
+ break;
+ case KeyEvent.DOM_VK_END:
+ if (keyComboForMove) {
+ gBrowser.moveTabToEnd();
+ } else {
+ focusedTabIndex = visibleTabs.length - 1;
+ }
+ break;
+ case KeyEvent.DOM_VK_SPACE:
+ if (visibleTabs[lastFocusedTabIndex].multiselected) {
+ gBrowser.removeFromMultiSelectedTabs(
+ visibleTabs[lastFocusedTabIndex]
+ );
+ } else {
+ gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
+ }
+ break;
+ default:
+ // Consume the keydown event for the above keyboard
+ // shortcuts only.
+ return;
+ }
+
+ if (arrowKeysShouldWrap) {
+ if (focusedTabIndex >= visibleTabs.length) {
+ focusedTabIndex = 0;
+ } else if (focusedTabIndex < 0) {
+ focusedTabIndex = visibleTabs.length - 1;
+ }
+ } else {
+ focusedTabIndex = Math.min(
+ visibleTabs.length - 1,
+ Math.max(0, focusedTabIndex)
+ );
+ }
+
+ if (keyComboForFocus && focusedTabIndex != lastFocusedTabIndex) {
+ this.ariaFocusedItem = visibleTabs[focusedTabIndex];
+ }
+
+ event.preventDefault();
+ }
+
+ on_dragstart(event) {
+ var tab = this._getDragTargetTab(event, false);
+ if (!tab || this._isCustomizing) {
+ return;
+ }
+
+ let selectedTabs = gBrowser.selectedTabs;
+ let otherSelectedTabs = selectedTabs.filter(
+ selectedTab => selectedTab != tab
+ );
+ let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
+
+ let dt = event.dataTransfer;
+ for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
+ let dtTab = dataTransferOrderedTabs[i];
+
+ dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
+ let dtBrowser = dtTab.linkedBrowser;
+
+ // We must not set text/x-moz-url or text/plain data here,
+ // otherwise trying to detach the tab by dropping it on the desktop
+ // may result in an "internet shortcut"
+ dt.mozSetDataAt(
+ "text/x-moz-text-internal",
+ dtBrowser.currentURI.spec,
+ i
+ );
+ }
+
+ // Set the cursor to an arrow during tab drags.
+ dt.mozCursor = "default";
+
+ // Set the tab as the source of the drag, which ensures we have a stable
+ // node to deliver the `dragend` event. See bug 1345473.
+ dt.addElement(tab);
+
+ if (tab.multiselected) {
+ this._groupSelectedTabs(tab);
+ }
+
+ // Create a canvas to which we capture the current tab.
+ // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
+ // canvas size (in CSS pixels) to the window's backing resolution in order
+ // to get a full-resolution drag image for use on HiDPI displays.
+ let windowUtils = window.windowUtils;
+ let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
+ let canvas = this._dndCanvas;
+ if (!canvas) {
+ this._dndCanvas = canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.style.width = "100%";
+ canvas.style.height = "100%";
+ canvas.mozOpaque = true;
+ }
+
+ canvas.width = 160 * scale;
+ canvas.height = 90 * scale;
+ let toDrag = canvas;
+ let dragImageOffset = -16;
+ let browser = tab.linkedBrowser;
+ if (gMultiProcessBrowser) {
+ var context = canvas.getContext("2d");
+ context.fillStyle = "white";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ let captureListener;
+ let platform = AppConstants.platform;
+ // On Windows and Mac we can update the drag image during a drag
+ // using updateDragImage. On Linux, we can use a panel.
+ if (platform == "win" || platform == "macosx") {
+ captureListener = function() {
+ dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
+ };
+ } else {
+ // Create a panel to use it in setDragImage
+ // which will tell xul to render a panel that follows
+ // the pointer while a dnd session is on.
+ if (!this._dndPanel) {
+ this._dndCanvas = canvas;
+ this._dndPanel = document.createXULElement("panel");
+ this._dndPanel.className = "dragfeedback-tab";
+ this._dndPanel.setAttribute("type", "drag");
+ let wrapper = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "div"
+ );
+ wrapper.style.width = "160px";
+ wrapper.style.height = "90px";
+ wrapper.appendChild(canvas);
+ this._dndPanel.appendChild(wrapper);
+ document.documentElement.appendChild(this._dndPanel);
+ }
+ toDrag = this._dndPanel;
+ }
+ // PageThumb is async with e10s but that's fine
+ // since we can update the image during the dnd.
+ PageThumbs.captureToCanvas(browser, canvas)
+ .then(captureListener)
+ .catch(e => Cu.reportError(e));
+ } else {
+ // For the non e10s case we can just use PageThumbs
+ // sync, so let's use the canvas for setDragImage.
+ PageThumbs.captureToCanvas(browser, canvas).catch(e =>
+ Cu.reportError(e)
+ );
+ dragImageOffset = dragImageOffset * scale;
+ }
+ dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
+
+ // _dragData.offsetX/Y give the coordinates that the mouse should be
+ // positioned relative to the corner of the new window created upon
+ // dragend such that the mouse appears to have the same position
+ // relative to the corner of the dragged tab.
+ function clientX(ele) {
+ return ele.getBoundingClientRect().left;
+ }
+ let tabOffsetX = clientX(tab) - clientX(this);
+ tab._dragData = {
+ offsetX: event.screenX - window.screenX - tabOffsetX,
+ offsetY: event.screenY - window.screenY,
+ scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
+ screenX: event.screenX,
+ movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab]).filter(
+ t => t.pinned == tab.pinned
+ ),
+ };
+
+ event.stopPropagation();
+ }
+
+ on_dragover(event) {
+ var effects = this._getDropEffectForTabDrag(event);
+
+ var ind = this._tabDropIndicator;
+ if (effects == "" || effects == "none") {
+ ind.hidden = true;
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+
+ var arrowScrollbox = this.arrowScrollbox;
+
+ // autoscroll the tab strip if we drag over the scroll
+ // buttons, even if we aren't dragging a tab, but then
+ // return to avoid drawing the drop indicator
+ var pixelsToScroll = 0;
+ if (this.getAttribute("overflow") == "true") {
+ switch (event.originalTarget) {
+ case arrowScrollbox._scrollButtonUp:
+ pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
+ break;
+ case arrowScrollbox._scrollButtonDown:
+ pixelsToScroll = arrowScrollbox.scrollIncrement;
+ break;
+ }
+ if (pixelsToScroll) {
+ arrowScrollbox.scrollByPixels(
+ (RTL_UI ? -1 : 1) * pixelsToScroll,
+ true
+ );
+ }
+ }
+
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (
+ (effects == "move" || effects == "copy") &&
+ this == draggedTab.container
+ ) {
+ ind.hidden = true;
+
+ if (!this._isGroupTabsAnimationOver()) {
+ // Wait for grouping tabs animation to finish
+ return;
+ }
+ this._finishGroupSelectedTabs(draggedTab);
+
+ if (effects == "move") {
+ this._animateTabMove(event);
+ return;
+ }
+ }
+
+ this._finishAnimateTabMove();
+
+ if (effects == "link") {
+ let tab = this._getDragTargetTab(event, true);
+ if (tab) {
+ if (!this._dragTime) {
+ this._dragTime = Date.now();
+ }
+ if (Date.now() >= this._dragTime + this._dragOverDelay) {
+ this.selectedItem = tab;
+ }
+ ind.hidden = true;
+ return;
+ }
+ }
+
+ var rect = arrowScrollbox.getBoundingClientRect();
+ var newMargin;
+ if (pixelsToScroll) {
+ // if we are scrolling, put the drop indicator at the edge
+ // so that it doesn't jump while scrolling
+ let scrollRect = arrowScrollbox.scrollClientRect;
+ let minMargin = scrollRect.left - rect.left;
+ let maxMargin = Math.min(
+ minMargin + scrollRect.width,
+ scrollRect.right
+ );
+ if (RTL_UI) {
+ [minMargin, maxMargin] = [
+ this.clientWidth - maxMargin,
+ this.clientWidth - minMargin,
+ ];
+ }
+ newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
+ } else {
+ let newIndex = this._getDropIndex(event, effects == "link");
+ let children = this.allTabs;
+ if (newIndex == children.length) {
+ let tabRect = children[newIndex - 1].getBoundingClientRect();
+ if (RTL_UI) {
+ newMargin = rect.right - tabRect.left;
+ } else {
+ newMargin = tabRect.right - rect.left;
+ }
+ } else {
+ let tabRect = children[newIndex].getBoundingClientRect();
+ if (RTL_UI) {
+ newMargin = rect.right - tabRect.right;
+ } else {
+ newMargin = tabRect.left - rect.left;
+ }
+ }
+ }
+
+ ind.hidden = false;
+ newMargin += ind.clientWidth / 2;
+ if (RTL_UI) {
+ newMargin *= -1;
+ }
+ ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
+ }
+
+ on_drop(event) {
+ var dt = event.dataTransfer;
+ var dropEffect = dt.dropEffect;
+ var draggedTab;
+ let movingTabs;
+ if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
+ // tab copy or move
+ draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ // not our drop then
+ if (!draggedTab) {
+ return;
+ }
+ movingTabs = draggedTab._dragData.movingTabs;
+ draggedTab.container._finishGroupSelectedTabs(draggedTab);
+ }
+
+ this._tabDropIndicator.hidden = true;
+ event.stopPropagation();
+ if (draggedTab && dropEffect == "copy") {
+ // copy the dropped tab (wherever it's from)
+ let newIndex = this._getDropIndex(event, false);
+ let draggedTabCopy;
+ for (let tab of movingTabs) {
+ let newTab = gBrowser.duplicateTab(tab);
+ gBrowser.moveTabTo(newTab, newIndex++);
+ if (tab == draggedTab) {
+ draggedTabCopy = newTab;
+ }
+ }
+ if (draggedTab.container != this || event.shiftKey) {
+ this.selectedItem = draggedTabCopy;
+ }
+ } else if (draggedTab && draggedTab.container == this) {
+ let oldTranslateX = Math.round(draggedTab._dragData.translateX);
+ let tabWidth = Math.round(draggedTab._dragData.tabWidth);
+ let translateOffset = oldTranslateX % tabWidth;
+ let newTranslateX = oldTranslateX - translateOffset;
+ if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
+ newTranslateX += tabWidth;
+ } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
+ newTranslateX -= tabWidth;
+ }
+
+ let dropIndex =
+ "animDropIndex" in draggedTab._dragData &&
+ draggedTab._dragData.animDropIndex;
+ let incrementDropIndex = true;
+ if (dropIndex && dropIndex > movingTabs[0]._tPos) {
+ dropIndex--;
+ incrementDropIndex = false;
+ }
+
+ if (oldTranslateX && oldTranslateX != newTranslateX && !gReduceMotion) {
+ for (let tab of movingTabs) {
+ tab.setAttribute("tabdrop-samewindow", "true");
+ tab.style.transform = "translateX(" + newTranslateX + "px)";
+ let onTransitionEnd = transitionendEvent => {
+ if (
+ transitionendEvent.propertyName != "transform" ||
+ transitionendEvent.originalTarget != tab
+ ) {
+ return;
+ }
+ tab.removeEventListener("transitionend", onTransitionEnd);
+
+ tab.removeAttribute("tabdrop-samewindow");
+
+ this._finishAnimateTabMove();
+ if (dropIndex !== false) {
+ gBrowser.moveTabTo(tab, dropIndex);
+ if (incrementDropIndex) {
+ dropIndex++;
+ }
+ }
+
+ gBrowser.syncThrobberAnimations(tab);
+ };
+ tab.addEventListener("transitionend", onTransitionEnd);
+ }
+ } else {
+ this._finishAnimateTabMove();
+ if (dropIndex !== false) {
+ for (let tab of movingTabs) {
+ gBrowser.moveTabTo(tab, dropIndex);
+ if (incrementDropIndex) {
+ dropIndex++;
+ }
+ }
+ }
+ }
+ } else if (draggedTab) {
+ let newIndex = this._getDropIndex(event, false);
+ let newTabs = [];
+ for (let tab of movingTabs) {
+ let newTab = gBrowser.adoptTab(tab, newIndex++, tab == draggedTab);
+ newTabs.push(newTab);
+ }
+
+ // Restore tab selection
+ gBrowser.addRangeToMultiSelectedTabs(
+ newTabs[0],
+ newTabs[newTabs.length - 1]
+ );
+ } else {
+ // Pass true to disallow dropping javascript: or data: urls
+ let links;
+ try {
+ links = browserDragAndDrop.dropLinks(event, true);
+ } catch (ex) {}
+
+ if (!links || links.length === 0) {
+ return;
+ }
+
+ let inBackground = Services.prefs.getBoolPref(
+ "browser.tabs.loadInBackground"
+ );
+ if (event.shiftKey) {
+ inBackground = !inBackground;
+ }
+
+ let targetTab = this._getDragTargetTab(event, true);
+ let userContextId = this.selectedItem.getAttribute("usercontextid");
+ let replace = !!targetTab;
+ let newIndex = this._getDropIndex(event, true);
+ let urls = links.map(link => link.url);
+ let csp = browserDragAndDrop.getCSP(event);
+ let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(
+ event
+ );
+
+ (async () => {
+ if (
+ urls.length >=
+ Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
+ ) {
+ // Sync dialog cannot be used inside drop event handler.
+ let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
+ urls.length,
+ window
+ );
+ if (!answer) {
+ return;
+ }
+ }
+
+ gBrowser.loadTabs(urls, {
+ inBackground,
+ replace,
+ allowThirdPartyFixup: true,
+ targetTab,
+ newIndex,
+ userContextId,
+ triggeringPrincipal,
+ csp,
+ });
+ })();
+ }
+
+ if (draggedTab) {
+ delete draggedTab._dragData;
+ }
+ }
+
+ on_dragend(event) {
+ var dt = event.dataTransfer;
+ var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+
+ // Prevent this code from running if a tabdrop animation is
+ // running since calling _finishAnimateTabMove would clear
+ // any CSS transition that is running.
+ if (draggedTab.hasAttribute("tabdrop-samewindow")) {
+ return;
+ }
+
+ this._finishGroupSelectedTabs(draggedTab);
+ this._finishAnimateTabMove();
+
+ if (
+ dt.mozUserCancelled ||
+ dt.dropEffect != "none" ||
+ this._isCustomizing
+ ) {
+ delete draggedTab._dragData;
+ return;
+ }
+
+ // Check if tab detaching is enabled
+ if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) {
+ return;
+ }
+
+ // Disable detach within the browser toolbox
+ var eX = event.screenX;
+ var eY = event.screenY;
+ var wX = window.screenX;
+ // check if the drop point is horizontally within the window
+ if (eX > wX && eX < wX + window.outerWidth) {
+ // also avoid detaching if the the tab was dropped too close to
+ // the tabbar (half a tab)
+ let rect = window.windowUtils.getBoundsWithoutFlushing(
+ this.arrowScrollbox
+ );
+ let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
+ if (eY < detachTabThresholdY && eY > window.screenY) {
+ return;
+ }
+ }
+
+ // screen.availLeft et. al. only check the screen that this window is on,
+ // but we want to look at the screen the tab is being dropped onto.
+ var screen = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager)
+ .screenForRect(eX, eY, 1, 1);
+ var fullX = {},
+ fullY = {},
+ fullWidth = {},
+ fullHeight = {};
+ var availX = {},
+ availY = {},
+ availWidth = {},
+ availHeight = {};
+ // get full screen rect and available rect, both in desktop pix
+ screen.GetRectDisplayPix(fullX, fullY, fullWidth, fullHeight);
+ screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
+
+ // scale factor to convert desktop pixels to CSS px
+ var scaleFactor =
+ screen.contentsScaleFactor / screen.defaultCSSScaleFactor;
+ // synchronize CSS-px top-left coordinates with the screen's desktop-px
+ // coordinates, to ensure uniqueness across multiple screens
+ // (compare the equivalent adjustments in nsGlobalWindow::GetScreenXY()
+ // and related methods)
+ availX.value = (availX.value - fullX.value) * scaleFactor + fullX.value;
+ availY.value = (availY.value - fullY.value) * scaleFactor + fullY.value;
+ availWidth.value *= scaleFactor;
+ availHeight.value *= scaleFactor;
+
+ // ensure new window entirely within screen
+ var winWidth = Math.min(window.outerWidth, availWidth.value);
+ var winHeight = Math.min(window.outerHeight, availHeight.value);
+ var left = Math.min(
+ Math.max(eX - draggedTab._dragData.offsetX, availX.value),
+ availX.value + availWidth.value - winWidth
+ );
+ var top = Math.min(
+ Math.max(eY - draggedTab._dragData.offsetY, availY.value),
+ availY.value + availHeight.value - winHeight
+ );
+
+ delete draggedTab._dragData;
+
+ if (gBrowser.tabs.length == 1) {
+ // resize _before_ move to ensure the window fits the new screen. if
+ // the window is too large for its screen, the window manager may do
+ // automatic repositioning.
+ window.resizeTo(winWidth, winHeight);
+ window.moveTo(left, top);
+ window.focus();
+ } else {
+ let props = { screenX: left, screenY: top, suppressanimation: 1 };
+ if (AppConstants.platform != "win") {
+ props.outerWidth = winWidth;
+ props.outerHeight = winHeight;
+ }
+ gBrowser.replaceTabsWithWindow(draggedTab, props);
+ }
+ event.stopPropagation();
+ }
+
+ on_dragexit(event) {
+ this._dragTime = 0;
+
+ // This does not work at all (see bug 458613)
+ var target = event.relatedTarget;
+ while (target && target != this) {
+ target = target.parentNode;
+ }
+ if (target) {
+ return;
+ }
+
+ this._tabDropIndicator.hidden = true;
+ event.stopPropagation();
+ }
+
+ get tabbox() {
+ return document.getElementById("tabbrowser-tabbox");
+ }
+
+ get newTabButton() {
+ return this.querySelector("#tabs-newtab-button");
+ }
+
+ // Accessor for tabs. arrowScrollbox has two non-tab elements at the
+ // end, everything else is <tab>s
+ get allTabs() {
+ let children = Array.from(this.arrowScrollbox.children);
+ children.pop();
+ children.pop();
+ return children;
+ }
+
+ appendChild(tab) {
+ return this.insertBefore(tab, null);
+ }
+
+ insertBefore(tab, node) {
+ if (!this.arrowScrollbox) {
+ throw new Error("Shouldn't call this without arrowscrollbox");
+ }
+
+ let { arrowScrollbox } = this;
+ if (node == null) {
+ // we have a toolbarbutton and a space at the end of the scrollbox
+ node = arrowScrollbox.lastChild.previousSibling;
+ }
+ return arrowScrollbox.insertBefore(tab, node);
+ }
+
+ set _tabMinWidth(val) {
+ this.style.setProperty("--tab-min-width", val + "px");
+ return val;
+ }
+
+ get _isCustomizing() {
+ return document.documentElement.getAttribute("customizing") == "true";
+ }
+
+ // This overrides the TabsBase _selectNewTab method so that we can
+ // potentially interrupt keyboard tab switching when sharing the
+ // window or screen.
+ _selectNewTab(aNewTab, aFallbackDir, aWrap) {
+ if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
+ super._selectNewTab(aNewTab, aFallbackDir, aWrap);
+ }
+ }
+
+ _initializeArrowScrollbox() {
+ let arrowScrollbox = this.arrowScrollbox;
+ arrowScrollbox.shadowRoot.addEventListener(
+ "underflow",
+ event => {
+ // Ignore underflow events:
+ // - from nested scrollable elements
+ // - for vertical orientation
+ // - corresponding to an overflow event that we ignored
+ if (
+ event.originalTarget != arrowScrollbox.scrollbox ||
+ event.detail == 0 ||
+ !this.hasAttribute("overflow")
+ ) {
+ return;
+ }
+
+ this.removeAttribute("overflow");
+
+ if (this._lastTabClosedByMouse) {
+ this._expandSpacerBy(this._scrollButtonWidth);
+ }
+
+ for (let tab of Array.from(gBrowser._removingTabs)) {
+ gBrowser.removeTab(tab);
+ }
+
+ this._positionPinnedTabs();
+ },
+ true
+ );
+
+ arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
+ // Ignore overflow events:
+ // - from nested scrollable elements
+ // - for vertical orientation
+ if (
+ event.originalTarget != arrowScrollbox.scrollbox ||
+ event.detail == 0
+ ) {
+ return;
+ }
+
+ this.setAttribute("overflow", "true");
+ this._positionPinnedTabs();
+ this._handleTabSelect(true);
+ });
+
+ // Override arrowscrollbox.js method, since our scrollbox's children are
+ // inherited from the scrollbox binding parent (this).
+ arrowScrollbox._getScrollableElements = () => {
+ return this.allTabs.filter(arrowScrollbox._canScrollToElement);
+ };
+ arrowScrollbox._canScrollToElement = tab => {
+ return !tab._pinnedUnscrollable && !tab.hidden;
+ };
+ }
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ // This is has to deal with changes in
+ // privacy.userContext.enabled and
+ // privacy.userContext.newTabContainerOnLeftClick.enabled.
+ let containersEnabled =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ !PrivateBrowsingUtils.isWindowPrivate(window);
+
+ // This pref won't change so often, so just recreate the menu.
+ const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
+ "privacy.userContext.newTabContainerOnLeftClick.enabled"
+ );
+
+ // There are separate "new tab" buttons for when the tab strip
+ // is overflowed and when it is not. Attach the long click
+ // popup to both of them.
+ const newTab = document.getElementById("new-tab-button");
+ const newTab2 = this.newTabButton;
+
+ for (let parent of [newTab, newTab2]) {
+ if (!parent) {
+ continue;
+ }
+
+ parent.removeAttribute("type");
+ if (parent.menupopup) {
+ parent.menupopup.remove();
+ }
+
+ if (containersEnabled) {
+ parent.setAttribute("context", "new-tab-button-popup");
+
+ let popup = document
+ .getElementById("new-tab-button-popup")
+ .cloneNode(true);
+ popup.removeAttribute("id");
+ popup.className = "new-tab-popup";
+ popup.setAttribute("position", "after_end");
+ parent.prepend(popup);
+ parent.setAttribute("type", "menu");
+ if (newTabLeftClickOpensContainersMenu) {
+ gClickAndHoldListenersOnElement.remove(parent);
+ // Update tooltip text
+ nodeToTooltipMap[parent.id] = "newTabAlwaysContainer.tooltip";
+ } else {
+ gClickAndHoldListenersOnElement.add(parent);
+ nodeToTooltipMap[parent.id] = "newTabContainer.tooltip";
+ }
+ } else {
+ nodeToTooltipMap[parent.id] = "newTabButton.tooltip";
+ parent.removeAttribute("context", "new-tab-button-popup");
+ }
+
+ // evict from tooltip cache
+ gDynamicTooltipCache.delete(parent.id);
+ }
+
+ break;
+ }
+ }
+
+ _getVisibleTabs() {
+ // Cannot access gBrowser before it's initialized.
+ if (!gBrowser) {
+ return this.allTabs[0];
+ }
+
+ return gBrowser.visibleTabs;
+ }
+
+ _setPositionalAttributes() {
+ let visibleTabs = this._getVisibleTabs();
+ if (!visibleTabs.length) {
+ return;
+ }
+ let selectedTab = this.selectedItem;
+ let selectedIndex = visibleTabs.indexOf(selectedTab);
+ if (this._beforeSelectedTab) {
+ this._beforeSelectedTab.removeAttribute("beforeselected-visible");
+ }
+
+ if (selectedTab.closing || selectedIndex <= 0) {
+ this._beforeSelectedTab = null;
+ } else {
+ let beforeSelectedTab = visibleTabs[selectedIndex - 1];
+ let separatedByScrollButton =
+ this.getAttribute("overflow") == "true" &&
+ beforeSelectedTab.pinned &&
+ !selectedTab.pinned;
+ if (!separatedByScrollButton) {
+ this._beforeSelectedTab = beforeSelectedTab;
+ this._beforeSelectedTab.setAttribute(
+ "beforeselected-visible",
+ "true"
+ );
+ }
+ }
+
+ if (this._firstTab) {
+ this._firstTab.removeAttribute("first-visible-tab");
+ }
+ this._firstTab = visibleTabs[0];
+ this._firstTab.setAttribute("first-visible-tab", "true");
+ if (this._lastTab) {
+ this._lastTab.removeAttribute("last-visible-tab");
+ }
+ this._lastTab = visibleTabs[visibleTabs.length - 1];
+ this._lastTab.setAttribute("last-visible-tab", "true");
+
+ let hoveredTab = this._hoveredTab;
+ if (hoveredTab) {
+ hoveredTab._mouseleave();
+ }
+ hoveredTab = this.querySelector("tab:hover");
+ if (hoveredTab) {
+ hoveredTab._mouseenter();
+ }
+
+ // Update before-multiselected attributes.
+ // gBrowser may not be initialized yet, so avoid using it
+ for (let i = 0; i < visibleTabs.length - 1; i++) {
+ let tab = visibleTabs[i];
+ let nextTab = visibleTabs[i + 1];
+ tab.removeAttribute("before-multiselected");
+ if (nextTab.multiselected) {
+ tab.setAttribute("before-multiselected", "true");
+ }
+ }
+ }
+
+ _updateCloseButtons() {
+ // If we're overflowing, tabs are at their minimum widths.
+ if (this.getAttribute("overflow") == "true") {
+ this.setAttribute("closebuttons", "activetab");
+ return;
+ }
+
+ if (this._closeButtonsUpdatePending) {
+ return;
+ }
+ this._closeButtonsUpdatePending = true;
+
+ // Wait until after the next paint to get current layout data from
+ // getBoundsWithoutFlushing.
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ this._closeButtonsUpdatePending = false;
+
+ // The scrollbox may have started overflowing since we checked
+ // overflow earlier, so check again.
+ if (this.getAttribute("overflow") == "true") {
+ this.setAttribute("closebuttons", "activetab");
+ return;
+ }
+
+ // Check if tab widths are below the threshold where we want to
+ // remove close buttons from background tabs so that people don't
+ // accidentally close tabs by selecting them.
+ let rect = ele => {
+ return window.windowUtils.getBoundsWithoutFlushing(ele);
+ };
+ let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
+ if (tab && rect(tab).width <= this._tabClipWidth) {
+ this.setAttribute("closebuttons", "activetab");
+ } else {
+ this.removeAttribute("closebuttons");
+ }
+ });
+ });
+ }
+
+ _updateHiddenTabsStatus() {
+ if (gBrowser.visibleTabs.length < gBrowser.tabs.length) {
+ this.setAttribute("hashiddentabs", "true");
+ } else {
+ this.removeAttribute("hashiddentabs");
+ }
+ }
+
+ _handleTabSelect(aInstant) {
+ let selectedTab = this.selectedItem;
+ if (this.getAttribute("overflow") == "true") {
+ this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
+ }
+
+ selectedTab._notselectedsinceload = false;
+ }
+
+ /**
+ * Try to keep the active tab's close button under the mouse cursor
+ */
+ _lockTabSizing(aTab, aTabWidth) {
+ let tabs = this._getVisibleTabs();
+ if (!tabs.length) {
+ return;
+ }
+
+ var isEndTab = aTab._tPos > tabs[tabs.length - 1]._tPos;
+
+ if (!this._tabDefaultMaxWidth) {
+ this._tabDefaultMaxWidth = parseFloat(
+ window.getComputedStyle(aTab).maxWidth
+ );
+ }
+ this._lastTabClosedByMouse = true;
+ this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
+ this.arrowScrollbox._scrollButtonDown
+ ).width;
+
+ if (this.getAttribute("overflow") == "true") {
+ // Don't need to do anything if we're in overflow mode and aren't scrolled
+ // all the way to the right, or if we're closing the last tab.
+ if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
+ return;
+ }
+ // If the tab has an owner that will become the active tab, the owner will
+ // be to the left of it, so we actually want the left tab to slide over.
+ // This can't be done as easily in non-overflow mode, so we don't bother.
+ if (aTab.owner) {
+ return;
+ }
+ this._expandSpacerBy(aTabWidth);
+ } else {
+ // non-overflow mode
+ // Locking is neither in effect nor needed, so let tabs expand normally.
+ if (isEndTab && !this._hasTabTempMaxWidth) {
+ return;
+ }
+ let numPinned = gBrowser._numPinnedTabs;
+ // Force tabs to stay the same width, unless we're closing the last tab,
+ // which case we need to let them expand just enough so that the overall
+ // tabbar width is the same.
+ if (isEndTab) {
+ let numNormalTabs = tabs.length - numPinned;
+ aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
+ if (aTabWidth > this._tabDefaultMaxWidth) {
+ aTabWidth = this._tabDefaultMaxWidth;
+ }
+ }
+ aTabWidth += "px";
+ let tabsToReset = [];
+ for (let i = numPinned; i < tabs.length; i++) {
+ let tab = tabs[i];
+ tab.style.setProperty("max-width", aTabWidth, "important");
+ if (!isEndTab) {
+ // keep tabs the same width
+ tab.style.transition = "none";
+ tabsToReset.push(tab);
+ }
+ }
+
+ if (tabsToReset.length) {
+ window
+ .promiseDocumentFlushed(() => {})
+ .then(() => {
+ window.requestAnimationFrame(() => {
+ for (let tab of tabsToReset) {
+ tab.style.transition = "";
+ }
+ });
+ });
+ }
+
+ this._hasTabTempMaxWidth = true;
+ gBrowser.addEventListener("mousemove", this);
+ window.addEventListener("mouseout", this);
+ }
+ }
+
+ _expandSpacerBy(pixels) {
+ let spacer = this._closingTabsSpacer;
+ spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
+ this.setAttribute("using-closing-tabs-spacer", "true");
+ gBrowser.addEventListener("mousemove", this);
+ window.addEventListener("mouseout", this);
+ }
+
+ _unlockTabSizing() {
+ gBrowser.removeEventListener("mousemove", this);
+ window.removeEventListener("mouseout", this);
+
+ if (this._hasTabTempMaxWidth) {
+ this._hasTabTempMaxWidth = false;
+ let tabs = this._getVisibleTabs();
+ for (let i = 0; i < tabs.length; i++) {
+ tabs[i].style.maxWidth = "";
+ }
+ }
+
+ if (this.hasAttribute("using-closing-tabs-spacer")) {
+ this.removeAttribute("using-closing-tabs-spacer");
+ this._closingTabsSpacer.style.width = 0;
+ }
+ }
+
+ uiDensityChanged() {
+ this._positionPinnedTabs();
+ this._updateCloseButtons();
+ this._handleTabSelect(true);
+ }
+
+ _positionPinnedTabs() {
+ let numPinned = gBrowser._numPinnedTabs;
+ let doPosition =
+ this.getAttribute("overflow") == "true" &&
+ this._getVisibleTabs().length > numPinned &&
+ numPinned > 0;
+ let tabs = this.allTabs;
+
+ if (doPosition) {
+ this.setAttribute("positionpinnedtabs", "true");
+
+ let layoutData = this._pinnedTabsLayoutCache;
+ let uiDensity = document.documentElement.getAttribute("uidensity");
+ if (!layoutData || layoutData.uiDensity != uiDensity) {
+ let arrowScrollbox = this.arrowScrollbox;
+ layoutData = this._pinnedTabsLayoutCache = {
+ uiDensity,
+ pinnedTabWidth: this.allTabs[0].getBoundingClientRect().width,
+ scrollButtonWidth: arrowScrollbox._scrollButtonDown.getBoundingClientRect()
+ .width,
+ };
+ }
+
+ let width = 0;
+ for (let i = numPinned - 1; i >= 0; i--) {
+ let tab = tabs[i];
+ width += layoutData.pinnedTabWidth;
+ tab.style.setProperty(
+ "margin-inline-start",
+ -(width + layoutData.scrollButtonWidth) + "px",
+ "important"
+ );
+ tab._pinnedUnscrollable = true;
+ }
+ this.style.paddingInlineStart = width + "px";
+ } else {
+ this.removeAttribute("positionpinnedtabs");
+
+ for (let i = 0; i < numPinned; i++) {
+ let tab = tabs[i];
+ tab.style.marginInlineStart = "";
+ tab._pinnedUnscrollable = false;
+ }
+
+ this.style.paddingInlineStart = "";
+ }
+
+ if (this._lastNumPinned != numPinned) {
+ this._lastNumPinned = numPinned;
+ this._handleTabSelect(true);
+ }
+ }
+
+ _animateTabMove(event) {
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ let movingTabs = draggedTab._dragData.movingTabs;
+
+ if (this.getAttribute("movingtab") != "true") {
+ this.setAttribute("movingtab", "true");
+ gNavToolbox.setAttribute("movingtab", "true");
+ if (!draggedTab.multiselected) {
+ this.selectedItem = draggedTab;
+ }
+ }
+
+ if (!("animLastScreenX" in draggedTab._dragData)) {
+ draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
+ }
+
+ let screenX = event.screenX;
+ if (screenX == draggedTab._dragData.animLastScreenX) {
+ return;
+ }
+
+ // Direction of the mouse movement.
+ let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
+
+ draggedTab._dragData.animLastScreenX = screenX;
+
+ let pinned = draggedTab.pinned;
+ let numPinned = gBrowser._numPinnedTabs;
+ let tabs = this._getVisibleTabs().slice(
+ pinned ? 0 : numPinned,
+ pinned ? numPinned : undefined
+ );
+
+ if (RTL_UI) {
+ tabs.reverse();
+ // Copy moving tabs array to avoid infinite reversing.
+ movingTabs = [...movingTabs].reverse();
+ }
+ let tabWidth = draggedTab.getBoundingClientRect().width;
+ let shiftWidth = tabWidth * movingTabs.length;
+ draggedTab._dragData.tabWidth = tabWidth;
+
+ // Move the dragged tab based on the mouse position.
+
+ let leftTab = tabs[0];
+ let rightTab = tabs[tabs.length - 1];
+ let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
+ let leftMovingTabScreenX = movingTabs[0].screenX;
+ let translateX = screenX - draggedTab._dragData.screenX;
+ if (!pinned) {
+ translateX +=
+ this.arrowScrollbox.scrollbox.scrollLeft -
+ draggedTab._dragData.scrollX;
+ }
+ let leftBound = leftTab.screenX - leftMovingTabScreenX;
+ let rightBound =
+ rightTab.screenX +
+ rightTab.getBoundingClientRect().width -
+ (rightMovingTabScreenX + tabWidth);
+ translateX = Math.min(Math.max(translateX, leftBound), rightBound);
+
+ for (let tab of movingTabs) {
+ tab.style.transform = "translateX(" + translateX + "px)";
+ }
+
+ draggedTab._dragData.translateX = translateX;
+
+ // Determine what tab we're dragging over.
+ // * Single tab dragging: Point of reference is the center of the dragged tab. If that
+ // point touches a background tab, the dragged tab would take that
+ // tab's position when dropped.
+ // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
+ // points of reference (center of tabs on the extremities). When
+ // mouse is moving from left to right, the right reference gets activated,
+ // otherwise the left reference will be used. Everything else works the same
+ // as single tab dragging.
+ // * We're doing a binary search in order to reduce the amount of
+ // tabs we need to check.
+
+ tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
+ let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
+ let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
+ let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
+ let newIndex = -1;
+ let oldIndex =
+ "animDropIndex" in draggedTab._dragData
+ ? draggedTab._dragData.animDropIndex
+ : movingTabs[0]._tPos;
+ let low = 0;
+ let high = tabs.length - 1;
+ while (low <= high) {
+ let mid = Math.floor((low + high) / 2);
+ if (tabs[mid] == draggedTab && ++mid > high) {
+ break;
+ }
+ screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
+ if (screenX > tabCenter) {
+ high = mid - 1;
+ } else if (
+ screenX + tabs[mid].getBoundingClientRect().width <
+ tabCenter
+ ) {
+ low = mid + 1;
+ } else {
+ newIndex = tabs[mid]._tPos;
+ break;
+ }
+ }
+ if (newIndex >= oldIndex) {
+ newIndex++;
+ }
+ if (newIndex < 0 || newIndex == oldIndex) {
+ return;
+ }
+ draggedTab._dragData.animDropIndex = newIndex;
+
+ // Shift background tabs to leave a gap where the dragged tab
+ // would currently be dropped.
+
+ for (let tab of tabs) {
+ if (tab != draggedTab) {
+ let shift = getTabShift(tab, newIndex);
+ tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
+ }
+ }
+
+ function getTabShift(tab, dropIndex) {
+ if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) {
+ return RTL_UI ? -shiftWidth : shiftWidth;
+ }
+ if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) {
+ return RTL_UI ? shiftWidth : -shiftWidth;
+ }
+ return 0;
+ }
+ }
+
+ _finishAnimateTabMove() {
+ if (this.getAttribute("movingtab") != "true") {
+ return;
+ }
+
+ for (let tab of this._getVisibleTabs()) {
+ tab.style.transform = "";
+ }
+
+ this.removeAttribute("movingtab");
+ gNavToolbox.removeAttribute("movingtab");
+
+ this._handleTabSelect();
+ }
+
+ /**
+ * Regroup all selected tabs around the
+ * tab in param
+ */
+ _groupSelectedTabs(tab) {
+ let draggedTabPos = tab._tPos;
+ let selectedTabs = gBrowser.selectedTabs;
+ let animate = !gReduceMotion;
+
+ tab.groupingTabsData = {
+ finished: !animate,
+ };
+
+ // Animate left selected tabs
+
+ let insertAtPos = draggedTabPos - 1;
+ for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
+ let movingTab = selectedTabs[i];
+ insertAtPos = newIndex(movingTab, insertAtPos);
+
+ if (animate) {
+ movingTab.groupingTabsData = {};
+ addAnimationData(movingTab, insertAtPos, "left");
+ } else {
+ gBrowser.moveTabTo(movingTab, insertAtPos);
+ }
+ insertAtPos--;
+ }
+
+ // Animate right selected tabs
+
+ insertAtPos = draggedTabPos + 1;
+ for (
+ let i = selectedTabs.indexOf(tab) + 1;
+ i < selectedTabs.length;
+ i++
+ ) {
+ let movingTab = selectedTabs[i];
+ insertAtPos = newIndex(movingTab, insertAtPos);
+
+ if (animate) {
+ movingTab.groupingTabsData = {};
+ addAnimationData(movingTab, insertAtPos, "right");
+ } else {
+ gBrowser.moveTabTo(movingTab, insertAtPos);
+ }
+ insertAtPos++;
+ }
+
+ // Slide the relevant tabs to their new position.
+ for (let t of this._getVisibleTabs()) {
+ if (t.groupingTabsData && t.groupingTabsData.translateX) {
+ let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
+ t.style.transform = "translateX(" + translateX + "px)";
+ }
+ }
+
+ function newIndex(aTab, index) {
+ // Don't allow mixing pinned and unpinned tabs.
+ if (aTab.pinned) {
+ return Math.min(index, gBrowser._numPinnedTabs - 1);
+ }
+ return Math.max(index, gBrowser._numPinnedTabs);
+ }
+
+ function addAnimationData(movingTab, movingTabNewIndex, side) {
+ let movingTabOldIndex = movingTab._tPos;
+
+ if (movingTabOldIndex == movingTabNewIndex) {
+ // movingTab is already at the right position
+ // and thus don't need to be animated.
+ return;
+ }
+
+ let movingTabWidth = movingTab.getBoundingClientRect().width;
+ let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
+
+ movingTab.groupingTabsData.animate = true;
+ movingTab.setAttribute("tab-grouping", "true");
+
+ movingTab.groupingTabsData.translateX = shift;
+
+ let onTransitionEnd = transitionendEvent => {
+ if (
+ transitionendEvent.propertyName != "transform" ||
+ transitionendEvent.originalTarget != movingTab
+ ) {
+ return;
+ }
+ movingTab.removeEventListener("transitionend", onTransitionEnd);
+ movingTab.groupingTabsData.newIndex = movingTabNewIndex;
+ movingTab.groupingTabsData.animate = false;
+ };
+
+ movingTab.addEventListener("transitionend", onTransitionEnd);
+
+ // Add animation data for tabs between movingTab (selected
+ // tab moving towards the dragged tab) and draggedTab.
+ // Those tabs in the middle should move in
+ // the opposite direction of movingTab.
+
+ let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
+ let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
+
+ for (let i = lowerIndex + 1; i < higherIndex; i++) {
+ let middleTab = gBrowser.visibleTabs[i];
+
+ if (middleTab.pinned != movingTab.pinned) {
+ // Don't mix pinned and unpinned tabs
+ break;
+ }
+
+ if (middleTab.multiselected) {
+ // Skip because this selected tab should
+ // be shifted towards the dragged Tab.
+ continue;
+ }
+
+ if (
+ !middleTab.groupingTabsData ||
+ !middleTab.groupingTabsData.translateX
+ ) {
+ middleTab.groupingTabsData = { translateX: 0 };
+ }
+ if (side == "left") {
+ middleTab.groupingTabsData.translateX -= movingTabWidth;
+ } else {
+ middleTab.groupingTabsData.translateX += movingTabWidth;
+ }
+
+ middleTab.setAttribute("tab-grouping", "true");
+ }
+ }
+ }
+
+ _finishGroupSelectedTabs(tab) {
+ if (!tab.groupingTabsData || tab.groupingTabsData.finished) {
+ return;
+ }
+
+ tab.groupingTabsData.finished = true;
+
+ let selectedTabs = gBrowser.selectedTabs;
+ let tabIndex = selectedTabs.indexOf(tab);
+
+ // Moving left tabs
+ for (let i = tabIndex - 1; i > -1; i--) {
+ let movingTab = selectedTabs[i];
+ if (movingTab.groupingTabsData.newIndex) {
+ gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
+ }
+ }
+
+ // Moving right tabs
+ for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
+ let movingTab = selectedTabs[i];
+ if (movingTab.groupingTabsData.newIndex) {
+ gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
+ }
+ }
+
+ for (let t of this._getVisibleTabs()) {
+ t.style.transform = "";
+ t.removeAttribute("tab-grouping");
+ delete t.groupingTabsData;
+ }
+ }
+
+ _isGroupTabsAnimationOver() {
+ for (let tab of gBrowser.selectedTabs) {
+ if (tab.groupingTabsData && tab.groupingTabsData.animate) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "resize":
+ if (aEvent.target != window) {
+ break;
+ }
+
+ this._updateCloseButtons();
+ this._handleTabSelect(true);
+ break;
+ case "mouseout":
+ // If the "related target" (the node to which the pointer went) is not
+ // a child of the current document, the mouse just left the window.
+ let relatedTarget = aEvent.relatedTarget;
+ if (relatedTarget && relatedTarget.ownerDocument == document) {
+ break;
+ }
+ // fall through
+ case "mousemove":
+ if (document.getElementById("tabContextMenu").state != "open") {
+ this._unlockTabSizing();
+ }
+ break;
+ default:
+ let methodName = `on_${aEvent.type}`;
+ if (methodName in this) {
+ this[methodName](aEvent);
+ } else {
+ throw new Error(`Unexpected event ${aEvent.type}`);
+ }
+ }
+ }
+
+ _notifyBackgroundTab(aTab) {
+ if (
+ aTab.pinned ||
+ aTab.hidden ||
+ this.getAttribute("overflow") != "true"
+ ) {
+ return;
+ }
+
+ this._lastTabToScrollIntoView = aTab;
+ if (!this._backgroundTabScrollPromise) {
+ this._backgroundTabScrollPromise = window
+ .promiseDocumentFlushed(() => {
+ let lastTabRect = this._lastTabToScrollIntoView.getBoundingClientRect();
+ let selectedTab = this.selectedItem;
+ if (selectedTab.pinned) {
+ selectedTab = null;
+ } else {
+ selectedTab = selectedTab.getBoundingClientRect();
+ selectedTab = {
+ left: selectedTab.left,
+ right: selectedTab.right,
+ };
+ }
+ return [
+ this._lastTabToScrollIntoView,
+ this.arrowScrollbox.scrollClientRect,
+ { left: lastTabRect.left, right: lastTabRect.right },
+ selectedTab,
+ ];
+ })
+ .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
+ // First off, remove the promise so we can re-enter if necessary.
+ delete this._backgroundTabScrollPromise;
+ // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
+ // the code above to get layout info for *that* tab, and don't do
+ // anything here, as we really just want to run this for the last-opened tab.
+ if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
+ this._notifyBackgroundTab(this._lastTabToScrollIntoView);
+ return;
+ }
+ delete this._lastTabToScrollIntoView;
+ // Is the new tab already completely visible?
+ if (
+ scrollRect.left <= tabRect.left &&
+ tabRect.right <= scrollRect.right
+ ) {
+ return;
+ }
+
+ if (this.arrowScrollbox.smoothScroll) {
+ // Can we make both the new tab and the selected tab completely visible?
+ if (
+ !selectedRect ||
+ Math.max(
+ tabRect.right - selectedRect.left,
+ selectedRect.right - tabRect.left
+ ) <= scrollRect.width
+ ) {
+ this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
+ return;
+ }
+
+ this.arrowScrollbox.scrollByPixels(
+ RTL_UI
+ ? selectedRect.right - scrollRect.right
+ : selectedRect.left - scrollRect.left
+ );
+ }
+
+ if (!this._animateElement.hasAttribute("highlight")) {
+ this._animateElement.setAttribute("highlight", "true");
+ setTimeout(
+ function(ele) {
+ ele.removeAttribute("highlight");
+ },
+ 150,
+ this._animateElement
+ );
+ }
+ });
+ }
+ }
+
+ _getDragTargetTab(event, isLink) {
+ let tab = event.target;
+ while (tab && tab.localName != "tab") {
+ tab = tab.parentNode;
+ }
+ if (tab && isLink) {
+ let { width } = tab.getBoundingClientRect();
+ if (
+ event.screenX < tab.screenX + width * 0.25 ||
+ event.screenX > tab.screenX + width * 0.75
+ ) {
+ return null;
+ }
+ }
+ return tab;
+ }
+
+ _getDropIndex(event, isLink) {
+ var tabs = this.allTabs;
+ var tab = this._getDragTargetTab(event, isLink);
+ if (!RTL_UI) {
+ for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) {
+ if (
+ event.screenX <
+ tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2
+ ) {
+ return i;
+ }
+ }
+ } else {
+ for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) {
+ if (
+ event.screenX >
+ tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2
+ ) {
+ return i;
+ }
+ }
+ }
+ return tabs.length;
+ }
+
+ _getDropEffectForTabDrag(event) {
+ var dt = event.dataTransfer;
+
+ let isMovingTabs = dt.mozItemCount > 0;
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ // tabs are always added as the first type
+ let types = dt.mozTypesAt(0);
+ if (types[0] != TAB_DROP_TYPE) {
+ isMovingTabs = false;
+ break;
+ }
+ }
+
+ if (isMovingTabs) {
+ let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (
+ sourceNode instanceof XULElement &&
+ sourceNode.localName == "tab" &&
+ sourceNode.ownerGlobal.isChromeWindow &&
+ sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
+ "navigator:browser" &&
+ sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
+ ) {
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (
+ PrivateBrowsingUtils.isWindowPrivate(window) !=
+ PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
+ ) {
+ return "none";
+ }
+
+ if (
+ window.gMultiProcessBrowser !=
+ sourceNode.ownerGlobal.gMultiProcessBrowser
+ ) {
+ return "none";
+ }
+
+ if (
+ window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
+ ) {
+ return "none";
+ }
+
+ return dt.dropEffect == "copy" ? "copy" : "move";
+ }
+ }
+
+ if (browserDragAndDrop.canDropLink(event)) {
+ return "link";
+ }
+ return "none";
+ }
+
+ _handleNewTab(tab) {
+ if (tab.container != this) {
+ return;
+ }
+ tab._fullyOpen = true;
+ gBrowser.tabAnimationsInProgress--;
+
+ this._updateCloseButtons();
+
+ if (tab.getAttribute("selected") == "true") {
+ this._handleTabSelect();
+ } else if (!tab.hasAttribute("skipbackgroundnotify")) {
+ this._notifyBackgroundTab(tab);
+ }
+
+ // XXXmano: this is a temporary workaround for bug 345399
+ // We need to manually update the scroll buttons disabled state
+ // if a tab was inserted to the overflow area or removed from it
+ // without any scrolling and when the tabbar has already
+ // overflowed.
+ this.arrowScrollbox._updateScrollButtonsDisabledState();
+
+ // If this browser isn't lazy (indicating it's probably created by
+ // session restore), preload the next about:newtab if we don't
+ // already have a preloaded browser.
+ if (tab.linkedPanel) {
+ NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
+ }
+
+ if (UserInteraction.running("browser.tabs.opening", window)) {
+ UserInteraction.finish("browser.tabs.opening", window);
+ }
+ }
+
+ _canAdvanceToTab(aTab) {
+ return !aTab.closing;
+ }
+
+ getRelatedElement(aTab) {
+ if (!aTab) {
+ return null;
+ }
+
+ // Cannot access gBrowser before it's initialized.
+ if (!gBrowser._initialized) {
+ return this.tabbox.tabpanels.firstElementChild;
+ }
+
+ // If the tab's browser is lazy, we need to `_insertBrowser` in order
+ // to have a linkedPanel. This will also serve to bind the browser
+ // and make it ready to use when the tab is selected.
+ gBrowser._insertBrowser(aTab);
+ return document.getElementById(aTab.linkedPanel);
+ }
+
+ _updateNewTabVisibility() {
+ // Helper functions to help deal with customize mode wrapping some items
+ let wrap = n =>
+ n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
+ let unwrap = n =>
+ n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
+
+ // Starting from the tabs element, find the next sibling that:
+ // - isn't hidden; and
+ // - isn't the all-tabs button.
+ // If it's the new tab button, consider the new tab button adjacent to the tabs.
+ // If the new tab button is marked as adjacent and the tabstrip doesn't
+ // overflow, we'll display the 'new tab' button inline in the tabstrip.
+ // In all other cases, the separate new tab button is displayed in its
+ // customized location.
+ let sib = this;
+ do {
+ sib = unwrap(wrap(sib).nextElementSibling);
+ } while (sib && (sib.hidden || sib.id == "alltabs-button"));
+
+ const kAttr = "hasadjacentnewtabbutton";
+ if (sib && sib.id == "new-tab-button") {
+ this.setAttribute(kAttr, "true");
+ } else {
+ this.removeAttribute(kAttr);
+ }
+ }
+
+ onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
+ if (
+ aContainer.ownerDocument == document &&
+ aContainer.id == "TabsToolbar-customization-target"
+ ) {
+ this._updateNewTabVisibility();
+ }
+ }
+
+ onAreaNodeRegistered(aArea, aContainer) {
+ if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
+ this._updateNewTabVisibility();
+ }
+ }
+
+ onAreaReset(aArea, aContainer) {
+ this.onAreaNodeRegistered(aArea, aContainer);
+ }
+
+ _hiddenSoundPlayingStatusChanged(tab, opts) {
+ let closed = opts && opts.closed;
+ if (!closed && tab.soundPlaying && tab.hidden) {
+ this._hiddenSoundPlayingTabs.add(tab);
+ this.setAttribute("hiddensoundplaying", "true");
+ } else {
+ this._hiddenSoundPlayingTabs.delete(tab);
+ if (this._hiddenSoundPlayingTabs.size == 0) {
+ this.removeAttribute("hiddensoundplaying");
+ }
+ }
+ }
+
+ destroy() {
+ if (this.boundObserve) {
+ Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
+ }
+
+ CustomizableUI.removeListener(this);
+ }
+ }
+
+ customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {
+ extends: "tabs",
+ });
+}
diff --git a/browser/base/content/tabbrowser.css b/browser/base/content/tabbrowser.css
new file mode 100644
index 0000000000..a0feb36906
--- /dev/null
+++ b/browser/base/content/tabbrowser.css
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.tab-close-button[pinned],
+#tabbrowser-tabs[closebuttons="activetab"] > #tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-content > .tab-close-button:not([selected="true"]),
+.tab-icon-pending:not([pendingicon]),
+.tab-icon-pending[busy],
+.tab-icon-pending[pinned],
+.tab-icon-image:not([src], [pinned], [crashed], [pictureinpicture])[selected],
+.tab-icon-image:not([src], [pinned], [crashed], [sharing], [pictureinpicture]),
+.tab-icon-image[busy],
+.tab-throbber:not([busy]),
+.tab-icon-sound:not([soundplaying], [muted], [activemedia-blocked], [pictureinpicture]),
+.tab-icon-sound[pinned],
+.tab-sharing-icon-overlay,
+.tab-icon-overlay {
+ display: none;
+}
+
+.tab-sharing-icon-overlay[sharing]:not([selected]),
+.tab-icon-overlay[soundplaying][pinned],
+.tab-icon-overlay[muted][pinned],
+.tab-icon-overlay[activemedia-blocked][pinned],
+.tab-icon-overlay[pictureinpicture],
+.tab-icon-overlay[crashed] {
+ display: -moz-box;
+}
+
+.tab-label {
+ white-space: nowrap;
+}
+
+.tab-label-container {
+ overflow: hidden;
+}
+
+.tab-label-container[pinned] {
+ width: 0;
+}
+
+.tab-label-container[textoverflow][labeldirection=ltr]:not([pinned]),
+.tab-label-container[textoverflow]:not([labeldirection], [pinned]):-moz-locale-dir(ltr) {
+ direction: ltr;
+ mask-image: linear-gradient(to left, transparent, black 2em);
+}
+
+.tab-label-container[textoverflow][labeldirection=rtl]:not([pinned]),
+.tab-label-container[textoverflow]:not([labeldirection], [pinned]):-moz-locale-dir(rtl) {
+ direction: rtl;
+ mask-image: linear-gradient(to right, transparent, black 2em);
+}
+
+tabpanels {
+ background-color: transparent;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ .tab-icon-image {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+.closing-tabs-spacer {
+ pointer-events: none;
+}
+
+#tabbrowser-arrowscrollbox:not(:hover) > .closing-tabs-spacer {
+ transition: width .15s ease-out;
+}
+
+browser[blank],
+browser[pendingpaint] {
+ opacity: 0;
+}
+
+#tabbrowser-tabpanels[pendingpaint] {
+ background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 30px;
+}
diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js
new file mode 100644
index 0000000000..551ff05c73
--- /dev/null
+++ b/browser/base/content/tabbrowser.js
@@ -0,0 +1,6836 @@
+/* -*- 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/. */
+
+{
+ // start private scope for gBrowser
+ /**
+ * A set of known icons to use for internal pages. These are hardcoded so we can
+ * start loading them faster than ContentLinkHandler would normally find them.
+ */
+ const FAVICON_DEFAULTS = {
+ "about:newtab": "chrome://branding/content/icon32.png",
+ "about:home": "chrome://branding/content/icon32.png",
+ "about:welcome": "chrome://branding/content/icon32.png",
+ "about:privatebrowsing":
+ "chrome://browser/skin/privatebrowsing/favicon.svg",
+ };
+
+ window._gBrowser = {
+ init() {
+ ChromeUtils.defineModuleGetter(
+ this,
+ "AsyncTabSwitcher",
+ "resource:///modules/AsyncTabSwitcher.jsm"
+ );
+ ChromeUtils.defineModuleGetter(
+ this,
+ "UrlbarProviderOpenTabs",
+ "resource:///modules/UrlbarProviderOpenTabs.jsm"
+ );
+
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ ChromeUtils.defineModuleGetter(
+ this,
+ "TabCrashHandler",
+ "resource:///modules/ContentCrashHandlers.jsm"
+ );
+ }
+
+ Services.obs.addObserver(this, "contextual-identity-updated");
+
+ Services.els.addSystemEventListener(document, "keydown", this, false);
+ Services.els.addSystemEventListener(document, "keypress", this, false);
+ window.addEventListener("sizemodechange", this);
+ window.addEventListener("occlusionstatechange", this);
+ window.addEventListener("framefocusrequested", this);
+
+ this.tabContainer.init();
+ this._setupInitialBrowserAndTab();
+
+ if (Services.prefs.getBoolPref("browser.display.use_system_colors")) {
+ this.tabpanels.style.backgroundColor = "-moz-default-background-color";
+ } else if (
+ Services.prefs.getIntPref("browser.display.document_color_use") == 2
+ ) {
+ this.tabpanels.style.backgroundColor = Services.prefs.getCharPref(
+ "browser.display.background_color"
+ );
+ }
+
+ this._setFindbarData();
+
+ XPCOMUtils.defineLazyModuleGetters(this, {
+ E10SUtils: "resource://gre/modules/E10SUtils.jsm",
+ });
+
+ // We take over setting the document title, so remove the l10n id to
+ // avoid it being re-translated and overwriting document content if
+ // we ever switch languages at runtime. After a language change, the
+ // window title will update at the next tab or location change.
+ document.querySelector("title").removeAttribute("data-l10n-id");
+
+ this._setupEventListeners();
+ this._initialized = true;
+ },
+
+ ownerGlobal: window,
+
+ ownerDocument: document,
+
+ closingTabsEnum: { ALL: 0, OTHER: 1, TO_END: 2, MULTI_SELECTED: 3 },
+
+ _visibleTabs: null,
+
+ _tabs: null,
+
+ _lastRelatedTabMap: new WeakMap(),
+
+ mProgressListeners: [],
+
+ mTabsProgressListeners: [],
+
+ _tabListeners: new Map(),
+
+ _tabFilters: new Map(),
+
+ _isBusy: false,
+
+ arrowKeysShouldWrap: AppConstants == "macosx",
+
+ _dateTimePicker: null,
+
+ _previewMode: false,
+
+ _lastFindValue: "",
+
+ _contentWaitingCount: 0,
+
+ _tabLayerCache: [],
+
+ tabAnimationsInProgress: 0,
+
+ /**
+ * Binding from browser to tab
+ */
+ _tabForBrowser: new WeakMap(),
+
+ /**
+ * `_createLazyBrowser` will define properties on the unbound lazy browser
+ * which correspond to properties defined in MozBrowser which will be bound to
+ * the browser when it is inserted into the document. If any of these
+ * properties are accessed by consumers, `_insertBrowser` is called and
+ * the browser is inserted to ensure that things don't break. This list
+ * provides the names of properties that may be called while the browser
+ * is in its unbound (lazy) state.
+ */
+ _browserBindingProperties: [
+ "canGoBack",
+ "canGoForward",
+ "goBack",
+ "goForward",
+ "permitUnload",
+ "reload",
+ "reloadWithFlags",
+ "stop",
+ "loadURI",
+ "gotoIndex",
+ "currentURI",
+ "documentURI",
+ "remoteType",
+ "preferences",
+ "imageDocument",
+ "isRemoteBrowser",
+ "messageManager",
+ "getTabBrowser",
+ "finder",
+ "fastFind",
+ "sessionHistory",
+ "contentTitle",
+ "characterSet",
+ "fullZoom",
+ "textZoom",
+ "tabHasCustomZoom",
+ "webProgress",
+ "addProgressListener",
+ "removeProgressListener",
+ "audioPlaybackStarted",
+ "audioPlaybackStopped",
+ "resumeMedia",
+ "mute",
+ "unmute",
+ "blockedPopups",
+ "lastURI",
+ "purgeSessionHistory",
+ "stopScroll",
+ "startScroll",
+ "userTypedValue",
+ "userTypedClear",
+ "didStartLoadSinceLastUserTyping",
+ "audioMuted",
+ ],
+
+ _removingTabs: [],
+
+ _multiSelectedTabsSet: new WeakSet(),
+
+ _lastMultiSelectedTabRef: null,
+
+ _clearMultiSelectionLocked: false,
+
+ _clearMultiSelectionLockedOnce: false,
+
+ _multiSelectChangeStarted: false,
+
+ _multiSelectChangeAdditions: new Set(),
+
+ _multiSelectChangeRemovals: new Set(),
+
+ _multiSelectChangeSelected: false,
+
+ /**
+ * Tab close requests are ignored if the window is closing anyway,
+ * e.g. when holding Ctrl+W.
+ */
+ _windowIsClosing: false,
+
+ preloadedBrowser: null,
+
+ /**
+ * This defines a proxy which allows us to access browsers by
+ * index without actually creating a full array of browsers.
+ */
+ browsers: new Proxy([], {
+ has: (target, name) => {
+ if (typeof name == "string" && Number.isInteger(parseInt(name))) {
+ return name in gBrowser.tabs;
+ }
+ return false;
+ },
+ get: (target, name) => {
+ if (name == "length") {
+ return gBrowser.tabs.length;
+ }
+ if (typeof name == "string" && Number.isInteger(parseInt(name))) {
+ if (!(name in gBrowser.tabs)) {
+ return undefined;
+ }
+ return gBrowser.tabs[name].linkedBrowser;
+ }
+ return target[name];
+ },
+ }),
+
+ /**
+ * List of browsers whose docshells must be active in order for print preview
+ * to work.
+ */
+ _printPreviewBrowsers: new Set(),
+
+ _switcher: null,
+
+ _soundPlayingAttrRemovalTimer: 0,
+
+ _hoverTabTimer: null,
+
+ get tabContainer() {
+ delete this.tabContainer;
+ return (this.tabContainer = document.getElementById("tabbrowser-tabs"));
+ },
+
+ get tabs() {
+ if (!this._tabs) {
+ this._tabs = this.tabContainer.allTabs;
+ }
+ return this._tabs;
+ },
+
+ get tabbox() {
+ delete this.tabbox;
+ return (this.tabbox = document.getElementById("tabbrowser-tabbox"));
+ },
+
+ get tabpanels() {
+ delete this.tabpanels;
+ return (this.tabpanels = document.getElementById("tabbrowser-tabpanels"));
+ },
+
+ addEventListener(...args) {
+ this.tabpanels.addEventListener(...args);
+ },
+
+ removeEventListener(...args) {
+ this.tabpanels.removeEventListener(...args);
+ },
+
+ dispatchEvent(...args) {
+ return this.tabpanels.dispatchEvent(...args);
+ },
+
+ get visibleTabs() {
+ if (!this._visibleTabs) {
+ this._visibleTabs = Array.prototype.filter.call(
+ this.tabs,
+ tab => !tab.hidden && !tab.closing
+ );
+ }
+ return this._visibleTabs;
+ },
+
+ get _numPinnedTabs() {
+ for (var i = 0; i < this.tabs.length; i++) {
+ if (!this.tabs[i].pinned) {
+ break;
+ }
+ }
+ return i;
+ },
+
+ set selectedTab(val) {
+ if (
+ gSharedTabWarning.willShowSharedTabWarning(val) ||
+ (gNavToolbox.collapsed && !this._allowTabChange)
+ ) {
+ return this.tabbox.selectedTab;
+ }
+ // Update the tab
+ this.tabbox.selectedTab = val;
+ return val;
+ },
+
+ get selectedTab() {
+ return this._selectedTab;
+ },
+
+ get selectedBrowser() {
+ return this._selectedBrowser;
+ },
+
+ _setupInitialBrowserAndTab() {
+ // See browser.js for the meaning of window.arguments.
+ // Bug 1485961 covers making this more sane.
+ let userContextId = window.arguments && window.arguments[5];
+
+ let openWindowInfo = window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).initialOpenWindowInfo;
+
+ if (!openWindowInfo && window.arguments && window.arguments[11]) {
+ openWindowInfo = window.arguments[11];
+ }
+
+ let tabArgument = gBrowserInit.getTabToAdopt();
+
+ // If we have a tab argument with browser, we use its remoteType. Otherwise,
+ // if e10s is disabled or there's a parent process opener (e.g. parent
+ // process about: page) for the content tab, we use a parent
+ // process remoteType. Otherwise, we check the URI to determine
+ // what to do - if there isn't one, we default to the default remote type.
+ //
+ // When adopting a tab, we'll also use that tab's browsingContextGroupId,
+ // if available, to ensure we don't spawn a new process.
+ let remoteType;
+ let initialBrowsingContextGroupId;
+
+ if (tabArgument && tabArgument.hasAttribute("usercontextid")) {
+ // The window's first argument is a tab if and only if we are swapping tabs.
+ // We must set the browser's usercontextid so that the newly created remote
+ // tab child has the correct usercontextid.
+ userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
+ }
+
+ if (tabArgument && tabArgument.linkedBrowser) {
+ remoteType = tabArgument.linkedBrowser.remoteType;
+ initialBrowsingContextGroupId =
+ tabArgument.linkedBrowser.browsingContext?.group.id;
+ } else if (openWindowInfo) {
+ userContextId = openWindowInfo.originAttributes.userContextId;
+ if (openWindowInfo.isRemote) {
+ remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
+ } else {
+ remoteType = E10SUtils.NOT_REMOTE;
+ }
+ } else {
+ let uriToLoad = gBrowserInit.uriToLoadPromise;
+ if (uriToLoad && Array.isArray(uriToLoad)) {
+ uriToLoad = uriToLoad[0]; // we only care about the first item
+ }
+
+ if (uriToLoad && typeof uriToLoad == "string") {
+ let oa = E10SUtils.predictOriginAttributes({
+ window,
+ userContextId,
+ });
+ remoteType = E10SUtils.getRemoteTypeForURI(
+ uriToLoad,
+ gMultiProcessBrowser,
+ gFissionBrowser,
+ E10SUtils.DEFAULT_REMOTE_TYPE,
+ null,
+ oa
+ );
+ } else {
+ remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
+ }
+ }
+
+ let createOptions = {
+ uriIsAboutBlank: false,
+ userContextId,
+ initialBrowsingContextGroupId,
+ remoteType,
+ openWindowInfo,
+ };
+ let browser = this.createBrowser(createOptions);
+ browser.setAttribute("primary", "true");
+ if (gBrowserAllowScriptsToCloseInitialTabs) {
+ browser.setAttribute("allowscriptstoclose", "true");
+ }
+ browser.droppedLinkHandler = handleDroppedLink;
+ browser.loadURI = _loadURI.bind(null, browser);
+
+ let uniqueId = this._generateUniquePanelID();
+ let panel = this.getPanel(browser);
+ panel.id = uniqueId;
+ this.tabpanels.appendChild(panel);
+
+ let tab = this.tabs[0];
+ tab.linkedPanel = uniqueId;
+ this._selectedTab = tab;
+ this._selectedBrowser = browser;
+ tab.permanentKey = browser.permanentKey;
+ tab._tPos = 0;
+ tab._fullyOpen = true;
+ tab.linkedBrowser = browser;
+
+ if (userContextId) {
+ tab.setAttribute("usercontextid", userContextId);
+ ContextualIdentityService.setTabStyle(tab);
+ }
+
+ this._tabForBrowser.set(browser, tab);
+
+ this._appendStatusPanel();
+
+ // This is the initial browser, so it's usually active; the default is false
+ // so we have to update it:
+ browser.docShellIsActive = this.shouldActivateDocShell(browser);
+
+ // Hook the browser up with a progress listener.
+ let tabListener = new TabProgressListener(tab, browser, true, false);
+ let filter = Cc[
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+ ].createInstance(Ci.nsIWebProgress);
+ filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ this._tabListeners.set(tab, tabListener);
+ this._tabFilters.set(tab, filter);
+ browser.webProgress.addProgressListener(
+ filter,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+ },
+
+ /**
+ * BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
+ * MAKE SURE TO ADD IT HERE AS WELL.
+ */
+ get canGoBack() {
+ return this.selectedBrowser.canGoBack;
+ },
+
+ get canGoForward() {
+ return this.selectedBrowser.canGoForward;
+ },
+
+ goBack(requireUserInteraction) {
+ return this.selectedBrowser.goBack(requireUserInteraction);
+ },
+
+ goForward(requireUserInteraction) {
+ return this.selectedBrowser.goForward(requireUserInteraction);
+ },
+
+ reload() {
+ return this.selectedBrowser.reload();
+ },
+
+ reloadWithFlags(aFlags) {
+ return this.selectedBrowser.reloadWithFlags(aFlags);
+ },
+
+ stop() {
+ return this.selectedBrowser.stop();
+ },
+
+ /**
+ * throws exception for unknown schemes
+ */
+ loadURI(aURI, aParams) {
+ return this.selectedBrowser.loadURI(aURI, aParams);
+ },
+
+ gotoIndex(aIndex) {
+ return this.selectedBrowser.gotoIndex(aIndex);
+ },
+
+ get currentURI() {
+ return this.selectedBrowser.currentURI;
+ },
+
+ get finder() {
+ return this.selectedBrowser.finder;
+ },
+
+ get docShell() {
+ return this.selectedBrowser.docShell;
+ },
+
+ get webNavigation() {
+ return this.selectedBrowser.webNavigation;
+ },
+
+ get webProgress() {
+ return this.selectedBrowser.webProgress;
+ },
+
+ get contentWindow() {
+ return this.selectedBrowser.contentWindow;
+ },
+
+ get sessionHistory() {
+ return this.selectedBrowser.sessionHistory;
+ },
+
+ get markupDocumentViewer() {
+ return this.selectedBrowser.markupDocumentViewer;
+ },
+
+ get contentDocument() {
+ return this.selectedBrowser.contentDocument;
+ },
+
+ get contentTitle() {
+ return this.selectedBrowser.contentTitle;
+ },
+
+ get contentPrincipal() {
+ return this.selectedBrowser.contentPrincipal;
+ },
+
+ get securityUI() {
+ return this.selectedBrowser.securityUI;
+ },
+
+ set fullZoom(val) {
+ this.selectedBrowser.fullZoom = val;
+ },
+
+ get fullZoom() {
+ return this.selectedBrowser.fullZoom;
+ },
+
+ set textZoom(val) {
+ this.selectedBrowser.textZoom = val;
+ },
+
+ get textZoom() {
+ return this.selectedBrowser.textZoom;
+ },
+
+ get isSyntheticDocument() {
+ return this.selectedBrowser.isSyntheticDocument;
+ },
+
+ set userTypedValue(val) {
+ this.selectedBrowser.userTypedValue = val;
+ },
+
+ get userTypedValue() {
+ return this.selectedBrowser.userTypedValue;
+ },
+
+ _invalidateCachedTabs() {
+ this._tabs = null;
+ this._visibleTabs = null;
+ },
+
+ _setFindbarData() {
+ // Ensure we know what the find bar key is in the content process:
+ let { sharedData } = Services.ppmm;
+ if (!sharedData.has("Findbar:Shortcut")) {
+ let keyEl = document.getElementById("key_find");
+ let mods = keyEl
+ .getAttribute("modifiers")
+ .replace(
+ /accel/i,
+ AppConstants.platform == "macosx" ? "meta" : "control"
+ );
+ sharedData.set("Findbar:Shortcut", {
+ key: keyEl.getAttribute("key"),
+ shiftKey: mods.includes("shift"),
+ ctrlKey: mods.includes("control"),
+ altKey: mods.includes("alt"),
+ metaKey: mods.includes("meta"),
+ });
+ }
+ },
+
+ isFindBarInitialized(aTab) {
+ return (aTab || this.selectedTab)._findBar != undefined;
+ },
+
+ /**
+ * Get the already constructed findbar
+ */
+ getCachedFindBar(aTab = this.selectedTab) {
+ return aTab._findBar;
+ },
+
+ /**
+ * Get the findbar, and create it if it doesn't exist.
+ * @return the find bar (or null if the window or tab is closed/closing in the interim).
+ */
+ async getFindBar(aTab = this.selectedTab) {
+ let findBar = this.getCachedFindBar(aTab);
+ if (findBar) {
+ return findBar;
+ }
+
+ // Avoid re-entrancy by caching the promise we're about to return.
+ if (!aTab._pendingFindBar) {
+ aTab._pendingFindBar = this._createFindBar(aTab);
+ }
+ return aTab._pendingFindBar;
+ },
+
+ /**
+ * Create a findbar instance.
+ * @param aTab the tab to create the find bar for.
+ * @return the created findbar, or null if the window or tab is closed/closing.
+ */
+ async _createFindBar(aTab) {
+ let findBar = document.createXULElement("findbar");
+ let browser = this.getBrowserForTab(aTab);
+
+ // The findbar should be inserted after the browserStack and, if present for
+ // this tab, after the StatusPanel as well.
+ let insertAfterElement = browser.parentNode;
+ if (insertAfterElement.nextElementSibling == StatusPanel.panel) {
+ insertAfterElement = StatusPanel.panel;
+ }
+ insertAfterElement.insertAdjacentElement("afterend", findBar);
+
+ await new Promise(r => requestAnimationFrame(r));
+ delete aTab._pendingFindBar;
+ if (window.closed || aTab.closing) {
+ return null;
+ }
+
+ findBar.browser = browser;
+ findBar._findField.value = this._lastFindValue;
+
+ aTab._findBar = findBar;
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabFindInitialized", true, false);
+ aTab.dispatchEvent(event);
+
+ return findBar;
+ },
+
+ _appendStatusPanel() {
+ this.selectedBrowser.parentNode.insertAdjacentElement(
+ "afterend",
+ StatusPanel.panel
+ );
+ },
+
+ _updateTabBarForPinnedTabs() {
+ this.tabContainer._unlockTabSizing();
+ this.tabContainer._positionPinnedTabs();
+ this.tabContainer._updateCloseButtons();
+ },
+
+ _notifyPinnedStatus(aTab) {
+ aTab.linkedBrowser.sendMessageToActor(
+ "Browser:AppTab",
+ { isAppTab: aTab.pinned },
+ "BrowserTab"
+ );
+
+ let event = document.createEvent("Events");
+ event.initEvent(aTab.pinned ? "TabPinned" : "TabUnpinned", true, false);
+ aTab.dispatchEvent(event);
+ },
+
+ pinTab(aTab) {
+ if (aTab.pinned) {
+ return;
+ }
+
+ if (aTab.hidden) {
+ this.showTab(aTab);
+ }
+
+ this.moveTabTo(aTab, this._numPinnedTabs);
+ aTab.setAttribute("pinned", "true");
+ this._updateTabBarForPinnedTabs();
+ this._notifyPinnedStatus(aTab);
+ },
+
+ unpinTab(aTab) {
+ if (!aTab.pinned) {
+ return;
+ }
+
+ this.moveTabTo(aTab, this._numPinnedTabs - 1);
+ aTab.removeAttribute("pinned");
+ aTab.style.marginInlineStart = "";
+ aTab._pinnedUnscrollable = false;
+ this._updateTabBarForPinnedTabs();
+ this._notifyPinnedStatus(aTab);
+ },
+
+ previewTab(aTab, aCallback) {
+ let currentTab = this.selectedTab;
+ try {
+ // Suppress focus, ownership and selected tab changes
+ this._previewMode = true;
+ this.selectedTab = aTab;
+ aCallback();
+ } finally {
+ this.selectedTab = currentTab;
+ this._previewMode = false;
+ }
+ },
+
+ _getAndMaybeCreateDateTimePickerPanel() {
+ if (!this._dateTimePicker) {
+ let wrapper = document.getElementById("dateTimePickerTemplate");
+ wrapper.replaceWith(wrapper.content);
+ this._dateTimePicker = document.getElementById("DateTimePickerPanel");
+ }
+
+ return this._dateTimePicker;
+ },
+
+ syncThrobberAnimations(aTab) {
+ aTab.ownerGlobal.promiseDocumentFlushed(() => {
+ if (!aTab.container) {
+ return;
+ }
+
+ const animations = Array.from(
+ aTab.container.getElementsByTagName("tab")
+ )
+ .map(tab => {
+ const throbber = tab.throbber;
+ return throbber ? throbber.getAnimations({ subtree: true }) : [];
+ })
+ .reduce((a, b) => a.concat(b))
+ .filter(
+ anim =>
+ anim instanceof CSSAnimation &&
+ (anim.animationName === "tab-throbber-animation" ||
+ anim.animationName === "tab-throbber-animation-rtl") &&
+ anim.playState === "running"
+ );
+
+ // Synchronize with the oldest running animation, if any.
+ const firstStartTime = Math.min(
+ ...animations.map(anim =>
+ anim.startTime === null ? Infinity : anim.startTime
+ )
+ );
+ if (firstStartTime === Infinity) {
+ return;
+ }
+ requestAnimationFrame(() => {
+ for (let animation of animations) {
+ // If |animation| has been cancelled since this rAF callback
+ // was scheduled we don't want to set its startTime since
+ // that would restart it. We check for a cancelled animation
+ // by looking for a null currentTime rather than checking
+ // the playState, since reading the playState of
+ // a CSSAnimation object will flush style.
+ if (animation.currentTime !== null) {
+ animation.startTime = firstStartTime;
+ }
+ }
+ });
+ });
+ },
+
+ getBrowserAtIndex(aIndex) {
+ return this.browsers[aIndex];
+ },
+
+ getBrowserForOuterWindowID(aID) {
+ for (let b of this.browsers) {
+ if (b.outerWindowID == aID) {
+ return b;
+ }
+ }
+
+ return null;
+ },
+
+ getTabForBrowser(aBrowser) {
+ return this._tabForBrowser.get(aBrowser);
+ },
+
+ getPanel(aBrowser) {
+ return this.getBrowserContainer(aBrowser).parentNode;
+ },
+
+ getBrowserContainer(aBrowser) {
+ return (aBrowser || this.selectedBrowser).parentNode.parentNode;
+ },
+
+ getNotificationBox(aBrowser) {
+ let browser = aBrowser || this.selectedBrowser;
+ if (!browser._notificationBox) {
+ browser._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "top");
+ this.getBrowserContainer(browser).prepend(element);
+ });
+ }
+ return browser._notificationBox;
+ },
+
+ getTabModalPromptBox(aBrowser) {
+ let browser = aBrowser || this.selectedBrowser;
+ if (!browser.tabModalPromptBox) {
+ browser.tabModalPromptBox = new TabModalPromptBox(browser);
+ }
+ return browser.tabModalPromptBox;
+ },
+
+ getTabDialogBox(aBrowser) {
+ let browser = aBrowser || this.selectedBrowser;
+ if (!browser.tabDialogBox) {
+ browser.tabDialogBox = new TabDialogBox(browser);
+ }
+ return browser.tabDialogBox;
+ },
+
+ getTabFromAudioEvent(aEvent) {
+ if (!aEvent.isTrusted) {
+ return null;
+ }
+
+ var browser = aEvent.originalTarget;
+ var tab = this.getTabForBrowser(browser);
+ return tab;
+ },
+
+ _callProgressListeners(
+ aBrowser,
+ aMethod,
+ aArguments,
+ aCallGlobalListeners = true,
+ aCallTabsListeners = true
+ ) {
+ var rv = true;
+
+ function callListeners(listeners, args) {
+ for (let p of listeners) {
+ if (aMethod in p) {
+ try {
+ if (!p[aMethod].apply(p, args)) {
+ rv = false;
+ }
+ } catch (e) {
+ // don't inhibit other listeners
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+
+ aBrowser = aBrowser || this.selectedBrowser;
+
+ if (aCallGlobalListeners && aBrowser == this.selectedBrowser) {
+ callListeners(this.mProgressListeners, aArguments);
+ }
+
+ if (aCallTabsListeners) {
+ aArguments.unshift(aBrowser);
+
+ callListeners(this.mTabsProgressListeners, aArguments);
+ }
+
+ return rv;
+ },
+
+ /**
+ * Sets an icon for the tab if the URI is defined in FAVICON_DEFAULTS.
+ */
+ setDefaultIcon(aTab, aURI) {
+ if (aURI && aURI.spec in FAVICON_DEFAULTS) {
+ this.setIcon(aTab, FAVICON_DEFAULTS[aURI.spec]);
+ }
+ },
+
+ setIcon(
+ aTab,
+ aIconURL = "",
+ aOriginalURL = aIconURL,
+ aLoadingPrincipal = null
+ ) {
+ let makeString = url => (url instanceof Ci.nsIURI ? url.spec : url);
+
+ aIconURL = makeString(aIconURL);
+ aOriginalURL = makeString(aOriginalURL);
+
+ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
+
+ if (
+ aIconURL &&
+ !aLoadingPrincipal &&
+ !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
+ ) {
+ console.error(
+ `Attempt to set a remote URL ${aIconURL} as a tab icon without a loading principal.`
+ );
+ return;
+ }
+
+ let browser = this.getBrowserForTab(aTab);
+ browser.mIconURL = aIconURL;
+
+ if (aIconURL != aTab.getAttribute("image")) {
+ if (aIconURL) {
+ if (aLoadingPrincipal) {
+ aTab.setAttribute("iconloadingprincipal", aLoadingPrincipal);
+ } else {
+ aTab.removeAttribute("iconloadingprincipal");
+ }
+ aTab.setAttribute("image", aIconURL);
+ } else {
+ aTab.removeAttribute("image");
+ aTab.removeAttribute("iconloadingprincipal");
+ }
+ this._tabAttrModified(aTab, ["image"]);
+ }
+
+ // The aOriginalURL argument is currently only used by tests.
+ this._callProgressListeners(browser, "onLinkIconAvailable", [
+ aIconURL,
+ aOriginalURL,
+ ]);
+ },
+
+ getIcon(aTab) {
+ let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
+ return browser.mIconURL;
+ },
+
+ setPageInfo(aURL, aDescription, aPreviewImage) {
+ if (aURL) {
+ let pageInfo = {
+ url: aURL,
+ description: aDescription,
+ previewImageURL: aPreviewImage,
+ };
+ PlacesUtils.history.update(pageInfo).catch(Cu.reportError);
+ }
+ },
+
+ getWindowTitleForBrowser(aBrowser) {
+ let docElement = document.documentElement;
+ let title = "";
+
+ // If location bar is hidden and the URL type supports a host,
+ // add the scheme and host to the title to prevent spoofing.
+ // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
+ try {
+ if (docElement.getAttribute("chromehidden").includes("location")) {
+ const uri = Services.uriFixup.createExposableURI(aBrowser.currentURI);
+ let prefix = uri.prePath;
+ if (uri.scheme == "about") {
+ prefix = uri.spec;
+ } else if (uri.scheme == "moz-extension") {
+ const ext = WebExtensionPolicy.getByHostname(uri.host);
+ if (ext && ext.name) {
+ let extensionLabel = document.getElementById(
+ "urlbar-label-extension"
+ );
+ prefix = `${extensionLabel.value} (${ext.name})`;
+ }
+ }
+ title = prefix + " - ";
+ }
+ } catch (e) {
+ // ignored
+ }
+
+ if (docElement.hasAttribute("titlepreface")) {
+ title += docElement.getAttribute("titlepreface");
+ }
+
+ let tab = this.getTabForBrowser(aBrowser);
+ if (tab._labelIsContentTitle) {
+ // Strip out any null bytes in the content title, since the
+ // underlying widget implementations of nsWindow::SetTitle pass
+ // null-terminated strings to system APIs.
+ title += tab.getAttribute("label").replace(/\0/g, "");
+ }
+
+ let dataSuffix =
+ docElement.getAttribute("privatebrowsingmode") == "temporary"
+ ? "Private"
+ : "Default";
+ if (title) {
+ // We're using a function rather than just using `title` as the
+ // new substring to avoid `$$`, `$'` etc. having a special
+ // meaning to `replace`.
+ // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter
+ // and the documentation for functions for more info about this.
+ return docElement.dataset["contentTitle" + dataSuffix].replace(
+ "CONTENTTITLE",
+ () => title
+ );
+ }
+
+ return docElement.dataset["title" + dataSuffix];
+ },
+
+ updateTitlebar() {
+ document.title = this.getWindowTitleForBrowser(this.selectedBrowser);
+ },
+
+ updateCurrentBrowser(aForceUpdate) {
+ let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
+ if (this.selectedBrowser == newBrowser && !aForceUpdate) {
+ return;
+ }
+
+ let newTab = this.getTabForBrowser(newBrowser);
+
+ if (!aForceUpdate) {
+ TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
+
+ if (gMultiProcessBrowser) {
+ this._getSwitcher().requestTab(newTab);
+ }
+
+ document.commandDispatcher.lock();
+ }
+
+ let oldTab = this.selectedTab;
+
+ // Preview mode should not reset the owner
+ if (!this._previewMode && !oldTab.selected) {
+ oldTab.owner = null;
+ }
+
+ let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
+ if (lastRelatedTab) {
+ if (!lastRelatedTab.selected) {
+ lastRelatedTab.owner = null;
+ }
+ }
+ this._lastRelatedTabMap = new WeakMap();
+
+ let oldBrowser = this.selectedBrowser;
+
+ if (!gMultiProcessBrowser) {
+ oldBrowser.removeAttribute("primary");
+ oldBrowser.docShellIsActive = false;
+ newBrowser.setAttribute("primary", "true");
+ newBrowser.docShellIsActive =
+ window.windowState != window.STATE_MINIMIZED &&
+ !window.isFullyOccluded;
+ }
+
+ this._selectedBrowser = newBrowser;
+ this._selectedTab = newTab;
+ this.showTab(newTab);
+
+ this._appendStatusPanel();
+
+ let oldBrowserPopupsBlocked = oldBrowser.popupBlocker.getBlockedPopupCount();
+ let newBrowserPopupsBlocked = newBrowser.popupBlocker.getBlockedPopupCount();
+ if (oldBrowserPopupsBlocked != newBrowserPopupsBlocked) {
+ newBrowser.popupBlocker.updateBlockedPopupsUI();
+ }
+
+ // Update the URL bar.
+ let webProgress = newBrowser.webProgress;
+ this._callProgressListeners(
+ null,
+ "onLocationChange",
+ [webProgress, null, newBrowser.currentURI, 0, true],
+ true,
+ false
+ );
+
+ let securityUI = newBrowser.securityUI;
+ if (securityUI) {
+ this._callProgressListeners(
+ null,
+ "onSecurityChange",
+ [webProgress, null, securityUI.state],
+ true,
+ false
+ );
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners(
+ null,
+ "onContentBlockingEvent",
+ [webProgress, null, newBrowser.getContentBlockingEvents(), true],
+ true,
+ false
+ );
+ }
+
+ let listener = this._tabListeners.get(newTab);
+ if (listener && listener.mStateFlags) {
+ this._callProgressListeners(
+ null,
+ "onUpdateCurrentBrowser",
+ [
+ listener.mStateFlags,
+ listener.mStatus,
+ listener.mMessage,
+ listener.mTotalProgress,
+ ],
+ true,
+ false
+ );
+ }
+
+ if (!this._previewMode) {
+ newTab.updateLastAccessed();
+ oldTab.updateLastAccessed();
+
+ let oldFindBar = oldTab._findBar;
+ if (
+ oldFindBar &&
+ oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
+ !oldFindBar.hidden
+ ) {
+ this._lastFindValue = oldFindBar._findField.value;
+ }
+
+ this.updateTitlebar();
+
+ newTab.removeAttribute("titlechanged");
+ newTab.removeAttribute("attention");
+ this._tabAttrModified(newTab, ["attention"]);
+
+ // The tab has been selected, it's not unselected anymore.
+ // (1) Call the current tab's finishUnselectedTabHoverTimer()
+ // to save a telemetry record.
+ // (2) Call the current browser's unselectedTabHover() with false
+ // to dispatch an event.
+ newTab.finishUnselectedTabHoverTimer();
+ newBrowser.unselectedTabHover(false);
+ }
+
+ // If the new tab is busy, and our current state is not busy, then
+ // we need to fire a start to all progress listeners.
+ if (newTab.hasAttribute("busy") && !this._isBusy) {
+ this._isBusy = true;
+ this._callProgressListeners(
+ null,
+ "onStateChange",
+ [
+ webProgress,
+ null,
+ Ci.nsIWebProgressListener.STATE_START |
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK,
+ 0,
+ ],
+ true,
+ false
+ );
+ }
+
+ // If the new tab is not busy, and our current state is busy, then
+ // we need to fire a stop to all progress listeners.
+ if (!newTab.hasAttribute("busy") && this._isBusy) {
+ this._isBusy = false;
+ this._callProgressListeners(
+ null,
+ "onStateChange",
+ [
+ webProgress,
+ null,
+ Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK,
+ 0,
+ ],
+ true,
+ false
+ );
+ }
+
+ // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
+ // that might rely upon the other changes suppressed.
+ // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
+ if (!this._previewMode) {
+ // We've selected the new tab, so go ahead and notify listeners.
+ let event = new CustomEvent("TabSelect", {
+ bubbles: true,
+ cancelable: false,
+ detail: {
+ previousTab: oldTab,
+ },
+ });
+ newTab.dispatchEvent(event);
+
+ this._tabAttrModified(oldTab, ["selected"]);
+ this._tabAttrModified(newTab, ["selected"]);
+
+ this._startMultiSelectChange();
+ this._multiSelectChangeSelected = true;
+ this.clearMultiSelectedTabs();
+ if (this._multiSelectChangeAdditions.size) {
+ // Some tab has been multiselected just before switching tabs.
+ // The tab that was selected at that point should also be multiselected.
+ this.addToMultiSelectedTabs(oldTab);
+ }
+
+ if (oldBrowser != newBrowser && oldBrowser.getInPermitUnload) {
+ oldBrowser.getInPermitUnload(inPermitUnload => {
+ if (!inPermitUnload) {
+ return;
+ }
+ // Since the user is switching away from a tab that has
+ // a beforeunload prompt active, we remove the prompt.
+ // This prevents confusing user flows like the following:
+ // 1. User attempts to close Firefox
+ // 2. User switches tabs (ingoring a beforeunload prompt)
+ // 3. User returns to tab, presses "Leave page"
+ let promptBox = this.getTabModalPromptBox(oldBrowser);
+ let prompts = promptBox.listPrompts();
+ // There might not be any prompts here if the tab was closed
+ // while in an onbeforeunload prompt, which will have
+ // destroyed aforementioned prompt already, so check there's
+ // something to remove, first:
+ if (prompts.length) {
+ // NB: This code assumes that the beforeunload prompt
+ // is the top-most prompt on the tab.
+ prompts[prompts.length - 1].abortPrompt();
+ }
+ });
+ }
+
+ if (!gMultiProcessBrowser) {
+ this._adjustFocusBeforeTabSwitch(oldTab, newTab);
+ this._adjustFocusAfterTabSwitch(newTab);
+ gURLBar.afterTabSwitchFocusChange();
+ }
+ }
+
+ updateUserContextUIIndicator();
+ gIdentityHandler.updateSharingIndicator();
+
+ // Enable touch events to start a native dragging
+ // session to allow the user to easily drag the selected tab.
+ // This is currently only supported on Windows.
+ oldTab.removeAttribute("touchdownstartsdrag");
+ newTab.setAttribute("touchdownstartsdrag", "true");
+
+ if (!gMultiProcessBrowser) {
+ this.tabContainer._setPositionalAttributes();
+
+ document.commandDispatcher.unlock();
+
+ let event = new CustomEvent("TabSwitchDone", {
+ bubbles: true,
+ cancelable: true,
+ });
+ this.dispatchEvent(event);
+ }
+
+ if (!aForceUpdate) {
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
+ }
+ },
+
+ _adjustFocusBeforeTabSwitch(oldTab, newTab) {
+ if (this._previewMode) {
+ return;
+ }
+
+ let oldBrowser = oldTab.linkedBrowser;
+ let newBrowser = newTab.linkedBrowser;
+
+ oldBrowser._urlbarFocused = gURLBar && gURLBar.focused;
+
+ if (this.isFindBarInitialized(oldTab)) {
+ let findBar = this.getCachedFindBar(oldTab);
+ oldTab._findBarFocused =
+ !findBar.hidden &&
+ findBar._findField.getAttribute("focused") == "true";
+ }
+
+ let activeEl = document.activeElement;
+ // If focus is on the old tab, move it to the new tab.
+ if (activeEl == oldTab) {
+ newTab.focus();
+ } else if (
+ gMultiProcessBrowser &&
+ activeEl != newBrowser &&
+ activeEl != newTab
+ ) {
+ // In e10s, if focus isn't already in the tabstrip or on the new browser,
+ // and the new browser's previous focus wasn't in the url bar but focus is
+ // there now, we need to adjust focus further.
+ let keepFocusOnUrlBar =
+ newBrowser && newBrowser._urlbarFocused && gURLBar && gURLBar.focused;
+ if (!keepFocusOnUrlBar) {
+ // Clear focus so that _adjustFocusAfterTabSwitch can detect if
+ // some element has been focused and respect that.
+ document.activeElement.blur();
+ }
+ }
+ },
+
+ _adjustFocusAfterTabSwitch(newTab) {
+ // Don't steal focus from the tab bar.
+ if (document.activeElement == newTab) {
+ return;
+ }
+
+ let newBrowser = this.getBrowserForTab(newTab);
+
+ if (newBrowser.hasAttribute("tabDialogShowing")) {
+ newBrowser.tabDialogBox.focus();
+ } else if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
+ // If there's a tabmodal prompt showing, focus it.
+ let prompts = newBrowser.tabModalPromptBox.listPrompts();
+ let prompt = prompts[prompts.length - 1];
+ // @tabmodalPromptShowing is also set for other tab modal prompts
+ // (e.g. the Payment Request dialog) so there may not be a <tabmodalprompt>.
+ // Bug 1492814 will implement this for the Payment Request dialog.
+ if (prompt) {
+ prompt.Dialog.setDefaultFocus();
+ return;
+ }
+ }
+
+ // Focus the location bar if it was previously focused for that tab.
+ // In full screen mode, only bother making the location bar visible
+ // if the tab is a blank one.
+ if (newBrowser._urlbarFocused && gURLBar) {
+ // If the user happened to type into the URL bar for this browser
+ // by the time we got here, focusing will cause the text to be
+ // selected which could cause them to overwrite what they've
+ // already typed in.
+ if (gURLBar.focused && newBrowser.userTypedValue) {
+ return;
+ }
+
+ if (!window.fullScreen || newTab.isEmpty) {
+ gURLBar.select();
+ return;
+ }
+ }
+
+ // Focus the find bar if it was previously focused for that tab.
+ if (
+ gFindBarInitialized &&
+ !gFindBar.hidden &&
+ this.selectedTab._findBarFocused
+ ) {
+ gFindBar._findField.focus();
+ return;
+ }
+
+ // Don't focus the content area if something has been focused after the
+ // tab switch was initiated.
+ if (gMultiProcessBrowser && document.activeElement != document.body) {
+ return;
+ }
+
+ // We're now committed to focusing the content area.
+ let fm = Services.focus;
+ let focusFlags = fm.FLAG_NOSCROLL;
+
+ if (!gMultiProcessBrowser) {
+ let newFocusedElement = fm.getFocusedElementForWindow(
+ window.content,
+ true,
+ {}
+ );
+
+ // for anchors, use FLAG_SHOWRING so that it is clear what link was
+ // last clicked when switching back to that tab
+ if (
+ newFocusedElement &&
+ (newFocusedElement instanceof HTMLAnchorElement ||
+ newFocusedElement.getAttributeNS(
+ "http://www.w3.org/1999/xlink",
+ "type"
+ ) == "simple")
+ ) {
+ focusFlags |= fm.FLAG_SHOWRING;
+ }
+ }
+
+ fm.setFocus(newBrowser, focusFlags);
+ },
+
+ _tabAttrModified(aTab, aChanged) {
+ if (aTab.closing) {
+ return;
+ }
+
+ let event = new CustomEvent("TabAttrModified", {
+ bubbles: true,
+ cancelable: false,
+ detail: {
+ changed: aChanged,
+ },
+ });
+ aTab.dispatchEvent(event);
+ },
+
+ resetBrowserSharing(aBrowser) {
+ let tab = this.getTabForBrowser(aBrowser);
+ if (!tab) {
+ return;
+ }
+ tab._sharingState = {};
+ tab.removeAttribute("sharing");
+ this._tabAttrModified(tab, ["sharing"]);
+ if (aBrowser == this.selectedBrowser) {
+ gIdentityHandler.updateSharingIndicator();
+ }
+ },
+
+ updateBrowserSharing(aBrowser, aState) {
+ let tab = this.getTabForBrowser(aBrowser);
+ if (!tab) {
+ return;
+ }
+ if (tab._sharingState == null) {
+ tab._sharingState = {};
+ }
+ tab._sharingState = Object.assign(tab._sharingState, aState);
+
+ if ("webRTC" in aState) {
+ if (tab._sharingState.webRTC?.sharing) {
+ if (tab._sharingState.webRTC.paused) {
+ tab.removeAttribute("sharing");
+ } else {
+ tab.setAttribute("sharing", aState.webRTC.sharing);
+ }
+ } else {
+ tab._sharingState.webRTC = null;
+ tab.removeAttribute("sharing");
+ }
+ this._tabAttrModified(tab, ["sharing"]);
+ }
+
+ if (aBrowser == this.selectedBrowser) {
+ gIdentityHandler.updateSharingIndicator();
+ }
+ },
+
+ getTabSharingState(aTab) {
+ // Normalize the state object for consumers (ie.extensions).
+ let state = Object.assign(
+ {},
+ aTab._sharingState && aTab._sharingState.webRTC
+ );
+ return {
+ camera: !!state.camera,
+ microphone: !!state.microphone,
+ screen: state.screen && state.screen.replace("Paused", ""),
+ };
+ },
+
+ setInitialTabTitle(aTab, aTitle, aOptions = {}) {
+ // Convert some non-content title (actually a url) to human readable title
+ if (!aOptions.isContentTitle && isBlankPageURL(aTitle)) {
+ aTitle = this.tabContainer.emptyTabTitle;
+ }
+
+ if (aTitle) {
+ if (!aTab.getAttribute("label")) {
+ aTab._labelIsInitialTitle = true;
+ }
+
+ this._setTabLabel(aTab, aTitle, aOptions);
+ }
+ },
+
+ setTabTitle(aTab) {
+ var browser = this.getBrowserForTab(aTab);
+ var title = browser.contentTitle;
+
+ if (aTab.hasAttribute("customizemode")) {
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle", [
+ brandShortName,
+ ]);
+ }
+
+ // Don't replace an initially set label with the URL while the tab
+ // is loading.
+ if (aTab._labelIsInitialTitle) {
+ if (!title) {
+ return false;
+ }
+ delete aTab._labelIsInitialTitle;
+ }
+
+ let isContentTitle = !!title;
+ if (!title) {
+ // See if we can use the URI as the title.
+ if (browser.currentURI.displaySpec) {
+ try {
+ title = Services.io.createExposableURI(browser.currentURI)
+ .displaySpec;
+ } catch (ex) {
+ title = browser.currentURI.displaySpec;
+ }
+ }
+
+ if (title && !isBlankPageURL(title)) {
+ // If it's a long data: URI that uses base64 encoding, truncate to a
+ // reasonable length rather than trying to display the entire thing,
+ // which can be slow.
+ // We can't shorten arbitrary URIs like this, as bidi etc might mean
+ // we need the trailing characters for display. But a base64-encoded
+ // data-URI is plain ASCII, so this is OK for tab-title display.
+ // (See bug 1408854.)
+ if (title.length > 500 && title.match(/^data:[^,]+;base64,/)) {
+ title = title.substring(0, 500) + "\u2026";
+ } else {
+ // Try to unescape not-ASCII URIs using the current character set.
+ try {
+ let characterSet = browser.characterSet;
+ title = Services.textToSubURI.unEscapeNonAsciiURI(
+ characterSet,
+ title
+ );
+ } catch (ex) {
+ /* Do nothing. */
+ }
+ }
+ } else {
+ // No suitable URI? Fall back to our untitled string.
+ title = this.tabContainer.emptyTabTitle;
+ }
+ }
+
+ return this._setTabLabel(aTab, title, { isContentTitle });
+ },
+
+ _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle } = {}) {
+ if (!aLabel) {
+ return false;
+ }
+
+ aTab._fullLabel = aLabel;
+
+ if (!isContentTitle) {
+ // Remove protocol and "www."
+ if (!("_regex_shortenURLForTabLabel" in this)) {
+ this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/;
+ }
+ aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, "");
+ }
+
+ aTab._labelIsContentTitle = isContentTitle;
+
+ if (aTab.getAttribute("label") == aLabel) {
+ return false;
+ }
+
+ let dwu = window.windowUtils;
+ let isRTL =
+ dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;
+
+ aTab.setAttribute("label", aLabel);
+ aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr");
+
+ // Dispatch TabAttrModified event unless we're setting the label
+ // before the TabOpen event was dispatched.
+ if (!beforeTabOpen) {
+ this._tabAttrModified(aTab, ["label"]);
+ }
+
+ if (aTab.selected) {
+ this.updateTitlebar();
+ }
+
+ return true;
+ },
+
+ loadOneTab(
+ aURI,
+ aReferrerInfoOrParams,
+ aCharset,
+ aPostData,
+ aLoadInBackground,
+ aAllowThirdPartyFixup
+ ) {
+ var aTriggeringPrincipal;
+ var aReferrerInfo;
+ var aFromExternal;
+ var aRelatedToCurrent;
+ var aAllowInheritPrincipal;
+ var aAllowMixedContent;
+ var aSkipAnimation;
+ var aForceNotRemote;
+ var aPreferredRemoteType;
+ var aUserContextId;
+ var aInitialBrowsingContextGroupId;
+ var aOriginPrincipal;
+ var aOriginStoragePrincipal;
+ var aOpenWindowInfo;
+ var aOpenerBrowser;
+ var aCreateLazyBrowser;
+ var aFocusUrlBar;
+ var aName;
+ var aCsp;
+ var aSkipLoad;
+ if (
+ arguments.length == 2 &&
+ typeof arguments[1] == "object" &&
+ !(arguments[1] instanceof Ci.nsIURI)
+ ) {
+ let params = arguments[1];
+ aTriggeringPrincipal = params.triggeringPrincipal;
+ aReferrerInfo = params.referrerInfo;
+ aCharset = params.charset;
+ aPostData = params.postData;
+ aLoadInBackground = params.inBackground;
+ aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ aFromExternal = params.fromExternal;
+ aRelatedToCurrent = params.relatedToCurrent;
+ aAllowInheritPrincipal = !!params.allowInheritPrincipal;
+ aAllowMixedContent = params.allowMixedContent;
+ aSkipAnimation = params.skipAnimation;
+ aForceNotRemote = params.forceNotRemote;
+ aPreferredRemoteType = params.preferredRemoteType;
+ aUserContextId = params.userContextId;
+ aInitialBrowsingContextGroupId = params.initialBrowsingContextGroupId;
+ aOriginPrincipal = params.originPrincipal;
+ aOriginStoragePrincipal = params.originStoragePrincipal;
+ aOpenWindowInfo = params.openWindowInfo;
+ aOpenerBrowser = params.openerBrowser;
+ aCreateLazyBrowser = params.createLazyBrowser;
+ aFocusUrlBar = params.focusUrlBar;
+ aName = params.name;
+ aCsp = params.csp;
+ aSkipLoad = params.skipLoad;
+ }
+
+ // all callers of loadOneTab need to pass a valid triggeringPrincipal.
+ if (!aTriggeringPrincipal) {
+ throw new Error(
+ "Required argument triggeringPrincipal missing within loadOneTab"
+ );
+ }
+
+ var bgLoad =
+ aLoadInBackground != null
+ ? aLoadInBackground
+ : Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+ var owner = bgLoad ? null : this.selectedTab;
+
+ var tab = this.addTab(aURI, {
+ triggeringPrincipal: aTriggeringPrincipal,
+ referrerInfo: aReferrerInfo,
+ charset: aCharset,
+ postData: aPostData,
+ ownerTab: owner,
+ allowInheritPrincipal: aAllowInheritPrincipal,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ fromExternal: aFromExternal,
+ relatedToCurrent: aRelatedToCurrent,
+ skipAnimation: aSkipAnimation,
+ allowMixedContent: aAllowMixedContent,
+ forceNotRemote: aForceNotRemote,
+ createLazyBrowser: aCreateLazyBrowser,
+ preferredRemoteType: aPreferredRemoteType,
+ userContextId: aUserContextId,
+ originPrincipal: aOriginPrincipal,
+ originStoragePrincipal: aOriginStoragePrincipal,
+ initialBrowsingContextGroupId: aInitialBrowsingContextGroupId,
+ openWindowInfo: aOpenWindowInfo,
+ openerBrowser: aOpenerBrowser,
+ focusUrlBar: aFocusUrlBar,
+ name: aName,
+ csp: aCsp,
+ skipLoad: aSkipLoad,
+ });
+ if (!bgLoad) {
+ this.selectedTab = tab;
+ }
+
+ return tab;
+ },
+
+ loadTabs(
+ aURIs,
+ {
+ allowInheritPrincipal,
+ allowThirdPartyFixup,
+ inBackground,
+ newIndex,
+ postDatas,
+ replace,
+ targetTab,
+ triggeringPrincipal,
+ csp,
+ userContextId,
+ fromExternal,
+ } = {}
+ ) {
+ if (!aURIs.length) {
+ return;
+ }
+
+ // The tab selected after this new tab is closed (i.e. the new tab's
+ // "owner") is the next adjacent tab (i.e. not the previously viewed tab)
+ // when several urls are opened here (i.e. closing the first should select
+ // the next of many URLs opened) or if the pref to have UI links opened in
+ // the background is set (i.e. the link is not being opened modally)
+ //
+ // i.e.
+ // Number of URLs Load UI Links in BG Focus Last Viewed?
+ // == 1 false YES
+ // == 1 true NO
+ // > 1 false/true NO
+ var multiple = aURIs.length > 1;
+ var owner = multiple || inBackground ? null : this.selectedTab;
+ var firstTabAdded = null;
+ var targetTabIndex = -1;
+
+ if (typeof newIndex != "number") {
+ newIndex = -1;
+ }
+
+ // When bulk opening tabs, such as from a bookmark folder, we want to insertAfterCurrent
+ // if necessary, but we also will set the bulkOrderedOpen flag so that the bookmarks
+ // open in the same order they are in the folder.
+ if (
+ multiple &&
+ newIndex < 0 &&
+ Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")
+ ) {
+ newIndex = this.selectedTab._tPos + 1;
+ }
+
+ if (replace) {
+ let browser;
+ if (targetTab) {
+ browser = this.getBrowserForTab(targetTab);
+ targetTabIndex = targetTab._tPos;
+ } else {
+ browser = this.selectedBrowser;
+ targetTabIndex = this.tabContainer.selectedIndex;
+ }
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (allowThirdPartyFixup) {
+ flags |=
+ Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
+ Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ if (!allowInheritPrincipal) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+ if (fromExternal) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
+ }
+ try {
+ browser.loadURI(aURIs[0], {
+ flags,
+ postData: postDatas && postDatas[0],
+ triggeringPrincipal,
+ csp,
+ });
+ } catch (e) {
+ // Ignore failure in case a URI is wrong, so we can continue
+ // opening the next ones.
+ }
+ } else {
+ let params = {
+ allowInheritPrincipal,
+ ownerTab: owner,
+ skipAnimation: multiple,
+ allowThirdPartyFixup,
+ postData: postDatas && postDatas[0],
+ userContextId,
+ triggeringPrincipal,
+ bulkOrderedOpen: multiple,
+ csp,
+ fromExternal,
+ };
+ if (newIndex > -1) {
+ params.index = newIndex;
+ }
+ firstTabAdded = this.addTab(aURIs[0], params);
+ if (newIndex > -1) {
+ targetTabIndex = firstTabAdded._tPos;
+ }
+ }
+
+ let tabNum = targetTabIndex;
+ for (let i = 1; i < aURIs.length; ++i) {
+ let params = {
+ allowInheritPrincipal,
+ skipAnimation: true,
+ allowThirdPartyFixup,
+ postData: postDatas && postDatas[i],
+ userContextId,
+ triggeringPrincipal,
+ bulkOrderedOpen: true,
+ csp,
+ fromExternal,
+ };
+ if (targetTabIndex > -1) {
+ params.index = ++tabNum;
+ }
+ this.addTab(aURIs[i], params);
+ }
+
+ if (firstTabAdded && !inBackground) {
+ this.selectedTab = firstTabAdded;
+ }
+ },
+
+ updateBrowserRemoteness(aBrowser, { newFrameloader, remoteType } = {}) {
+ let isRemote = aBrowser.getAttribute("remote") == "true";
+
+ // We have to be careful with this here, as the "no remote type" is null,
+ // not a string. Make sure to check only for undefined, since null is
+ // allowed.
+ if (remoteType === undefined) {
+ throw new Error("Remote type must be set!");
+ }
+
+ let shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
+
+ if (!gMultiProcessBrowser && shouldBeRemote) {
+ throw new Error(
+ "Cannot switch to remote browser in a window " +
+ "without the remote tabs load context."
+ );
+ }
+
+ // Abort if we're not going to change anything
+ let oldRemoteType = aBrowser.remoteType;
+ if (
+ isRemote == shouldBeRemote &&
+ !newFrameloader &&
+ (!isRemote || oldRemoteType == remoteType)
+ ) {
+ return false;
+ }
+
+ let tab = this.getTabForBrowser(aBrowser);
+ // aBrowser needs to be inserted now if it hasn't been already.
+ this._insertBrowser(tab);
+
+ let evt = document.createEvent("Events");
+ evt.initEvent("BeforeTabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ let wasActive = document.activeElement == aBrowser;
+
+ // Unhook our progress listener.
+ let filter = this._tabFilters.get(tab);
+ let listener = this._tabListeners.get(tab);
+ aBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(listener);
+
+ // We'll be creating a new listener, so destroy the old one.
+ listener.destroy();
+
+ let oldDroppedLinkHandler = aBrowser.droppedLinkHandler;
+ let oldUserTypedValue = aBrowser.userTypedValue;
+ let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
+
+ // Change the "remote" attribute.
+
+ // Make sure the browser is destroyed so it unregisters from observer notifications
+ aBrowser.destroy();
+
+ if (shouldBeRemote) {
+ aBrowser.setAttribute("remote", "true");
+ aBrowser.setAttribute("remoteType", remoteType);
+ } else {
+ aBrowser.setAttribute("remote", "false");
+ aBrowser.removeAttribute("remoteType");
+ }
+
+ // This call actually switches out our frameloaders. Do this as late as
+ // possible before rebuilding the browser, as we'll need the new browser
+ // state set up completely first.
+ aBrowser.changeRemoteness({
+ remoteType,
+ });
+
+ // Once we have new frameloaders, this call sets the browser back up.
+ aBrowser.construct();
+
+ aBrowser.userTypedValue = oldUserTypedValue;
+ if (hadStartedLoad) {
+ aBrowser.urlbarChangeTracker.startedLoad();
+ }
+
+ aBrowser.droppedLinkHandler = oldDroppedLinkHandler;
+
+ // This shouldn't really be necessary (it should always set the same
+ // value as activeness is correctly preserved across remoteness changes).
+ // However, this has the side effect of sending MozLayerTreeReady /
+ // MozLayerTreeCleared events for remote frames, which the tab switcher
+ // depends on.
+ aBrowser.docShellIsActive = this.shouldActivateDocShell(aBrowser);
+
+ // Create a new tab progress listener for the new browser we just injected,
+ // since tab progress listeners have logic for handling the initial about:blank
+ // load
+ listener = new TabProgressListener(tab, aBrowser, true, false);
+ this._tabListeners.set(tab, listener);
+ filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ // Restore the progress listener.
+ aBrowser.webProgress.addProgressListener(
+ filter,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+
+ // Restore the securityUI state.
+ let securityUI = aBrowser.securityUI;
+ let state = securityUI
+ ? securityUI.state
+ : Ci.nsIWebProgressListener.STATE_IS_INSECURE;
+ this._callProgressListeners(
+ aBrowser,
+ "onSecurityChange",
+ [aBrowser.webProgress, null, state],
+ true,
+ false
+ );
+ let event = aBrowser.getContentBlockingEvents();
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners(
+ aBrowser,
+ "onContentBlockingEvent",
+ [aBrowser.webProgress, null, event, true],
+ true,
+ false
+ );
+
+ if (shouldBeRemote) {
+ // Switching the browser to be remote will connect to a new child
+ // process so the browser can no longer be considered to be
+ // crashed.
+ tab.removeAttribute("crashed");
+ } else {
+ aBrowser.sendMessageToActor(
+ "Browser:AppTab",
+ { isAppTab: tab.pinned },
+ "BrowserTab"
+ );
+ }
+
+ if (wasActive) {
+ aBrowser.focus();
+ }
+
+ // If the findbar has been initialised, reset its browser reference.
+ if (this.isFindBarInitialized(tab)) {
+ this.getCachedFindBar(tab).browser = aBrowser;
+ }
+
+ tab.linkedBrowser.sendMessageToActor(
+ "Browser:HasSiblings",
+ this.tabs.length > 1,
+ "BrowserTab"
+ );
+
+ evt = document.createEvent("Events");
+ evt.initEvent("TabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ return true;
+ },
+
+ updateBrowserRemotenessByURL(aBrowser, aURL, aOptions = {}) {
+ if (!gMultiProcessBrowser) {
+ return this.updateBrowserRemoteness(aBrowser, {
+ remoteType: E10SUtils.NOT_REMOTE,
+ });
+ }
+
+ let oldRemoteType = aBrowser.remoteType;
+
+ let oa = E10SUtils.predictOriginAttributes({ browser: aBrowser });
+
+ aOptions.remoteType = E10SUtils.getRemoteTypeForURI(
+ aURL,
+ gMultiProcessBrowser,
+ gFissionBrowser,
+ oldRemoteType,
+ aBrowser.currentURI,
+ oa
+ );
+
+ // If this URL can't load in the current browser then flip it to the
+ // correct type.
+ if (oldRemoteType != aOptions.remoteType || aOptions.newFrameloader) {
+ return this.updateBrowserRemoteness(aBrowser, aOptions);
+ }
+
+ return false;
+ },
+
+ createBrowser({
+ isPreloadBrowser,
+ name,
+ openWindowInfo,
+ remoteType,
+ initialBrowsingContextGroupId,
+ uriIsAboutBlank,
+ userContextId,
+ skipLoad,
+ initiallyActive,
+ } = {}) {
+ let b = document.createXULElement("browser");
+ // Use the JSM global to create the permanentKey, so that if the
+ // permanentKey is held by something after this window closes, it
+ // doesn't keep the window alive.
+ b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
+
+ // Ensure that SessionStore has flushed any session history state from the
+ // content process before we this browser's remoteness.
+ if (!Services.appinfo.sessionHistoryInParent) {
+ b.prepareToChangeRemoteness = () =>
+ SessionStore.prepareToChangeRemoteness(b);
+ b.afterChangeRemoteness = switchId => {
+ let tab = this.getTabForBrowser(b);
+ SessionStore.finishTabRemotenessChange(tab, switchId);
+ return true;
+ };
+ }
+
+ const defaultBrowserAttributes = {
+ contextmenu: "contentAreaContextMenu",
+ maychangeremoteness: "true",
+ message: "true",
+ messagemanagergroup: "browsers",
+ selectmenulist: "ContentSelectDropdown",
+ tooltip: "aHTMLTooltip",
+ type: "content",
+ };
+ for (let attribute in defaultBrowserAttributes) {
+ b.setAttribute(attribute, defaultBrowserAttributes[attribute]);
+ }
+
+ if (!initiallyActive) {
+ b.setAttribute("initiallyactive", "false");
+ }
+
+ if (userContextId) {
+ b.setAttribute("usercontextid", userContextId);
+ }
+
+ if (remoteType) {
+ b.setAttribute("remoteType", remoteType);
+ b.setAttribute("remote", "true");
+ }
+
+ if (!isPreloadBrowser) {
+ b.setAttribute("autocompletepopup", "PopupAutoComplete");
+ }
+
+ /*
+ * This attribute is meant to describe if the browser is the
+ * preloaded browser. There are 2 defined states: "preloaded" or
+ * "consumed". The order of events goes as follows:
+ * 1. The preloaded browser is created and the 'preloadedState'
+ * attribute for that browser is set to "preloaded".
+ * 2. When a new tab is opened and it is time to show that
+ * preloaded browser, the 'preloadedState' attribute for that
+ * browser is set to "consumed"
+ * 3. When we then navigate away from about:newtab, the "consumed"
+ * browsers will attempt to switch to a new content process,
+ * therefore the 'preloadedState' attribute is removed from
+ * that browser altogether
+ * See more details on Bug 1420285.
+ */
+ if (isPreloadBrowser) {
+ b.setAttribute("preloadedState", "preloaded");
+ }
+
+ // Ensure that the browser will be created in a specific initial
+ // BrowsingContextGroup. This may change the process selection behaviour
+ // of the newly created browser, and is often used in combination with
+ // "remoteType" to ensure that the initial about:blank load occurs
+ // within the same process as another window.
+ if (initialBrowsingContextGroupId) {
+ b.setAttribute(
+ "initialBrowsingContextGroupId",
+ initialBrowsingContextGroupId
+ );
+ }
+
+ // Propagate information about the opening content window to the browser.
+ if (openWindowInfo) {
+ b.openWindowInfo = openWindowInfo;
+ }
+
+ // This will be used by gecko to control the name of the opened
+ // window.
+ if (name) {
+ // XXX: The `name` property is special in HTML and XUL. Should
+ // we use a different attribute name for this?
+ b.setAttribute("name", name);
+ }
+
+ let notificationbox = document.createXULElement("notificationbox");
+ notificationbox.setAttribute("notificationside", "top");
+
+ // We set large flex on both containers to allow the devtools toolbox to
+ // set a flex attribute. We don't want the toolbox to actually take up free
+ // space, but we do want it to collapse when the window shrinks, and with
+ // flex=0 it can't. When the toolbox is on the bottom it's a sibling of
+ // browserStack, and when it's on the side it's a sibling of
+ // browserContainer.
+ let stack = document.createXULElement("stack");
+ stack.className = "browserStack";
+ stack.appendChild(b);
+ stack.setAttribute("flex", "10000");
+
+ let browserContainer = document.createXULElement("vbox");
+ browserContainer.className = "browserContainer";
+ browserContainer.appendChild(notificationbox);
+ browserContainer.appendChild(stack);
+ browserContainer.setAttribute("flex", "10000");
+
+ let browserSidebarContainer = document.createXULElement("hbox");
+ browserSidebarContainer.className = "browserSidebarContainer";
+ browserSidebarContainer.appendChild(browserContainer);
+
+ // Prevent the superfluous initial load of a blank document
+ // if we're going to load something other than about:blank.
+ if (!uriIsAboutBlank || skipLoad) {
+ b.setAttribute("nodefaultsrc", "true");
+ }
+
+ return b;
+ },
+
+ _createLazyBrowser(aTab) {
+ let browser = aTab.linkedBrowser;
+
+ let names = this._browserBindingProperties;
+
+ for (let i = 0; i < names.length; i++) {
+ let name = names[i];
+ let getter;
+ let setter;
+ switch (name) {
+ case "audioMuted":
+ getter = () => aTab.hasAttribute("muted");
+ break;
+ case "contentTitle":
+ getter = () => SessionStore.getLazyTabValue(aTab, "title");
+ break;
+ case "currentURI":
+ getter = () => {
+ let url = SessionStore.getLazyTabValue(aTab, "url");
+ // Avoid recreating the same nsIURI object over and over again...
+ if (browser._cachedCurrentURI) {
+ return browser._cachedCurrentURI;
+ }
+ return (browser._cachedCurrentURI = Services.io.newURI(url));
+ };
+ break;
+ case "didStartLoadSinceLastUserTyping":
+ getter = () => () => false;
+ break;
+ case "fullZoom":
+ case "textZoom":
+ getter = () => 1;
+ break;
+ case "tabHasCustomZoom":
+ getter = () => false;
+ break;
+ case "getTabBrowser":
+ getter = () => () => this;
+ break;
+ case "isRemoteBrowser":
+ getter = () => browser.getAttribute("remote") == "true";
+ break;
+ case "permitUnload":
+ getter = () => () => ({ permitUnload: true });
+ break;
+ case "reload":
+ case "reloadWithFlags":
+ getter = () => params => {
+ // Wait for load handler to be instantiated before
+ // initializing the reload.
+ aTab.addEventListener(
+ "SSTabRestoring",
+ () => {
+ browser[name](params);
+ },
+ { once: true }
+ );
+ gBrowser._insertBrowser(aTab);
+ };
+ break;
+ case "remoteType":
+ getter = () => {
+ let url = SessionStore.getLazyTabValue(aTab, "url");
+ // Avoid recreating the same nsIURI object over and over again...
+ let uri;
+ if (browser._cachedCurrentURI) {
+ uri = browser._cachedCurrentURI;
+ } else {
+ uri = browser._cachedCurrentURI = Services.io.newURI(url);
+ }
+ let oa = E10SUtils.predictOriginAttributes({
+ browser,
+ userContextId: aTab.getAttribute("usercontextid"),
+ });
+ return E10SUtils.getRemoteTypeForURI(
+ url,
+ gMultiProcessBrowser,
+ gFissionBrowser,
+ undefined,
+ uri,
+ oa
+ );
+ };
+ break;
+ case "userTypedValue":
+ case "userTypedClear":
+ getter = () => SessionStore.getLazyTabValue(aTab, name);
+ break;
+ default:
+ getter = () => {
+ if (AppConstants.NIGHTLY_BUILD) {
+ let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
+ console.log(message + new Error().stack);
+ }
+ this._insertBrowser(aTab);
+ return browser[name];
+ };
+ setter = value => {
+ if (AppConstants.NIGHTLY_BUILD) {
+ let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
+ console.log(message + new Error().stack);
+ }
+ this._insertBrowser(aTab);
+ return (browser[name] = value);
+ };
+ }
+ Object.defineProperty(browser, name, {
+ get: getter,
+ set: setter,
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ },
+
+ _insertBrowser(aTab, aInsertedOnTabCreation) {
+ "use strict";
+
+ // If browser is already inserted or window is closed don't do anything.
+ if (aTab.linkedPanel || window.closed) {
+ return;
+ }
+
+ let browser = aTab.linkedBrowser;
+
+ // If browser is a lazy browser, delete the substitute properties.
+ if (this._browserBindingProperties[0] in browser) {
+ for (let name of this._browserBindingProperties) {
+ delete browser[name];
+ }
+ }
+
+ let { uriIsAboutBlank, usingPreloadedContent } = aTab._browserParams;
+ delete aTab._browserParams;
+ delete aTab._cachedCurrentURI;
+
+ let panel = this.getPanel(browser);
+ let uniqueId = this._generateUniquePanelID();
+ panel.id = uniqueId;
+ aTab.linkedPanel = uniqueId;
+
+ // Inject the <browser> into the DOM if necessary.
+ if (!panel.parentNode) {
+ // NB: this appendChild call causes us to run constructors for the
+ // browser element, which fires off a bunch of notifications. Some
+ // of those notifications can cause code to run that inspects our
+ // state, so it is important that the tab element is fully
+ // initialized by this point.
+ this.tabpanels.appendChild(panel);
+ }
+
+ // wire up a progress listener for the new browser object.
+ let tabListener = new TabProgressListener(
+ aTab,
+ browser,
+ uriIsAboutBlank,
+ usingPreloadedContent
+ );
+ const filter = Cc[
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+ ].createInstance(Ci.nsIWebProgress);
+ filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ browser.webProgress.addProgressListener(
+ filter,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+ this._tabListeners.set(aTab, tabListener);
+ this._tabFilters.set(aTab, filter);
+
+ browser.droppedLinkHandler = handleDroppedLink;
+ browser.loadURI = _loadURI.bind(null, browser);
+
+ // Most of the time, we start our browser's docShells out as inactive,
+ // and then maintain activeness in the tab switcher. Preloaded about:newtab's
+ // are already created with their docShell's as inactive, but then explicitly
+ // render their layers to ensure that we can switch to them quickly. We avoid
+ // setting docShellIsActive to false again in this case, since that'd cause
+ // the layers for the preloaded tab to be dropped, and we'd see a flash
+ // of empty content instead.
+ //
+ // So for all browsers except for the preloaded case, we set the browser
+ // docShell to inactive.
+ if (!usingPreloadedContent) {
+ browser.docShellIsActive = false;
+ }
+
+ // If we transitioned from one browser to two browsers, we need to set
+ // hasSiblings=false on both the existing browser and the new browser.
+ if (this.tabs.length == 2) {
+ this.tabs[0].linkedBrowser.sendMessageToActor(
+ "Browser:HasSiblings",
+ true,
+ "BrowserTab"
+ );
+ this.tabs[1].linkedBrowser.sendMessageToActor(
+ "Browser:HasSiblings",
+ true,
+ "BrowserTab"
+ );
+ } else {
+ aTab.linkedBrowser.sendMessageToActor(
+ "Browser:HasSiblings",
+ this.tabs.length > 1,
+ "BrowserTab"
+ );
+ }
+
+ // Only fire this event if the tab is already in the DOM
+ // and will be handled by a listener.
+ if (aTab.isConnected) {
+ var evt = new CustomEvent("TabBrowserInserted", {
+ bubbles: true,
+ detail: { insertedOnTabCreation: aInsertedOnTabCreation },
+ });
+ aTab.dispatchEvent(evt);
+ }
+ },
+
+ _mayDiscardBrowser(aTab, aForceDiscard) {
+ let browser = aTab.linkedBrowser;
+ let action = aForceDiscard ? "unload" : "dontUnload";
+
+ if (
+ !aTab ||
+ aTab.selected ||
+ aTab.closing ||
+ this._windowIsClosing ||
+ !browser.isConnected ||
+ !browser.isRemoteBrowser ||
+ !browser.permitUnload(action).permitUnload
+ ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ discardBrowser(aTab, aForceDiscard) {
+ "use strict";
+ let browser = aTab.linkedBrowser;
+
+ if (!this._mayDiscardBrowser(aTab, aForceDiscard)) {
+ return false;
+ }
+
+ // Reset sharing state.
+ if (aTab._sharingState) {
+ this.resetBrowserSharing(browser);
+ }
+ webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext);
+
+ // Set browser parameters for when browser is restored. Also remove
+ // listeners and set up lazy restore data in SessionStore. This must
+ // be done before browser is destroyed and removed from the document.
+ aTab._browserParams = {
+ uriIsAboutBlank: browser.currentURI.spec == "about:blank",
+ remoteType: browser.remoteType,
+ usingPreloadedContent: false,
+ };
+
+ SessionStore.resetBrowserToLazyState(aTab);
+
+ // Remove the tab's filter and progress listener.
+ let filter = this._tabFilters.get(aTab);
+ let listener = this._tabListeners.get(aTab);
+ browser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+
+ this._tabListeners.delete(aTab);
+ this._tabFilters.delete(aTab);
+
+ // Reset the findbar and remove it if it is attached to the tab.
+ if (aTab._findBar) {
+ aTab._findBar.close(true);
+ aTab._findBar.remove();
+ delete aTab._findBar;
+ }
+
+ // Remove potentially stale attributes.
+ let attributesToRemove = [
+ "activemedia-blocked",
+ "busy",
+ "pendingicon",
+ "progress",
+ "soundplaying",
+ ];
+ let removedAttributes = [];
+ for (let attr of attributesToRemove) {
+ if (aTab.hasAttribute(attr)) {
+ removedAttributes.push(attr);
+ aTab.removeAttribute(attr);
+ }
+ }
+ if (removedAttributes.length) {
+ this._tabAttrModified(aTab, removedAttributes);
+ }
+
+ browser.destroy();
+ this.getPanel(browser).remove();
+ aTab.removeAttribute("linkedpanel");
+
+ this._createLazyBrowser(aTab);
+
+ let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
+ aTab.dispatchEvent(evt);
+ return true;
+ },
+
+ /**
+ * Loads a tab with a default null principal unless specified
+ */
+ addWebTab(aURI, params = {}) {
+ if (!params.triggeringPrincipal) {
+ params.triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal(
+ {
+ userContextId: params.userContextId,
+ }
+ );
+ }
+ if (params.triggeringPrincipal.isSystemPrincipal) {
+ throw new Error(
+ "System principal should never be passed into addWebTab()"
+ );
+ }
+ return this.addTab(aURI, params);
+ },
+
+ /**
+ * Must only be used sparingly for content that came from Chrome context
+ * If in doubt use addWebTab
+ */
+ addTrustedTab(aURI, params = {}) {
+ params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ return this.addTab(aURI, params);
+ },
+
+ // eslint-disable-next-line complexity
+ addTab(
+ aURI,
+ {
+ allowInheritPrincipal,
+ allowMixedContent,
+ allowThirdPartyFixup,
+ bulkOrderedOpen,
+ charset,
+ createLazyBrowser,
+ disableTRR,
+ eventDetail,
+ focusUrlBar,
+ forceNotRemote,
+ fromExternal,
+ index,
+ lazyTabTitle,
+ name,
+ noInitialLabel,
+ openWindowInfo,
+ openerBrowser,
+ originPrincipal,
+ originStoragePrincipal,
+ ownerTab,
+ pinned,
+ postData,
+ preferredRemoteType,
+ referrerInfo,
+ relatedToCurrent,
+ initialBrowsingContextGroupId,
+ skipAnimation,
+ skipBackgroundNotify,
+ triggeringPrincipal,
+ userContextId,
+ csp,
+ skipLoad,
+ batchInsertingTabs,
+ } = {}
+ ) {
+ // all callers of addTab that pass a params object need to pass
+ // a valid triggeringPrincipal.
+ if (!triggeringPrincipal) {
+ throw new Error(
+ "Required argument triggeringPrincipal missing within addTab"
+ );
+ }
+
+ if (!UserInteraction.running("browser.tabs.opening", window)) {
+ UserInteraction.start("browser.tabs.opening", "initting", window);
+ }
+
+ // Don't use document.l10n.setAttributes because the FTL file is loaded
+ // lazily and we won't be able to resolve the string.
+ document
+ .getElementById("History:UndoCloseTab")
+ .setAttribute("data-l10n-args", JSON.stringify({ tabCount: 1 }));
+ SessionStore.setLastClosedTabCount(window, 1);
+
+ // if we're adding tabs, we're past interrupt mode, ditch the owner
+ if (this.selectedTab.owner) {
+ this.selectedTab.owner = null;
+ }
+
+ // Find the tab that opened this one, if any. This is used for
+ // determining positioning, and inherited attributes such as the
+ // user context ID.
+ //
+ // If we have a browser opener (which is usually the browser
+ // element from a remote window.open() call), use that.
+ //
+ // Otherwise, if the tab is related to the current tab (e.g.,
+ // because it was opened by a link click), use the selected tab as
+ // the owner. If referrerInfo is set, and we don't have an
+ // explicit relatedToCurrent arg, we assume that the tab is
+ // related to the current tab, since referrerURI is null or
+ // undefined if the tab is opened from an external application or
+ // bookmark (i.e. somewhere other than an existing tab).
+ if (relatedToCurrent == null) {
+ relatedToCurrent = !!(referrerInfo && referrerInfo.originalReferrer);
+ }
+ let openerTab =
+ (openerBrowser && this.getTabForBrowser(openerBrowser)) ||
+ (relatedToCurrent && this.selectedTab);
+
+ var t = document.createXULElement("tab", { is: "tabbrowser-tab" });
+ // Tag the tab as being created so extension code can ignore events
+ // prior to TabOpen.
+ t.initializingTab = true;
+ t.openerTab = openerTab;
+
+ aURI = aURI || "about:blank";
+ let aURIObject = null;
+ try {
+ aURIObject = Services.io.newURI(aURI);
+ } catch (ex) {
+ /* we'll try to fix up this URL later */
+ }
+
+ let lazyBrowserURI;
+ if (createLazyBrowser && aURI != "about:blank") {
+ lazyBrowserURI = aURIObject;
+ aURI = "about:blank";
+ }
+
+ var uriIsAboutBlank = aURI == "about:blank";
+
+ // When overflowing, new tabs are scrolled into view smoothly, which
+ // doesn't go well together with the width transition. So we skip the
+ // transition in that case.
+ let animate =
+ !skipAnimation &&
+ !pinned &&
+ this.tabContainer.getAttribute("overflow") != "true" &&
+ !gReduceMotion;
+
+ // Related tab inherits current tab's user context unless a different
+ // usercontextid is specified
+ if (userContextId == null && openerTab) {
+ userContextId = openerTab.getAttribute("usercontextid") || 0;
+ }
+
+ if (!noInitialLabel) {
+ if (isBlankPageURL(aURI)) {
+ t.setAttribute("label", this.tabContainer.emptyTabTitle);
+ } else {
+ // Set URL as label so that the tab isn't empty initially.
+ this.setInitialTabTitle(t, aURI, { beforeTabOpen: true });
+ }
+ }
+
+ if (userContextId) {
+ t.setAttribute("usercontextid", userContextId);
+ ContextualIdentityService.setTabStyle(t);
+ }
+
+ if (skipBackgroundNotify) {
+ t.setAttribute("skipbackgroundnotify", true);
+ }
+
+ if (pinned) {
+ t.setAttribute("pinned", "true");
+ }
+
+ t.classList.add("tabbrowser-tab");
+
+ this.tabContainer._unlockTabSizing();
+
+ if (!animate) {
+ UserInteraction.update("browser.tabs.opening", "not-animated", window);
+ t.setAttribute("fadein", "true");
+
+ // Call _handleNewTab asynchronously as it needs to know if the
+ // new tab is selected.
+ setTimeout(
+ function(tabContainer) {
+ tabContainer._handleNewTab(t);
+ },
+ 0,
+ this.tabContainer
+ );
+ } else {
+ UserInteraction.update("browser.tabs.opening", "animated", window);
+ }
+
+ let usingPreloadedContent = false;
+ let b;
+
+ try {
+ if (!batchInsertingTabs) {
+ // When we are not restoring a session, we need to know
+ // insert the tab into the tab container in the correct position
+ this._insertTabAtIndex(t, {
+ index,
+ ownerTab,
+ openerTab,
+ pinned,
+ bulkOrderedOpen,
+ });
+ }
+
+ // If we don't have a preferred remote type, and we have a remote
+ // opener, use the opener's remote type.
+ if (!preferredRemoteType && openerBrowser) {
+ preferredRemoteType = openerBrowser.remoteType;
+ }
+
+ var oa = E10SUtils.predictOriginAttributes({ window, userContextId });
+
+ // If URI is about:blank and we don't have a preferred remote type,
+ // then we need to use the referrer, if we have one, to get the
+ // correct remote type for the new tab.
+ if (
+ uriIsAboutBlank &&
+ !preferredRemoteType &&
+ referrerInfo &&
+ referrerInfo.originalReferrer
+ ) {
+ preferredRemoteType = E10SUtils.getRemoteTypeForURI(
+ referrerInfo.originalReferrer.spec,
+ gMultiProcessBrowser,
+ gFissionBrowser,
+ E10SUtils.DEFAULT_REMOTE_TYPE,
+ null,
+ oa
+ );
+ }
+
+ let remoteType = forceNotRemote
+ ? E10SUtils.NOT_REMOTE
+ : E10SUtils.getRemoteTypeForURI(
+ aURI,
+ gMultiProcessBrowser,
+ gFissionBrowser,
+ preferredRemoteType,
+ null,
+ oa
+ );
+
+ // If we open a new tab with the newtab URL in the default
+ // userContext, check if there is a preloaded browser ready.
+ if (aURI == BROWSER_NEW_TAB_URL && !userContextId) {
+ b = NewTabPagePreloading.getPreloadedBrowser(window);
+ if (b) {
+ usingPreloadedContent = true;
+ }
+ }
+
+ if (!b) {
+ // No preloaded browser found, create one.
+ b = this.createBrowser({
+ remoteType,
+ uriIsAboutBlank,
+ userContextId,
+ initialBrowsingContextGroupId,
+ openWindowInfo,
+ name,
+ skipLoad,
+ });
+ }
+
+ t.linkedBrowser = b;
+
+ if (focusUrlBar) {
+ b._urlbarFocused = true;
+ }
+
+ this._tabForBrowser.set(b, t);
+ t.permanentKey = b.permanentKey;
+ t._browserParams = {
+ uriIsAboutBlank,
+ remoteType,
+ usingPreloadedContent,
+ };
+
+ // If the caller opts in, create a lazy browser.
+ if (createLazyBrowser) {
+ this._createLazyBrowser(t);
+
+ if (lazyBrowserURI) {
+ // Lazy browser must be explicitly registered so tab will appear as
+ // a switch-to-tab candidate in autocomplete.
+ this.UrlbarProviderOpenTabs.registerOpenTab(
+ lazyBrowserURI.spec,
+ userContextId
+ );
+ b.registeredOpenURI = lazyBrowserURI;
+ }
+ SessionStore.setTabState(t, {
+ entries: [
+ {
+ url: lazyBrowserURI ? lazyBrowserURI.spec : "about:blank",
+ title: lazyTabTitle,
+ triggeringPrincipal_base64: E10SUtils.serializePrincipal(
+ triggeringPrincipal
+ ),
+ },
+ ],
+ });
+ } else {
+ this._insertBrowser(t, true);
+ }
+ } catch (e) {
+ Cu.reportError("Failed to create tab");
+ Cu.reportError(e);
+ t.remove();
+ if (t.linkedBrowser) {
+ this._tabFilters.delete(t);
+ this._tabListeners.delete(t);
+ this.getPanel(t.linkedBrowser).remove();
+ }
+ throw e;
+ }
+
+ // Hack to ensure that the about:newtab, and about:welcome favicon is loaded
+ // instantaneously, to avoid flickering and improve perceived performance.
+ this.setDefaultIcon(t, aURIObject);
+
+ if (!batchInsertingTabs) {
+ // Fire a TabOpen event
+ this._fireTabOpen(t, eventDetail);
+
+ if (
+ !usingPreloadedContent &&
+ originPrincipal &&
+ originStoragePrincipal &&
+ aURI
+ ) {
+ let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
+ // Unless we know for sure we're not inheriting principals,
+ // force the about:blank viewer to have the right principal:
+ if (
+ !aURIObject ||
+ doGetProtocolFlags(aURIObject) & URI_INHERITS_SECURITY_CONTEXT
+ ) {
+ b.createAboutBlankContentViewer(
+ originPrincipal,
+ originStoragePrincipal
+ );
+ }
+ }
+
+ // If we didn't swap docShells with a preloaded browser
+ // then let's just continue loading the page normally.
+ if (
+ !usingPreloadedContent &&
+ (!uriIsAboutBlank || !allowInheritPrincipal) &&
+ !skipLoad
+ ) {
+ // pretend the user typed this so it'll be available till
+ // the document successfully loads
+ if (aURI && !gInitialPages.includes(aURI)) {
+ b.userTypedValue = aURI;
+ }
+
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (allowThirdPartyFixup) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ if (fromExternal) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
+ } else if (!triggeringPrincipal.isSystemPrincipal) {
+ // XXX this code must be reviewed and changed when bug 1616353
+ // lands.
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIRST_LOAD;
+ }
+ if (allowMixedContent) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
+ }
+ if (!allowInheritPrincipal) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+ if (disableTRR) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISABLE_TRR;
+ }
+ try {
+ b.loadURI(aURI, {
+ flags,
+ triggeringPrincipal,
+ referrerInfo,
+ charset,
+ postData,
+ csp,
+ });
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+
+ // This field is updated regardless if we actually animate
+ // since it's important that we keep this count correct in all cases.
+ this.tabAnimationsInProgress++;
+
+ if (animate) {
+ requestAnimationFrame(function() {
+ // kick the animation off
+ t.setAttribute("fadein", "true");
+ });
+ }
+
+ // Additionally send pinned tab events
+ if (pinned) {
+ this._notifyPinnedStatus(t);
+ }
+
+ gSharedTabWarning.tabAdded(t);
+
+ return t;
+ },
+
+ addMultipleTabs(restoreTabsLazily, selectTab, aPropertiesTabs) {
+ let tabs = [];
+ let tabsFragment = document.createDocumentFragment();
+ let tabToSelect = null;
+ let hiddenTabs = new Map();
+ let shouldUpdateForPinnedTabs = false;
+
+ // We create each tab and browser, but only insert them
+ // into a document fragment so that we can insert them all
+ // together. This prevents synch reflow for each tab
+ // insertion.
+ for (var i = 0; i < aPropertiesTabs.length; i++) {
+ let tabData = aPropertiesTabs[i];
+
+ let userContextId = tabData.userContextId;
+ let select = i == selectTab - 1;
+ let tab;
+ let tabWasReused = false;
+
+ // Re-use existing selected tab if possible to avoid the overhead of
+ // selecting a new tab.
+ if (select && this.selectedTab.userContextId == userContextId) {
+ tabWasReused = true;
+ tab = this.selectedTab;
+ if (!tabData.pinned) {
+ this.unpinTab(tab);
+ } else {
+ this.pinTab(tab);
+ }
+ if (gMultiProcessBrowser && !tab.linkedBrowser.isRemoteBrowser) {
+ this.updateBrowserRemoteness(tab.linkedBrowser, {
+ remoteType: E10SUtils.DEFAULT_REMOTE_TYPE,
+ });
+ }
+ }
+
+ // Add a new tab if needed.
+ if (!tab) {
+ let createLazyBrowser =
+ restoreTabsLazily && !select && !tabData.pinned;
+
+ let url = "about:blank";
+ if (createLazyBrowser && tabData.entries && tabData.entries.length) {
+ // Let tabbrowser know the future URI because progress listeners won't
+ // get onLocationChange notification before the browser is inserted.
+ let activeIndex = (tabData.index || tabData.entries.length) - 1;
+ // Ensure the index is in bounds.
+ activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
+ activeIndex = Math.max(activeIndex, 0);
+ url = tabData.entries[activeIndex].url;
+ }
+
+ // Setting noInitialLabel is a perf optimization. Rendering tab labels
+ // would make resizing the tabs more expensive as we're adding them.
+ // Each tab will get its initial label set in restoreTab.
+ tab = this.addTrustedTab(url, {
+ createLazyBrowser,
+ skipAnimation: true,
+ allowInheritPrincipal: true,
+ noInitialLabel: true,
+ userContextId,
+ skipBackgroundNotify: true,
+ bulkOrderedOpen: true,
+ batchInsertingTabs: true,
+ });
+
+ if (select) {
+ tabToSelect = tab;
+ }
+ }
+
+ tabs.push(tab);
+
+ if (tabData.pinned) {
+ // Calling `pinTab` calls `moveTabTo`, which assumes the tab is
+ // inserted in the DOM. If the tab is not yet in the DOM,
+ // just insert it in the right place from the start.
+ if (!tab.parentNode) {
+ tab._tPos = this._numPinnedTabs;
+ this.tabContainer.insertBefore(tab, this.tabs[this._numPinnedTabs]);
+ tab.setAttribute("pinned", "true");
+ this._invalidateCachedTabs();
+ // Then ensure all the tab open/pinning information is sent.
+ this._fireTabOpen(tab, {});
+ this._notifyPinnedStatus(tab);
+ // Once we're done adding all tabs, _updateTabBarForPinnedTabs
+ // needs calling:
+ shouldUpdateForPinnedTabs = true;
+ }
+ } else {
+ if (tab.hidden) {
+ tab.setAttribute("hidden", "true");
+ hiddenTabs.set(tab, tabData.extData && tabData.extData.hiddenBy);
+ }
+
+ tabsFragment.appendChild(tab);
+ if (tabWasReused) {
+ this._invalidateCachedTabs();
+ }
+ }
+
+ tab.initialize();
+ }
+
+ // inject the new DOM nodes
+ this.tabContainer.appendChild(tabsFragment);
+
+ for (let [tab, hiddenBy] of hiddenTabs) {
+ let event = document.createEvent("Events");
+ event.initEvent("TabHide", true, false);
+ tab.dispatchEvent(event);
+ if (hiddenBy) {
+ SessionStore.setCustomTabValue(tab, "hiddenBy", hiddenBy);
+ }
+ }
+
+ this._invalidateCachedTabs();
+ if (shouldUpdateForPinnedTabs) {
+ this._updateTabBarForPinnedTabs();
+ }
+
+ // We need to wait until after all tabs have been appended to the DOM
+ // to remove the old selected tab.
+ if (tabToSelect) {
+ let leftoverTab = this.selectedTab;
+ this.selectedTab = tabToSelect;
+ this.removeTab(leftoverTab);
+ }
+
+ if (tabs.length > 1 || !tabs[0].selected) {
+ this._updateTabsAfterInsert();
+ this.tabContainer._setPositionalAttributes();
+ TabBarVisibility.update();
+
+ for (let tab of tabs) {
+ // If tabToSelect is a tab, we didn't reuse the selected tab.
+ if (tabToSelect || !tab.selected) {
+ // Fire a TabOpen event for all unpinned tabs, except reused selected
+ // tabs.
+ if (!tab.pinned) {
+ this._fireTabOpen(tab, {});
+ }
+
+ // Fire a TabBrowserInserted event on all tabs that have a connected,
+ // real browser, except for reused selected tabs.
+ if (tab.linkedPanel) {
+ var evt = new CustomEvent("TabBrowserInserted", {
+ bubbles: true,
+ detail: { insertedOnTabCreation: true },
+ });
+ tab.dispatchEvent(evt);
+ }
+ }
+ }
+ }
+
+ return tabs;
+ },
+
+ moveTabsToStart(contextTab) {
+ let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
+ // Walk the array in reverse order so the tabs are kept in order.
+ for (let i = tabs.length - 1; i >= 0; i--) {
+ let tab = tabs[i];
+ if (tab._tPos > 0) {
+ this.moveTabTo(tab, 0);
+ }
+ }
+ },
+
+ moveTabsToEnd(contextTab) {
+ let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
+ for (let tab of tabs) {
+ if (tab._tPos < this.tabs.length - 1) {
+ this.moveTabTo(tab, this.tabs.length - 1);
+ }
+ }
+ },
+
+ warnAboutClosingTabs(tabsToClose, aCloseTabs) {
+ if (tabsToClose <= 1) {
+ return true;
+ }
+
+ const pref =
+ aCloseTabs == this.closingTabsEnum.ALL
+ ? "browser.tabs.warnOnClose"
+ : "browser.tabs.warnOnCloseOtherTabs";
+ var shouldPrompt = Services.prefs.getBoolPref(pref);
+ if (!shouldPrompt) {
+ return true;
+ }
+
+ const maxTabsUndo = Services.prefs.getIntPref(
+ "browser.sessionstore.max_tabs_undo"
+ );
+ if (
+ aCloseTabs != this.closingTabsEnum.ALL &&
+ tabsToClose <= maxTabsUndo
+ ) {
+ return true;
+ }
+
+ var ps = Services.prompt;
+
+ // default to true: if it were false, we wouldn't get this far
+ var warnOnClose = { value: true };
+
+ // focus the window before prompting.
+ // this will raise any minimized window, which will
+ // make it obvious which window the prompt is for and will
+ // solve the problem of windows "obscuring" the prompt.
+ // see bug #350299 for more details
+ window.focus();
+ let warningMessage = gTabBrowserBundle.GetStringFromName(
+ "tabs.closeWarningMultiple"
+ );
+ warningMessage = PluralForm.get(tabsToClose, warningMessage).replace(
+ "#1",
+ tabsToClose
+ );
+ let flags =
+ ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0 +
+ ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1;
+ let checkboxLabel =
+ aCloseTabs == this.closingTabsEnum.ALL
+ ? gTabBrowserBundle.GetStringFromName("tabs.closeWarningPromptMe")
+ : null;
+ var buttonPressed = ps.confirmEx(
+ window,
+ gTabBrowserBundle.GetStringFromName("tabs.closeTitleTabs"),
+ warningMessage,
+ flags,
+ gTabBrowserBundle.GetStringFromName("tabs.closeButtonMultiple"),
+ null,
+ null,
+ checkboxLabel,
+ warnOnClose
+ );
+ var reallyClose = buttonPressed == 0;
+
+ // don't set the pref unless they press OK and it's false
+ if (
+ aCloseTabs == this.closingTabsEnum.ALL &&
+ reallyClose &&
+ !warnOnClose.value
+ ) {
+ Services.prefs.setBoolPref(pref, false);
+ }
+
+ return reallyClose;
+ },
+
+ /**
+ * This determines where the tab should be inserted within the tabContainer
+ */
+ _insertTabAtIndex(
+ tab,
+ { index, ownerTab, openerTab, pinned, bulkOrderedOpen } = {}
+ ) {
+ // If this new tab is owned by another, assert that relationship
+ if (ownerTab) {
+ tab.owner = ownerTab;
+ }
+
+ // Ensure we have an index if one was not provided.
+ if (typeof index != "number") {
+ // Move the new tab after another tab if needed.
+ if (
+ !bulkOrderedOpen &&
+ ((openerTab &&
+ Services.prefs.getBoolPref(
+ "browser.tabs.insertRelatedAfterCurrent"
+ )) ||
+ Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent"))
+ ) {
+ let lastRelatedTab =
+ openerTab && this._lastRelatedTabMap.get(openerTab);
+ let previousTab = lastRelatedTab || openerTab || this.selectedTab;
+ if (previousTab.multiselected) {
+ index = this.selectedTabs[this.selectedTabs.length - 1]._tPos + 1;
+ } else {
+ index = previousTab._tPos + 1;
+ }
+
+ if (lastRelatedTab) {
+ lastRelatedTab.owner = null;
+ } else if (openerTab) {
+ tab.owner = openerTab;
+ }
+ // Always set related map if opener exists.
+ if (openerTab) {
+ this._lastRelatedTabMap.set(openerTab, tab);
+ }
+ } else {
+ index = Infinity;
+ }
+ }
+ // Ensure index is within bounds.
+ if (pinned) {
+ index = Math.max(index, 0);
+ index = Math.min(index, this._numPinnedTabs);
+ } else {
+ index = Math.max(index, this._numPinnedTabs);
+ index = Math.min(index, this.tabs.length);
+ }
+
+ let tabAfter = this.tabs[index] || null;
+ this._invalidateCachedTabs();
+ // Prevent a flash of unstyled content by setting up the tab content
+ // and inherited attributes before appending it (see Bug 1592054):
+ tab.initialize();
+ this.tabContainer.insertBefore(tab, tabAfter);
+ if (tabAfter) {
+ this._updateTabsAfterInsert();
+ } else {
+ tab._tPos = index;
+ }
+
+ if (pinned) {
+ this._updateTabBarForPinnedTabs();
+ }
+ this.tabContainer._setPositionalAttributes();
+
+ TabBarVisibility.update();
+ },
+
+ /**
+ * Dispatch a new tab event. This should be called when things are in a
+ * consistent state, such that listeners of this event can again open
+ * or close tabs.
+ */
+ _fireTabOpen(tab, eventDetail) {
+ delete tab.initializingTab;
+ let evt = new CustomEvent("TabOpen", {
+ bubbles: true,
+ detail: eventDetail || {},
+ });
+ tab.dispatchEvent(evt);
+ },
+
+ getTabsToTheEndFrom(aTab) {
+ let tabsToEnd = [];
+ let tabs = this.visibleTabs;
+ for (let i = tabs.length - 1; i >= 0; --i) {
+ if (tabs[i] == aTab || tabs[i].pinned) {
+ break;
+ }
+ // In a multi-select context, select all unselected tabs
+ // starting from the context tab.
+ if (aTab.multiselected && tabs[i].multiselected) {
+ continue;
+ }
+ tabsToEnd.push(tabs[i]);
+ }
+ return tabsToEnd;
+ },
+
+ /**
+ * In a multi-select context, the tabs (except pinned tabs) that are located to the
+ * right of the rightmost selected tab will be removed.
+ */
+ removeTabsToTheEndFrom(aTab) {
+ let tabs = this.getTabsToTheEndFrom(aTab);
+ if (
+ !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)
+ ) {
+ return;
+ }
+
+ this.removeTabs(tabs);
+ },
+
+ /**
+ * In a multi-select context, all unpinned and unselected tabs are removed.
+ * Otherwise all unpinned tabs except aTab are removed.
+ */
+ removeAllTabsBut(aTab) {
+ let tabsToRemove = [];
+ if (aTab && aTab.multiselected) {
+ tabsToRemove = this.visibleTabs.filter(
+ tab => !tab.multiselected && !tab.pinned
+ );
+ } else {
+ tabsToRemove = this.visibleTabs.filter(
+ tab => tab != aTab && !tab.pinned
+ );
+ }
+
+ if (
+ !this.warnAboutClosingTabs(
+ tabsToRemove.length,
+ this.closingTabsEnum.OTHER
+ )
+ ) {
+ return;
+ }
+
+ this.removeTabs(tabsToRemove);
+ },
+
+ removeMultiSelectedTabs() {
+ let selectedTabs = this.selectedTabs;
+ if (
+ !this.warnAboutClosingTabs(
+ selectedTabs.length,
+ this.closingTabsEnum.MULTI_SELECTED
+ )
+ ) {
+ return;
+ }
+
+ this.removeTabs(selectedTabs);
+ },
+
+ removeTabs(
+ tabs,
+ { animate = true, suppressWarnAboutClosingWindow = false } = {}
+ ) {
+ // When 'closeWindowWithLastTab' pref is enabled, closing all tabs
+ // can be considered equivalent to closing the window.
+ if (
+ this.tabs.length == tabs.length &&
+ Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab")
+ ) {
+ window.closeWindow(
+ true,
+ suppressWarnAboutClosingWindow ? null : window.warnAboutClosingWindow
+ );
+ return;
+ }
+
+ let initialTabCount = tabs.length;
+ this._clearMultiSelectionLocked = true;
+
+ // Guarantee that _clearMultiSelectionLocked lock gets released.
+ try {
+ let tabsWithBeforeUnload = [];
+ let lastToClose;
+ let aParams = { animate, prewarmed: true };
+
+ for (let tab of tabs) {
+ if (tab.selected) {
+ lastToClose = tab;
+ let toBlurTo = this._findTabToBlurTo(lastToClose, tabs);
+ if (toBlurTo) {
+ this._getSwitcher().warmupTab(toBlurTo);
+ }
+ } else if (this._hasBeforeUnload(tab)) {
+ tabsWithBeforeUnload.push(tab);
+ } else {
+ this.removeTab(tab, aParams);
+ }
+ }
+ for (let tab of tabsWithBeforeUnload) {
+ this.removeTab(tab, aParams);
+ }
+
+ // Avoid changing the selected browser several times by removing it,
+ // if appropriate, lastly.
+ if (lastToClose) {
+ this.removeTab(lastToClose, aParams);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ this._clearMultiSelectionLocked = false;
+ this.avoidSingleSelectedTab();
+ let closedTabsCount =
+ initialTabCount - tabs.filter(t => t.isConnected && !t.closing).length;
+ // Don't use document.l10n.setAttributes because the FTL file is loaded
+ // lazily and we won't be able to resolve the string.
+ document
+ .getElementById("History:UndoCloseTab")
+ .setAttribute(
+ "data-l10n-args",
+ JSON.stringify({ tabCount: closedTabsCount })
+ );
+ SessionStore.setLastClosedTabCount(window, closedTabsCount);
+ },
+
+ removeCurrentTab(aParams) {
+ this.removeTab(this.selectedTab, aParams);
+ },
+
+ removeTab(
+ aTab,
+ {
+ animate,
+ byMouse,
+ skipPermitUnload,
+ closeWindowWithLastTab,
+ prewarmed,
+ } = {}
+ ) {
+ if (UserInteraction.running("browser.tabs.opening", window)) {
+ UserInteraction.finish("browser.tabs.opening", window);
+ }
+
+ // Telemetry stopwatches may already be running if removeTab gets
+ // called again for an already closing tab.
+ if (
+ !TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_ANIM_MS", aTab) &&
+ !TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab)
+ ) {
+ // Speculatevely start both stopwatches now. We'll cancel one of
+ // the two later depending on whether we're animating.
+ TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
+ TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
+ }
+
+ // Handle requests for synchronously removing an already
+ // asynchronously closing tab.
+ if (!animate && aTab.closing) {
+ this._endRemoveTab(aTab);
+ return;
+ }
+
+ var isLastTab = this.tabs.length - this._removingTabs.length == 1;
+ let windowUtils = window.windowUtils;
+ // We have to sample the tab width now, since _beginRemoveTab might
+ // end up modifying the DOM in such a way that aTab gets a new
+ // frame created for it (for example, by updating the visually selected
+ // state).
+ let tabWidth = windowUtils.getBoundsWithoutFlushing(aTab).width;
+
+ if (
+ !this._beginRemoveTab(aTab, {
+ closeWindowFastpath: true,
+ skipPermitUnload,
+ closeWindowWithLastTab,
+ prewarmed,
+ })
+ ) {
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
+ return;
+ }
+
+ if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse) {
+ this.tabContainer._lockTabSizing(aTab, tabWidth);
+ } else {
+ this.tabContainer._unlockTabSizing();
+ }
+
+ if (
+ !animate /* the caller didn't opt in */ ||
+ gReduceMotion ||
+ isLastTab ||
+ aTab.pinned ||
+ aTab.hidden ||
+ this._removingTabs.length >
+ 3 /* don't want lots of concurrent animations */ ||
+ aTab.getAttribute("fadein") !=
+ "true" /* fade-in transition hasn't been triggered yet */ ||
+ window.getComputedStyle(aTab).maxWidth ==
+ "0.1px" /* fade-in transition hasn't moved yet */
+ ) {
+ // We're not animating, so we can cancel the animation stopwatch.
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
+ this._endRemoveTab(aTab);
+ return;
+ }
+
+ // We're animating, so we can cancel the non-animation stopwatch.
+ TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
+
+ aTab.style.maxWidth = ""; // ensure that fade-out transition happens
+ aTab.removeAttribute("fadein");
+ aTab.removeAttribute("bursting");
+
+ setTimeout(
+ function(tab, tabbrowser) {
+ if (
+ tab.container &&
+ window.getComputedStyle(tab).maxWidth == "0.1px"
+ ) {
+ console.assert(
+ false,
+ "Giving up waiting for the tab closing animation to finish (bug 608589)"
+ );
+ tabbrowser._endRemoveTab(tab);
+ }
+ },
+ 3000,
+ aTab,
+ this
+ );
+ },
+
+ _hasBeforeUnload(aTab) {
+ let browser = aTab.linkedBrowser;
+ if (browser.isRemoteBrowser && browser.frameLoader) {
+ return browser.hasBeforeUnload;
+ }
+ return false;
+ },
+
+ _beginRemoveTab(
+ aTab,
+ {
+ adoptedByTab,
+ closeWindowWithLastTab,
+ closeWindowFastpath,
+ skipPermitUnload,
+ prewarmed,
+ } = {}
+ ) {
+ if (aTab.closing || this._windowIsClosing) {
+ return false;
+ }
+
+ var browser = this.getBrowserForTab(aTab);
+ if (
+ !skipPermitUnload &&
+ !adoptedByTab &&
+ aTab.linkedPanel &&
+ !aTab._pendingPermitUnload &&
+ (!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))
+ ) {
+ if (!prewarmed) {
+ let blurTab = this._findTabToBlurTo(aTab);
+ if (blurTab) {
+ this.warmupTab(blurTab);
+ }
+ }
+
+ TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
+
+ // We need to block while calling permitUnload() because it
+ // processes the event queue and may lead to another removeTab()
+ // call before permitUnload() returns.
+ aTab._pendingPermitUnload = true;
+ let { permitUnload } = browser.permitUnload();
+ aTab._pendingPermitUnload = false;
+
+ TelemetryStopwatch.finish("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
+
+ // If we were closed during onbeforeunload, we return false now
+ // so we don't (try to) close the same tab again. Of course, we
+ // also stop if the unload was cancelled by the user:
+ if (aTab.closing || !permitUnload) {
+ return false;
+ }
+ }
+
+ // this._switcher would normally cover removing a tab from this
+ // cache, but we may not have one at this time.
+ let tabCacheIndex = this._tabLayerCache.indexOf(aTab);
+ if (tabCacheIndex != -1) {
+ this._tabLayerCache.splice(tabCacheIndex, 1);
+ }
+
+ // Delay hiding the the active tab if we're screen sharing.
+ // See Bug 1642747.
+ let screenShareInActiveTab =
+ aTab == this.selectedTab && aTab._sharingState?.webRTC?.screen;
+
+ if (!screenShareInActiveTab) {
+ this._blurTab(aTab);
+ }
+
+ var closeWindow = false;
+ var newTab = false;
+ if (this.tabs.length - this._removingTabs.length == 1) {
+ closeWindow =
+ closeWindowWithLastTab != null
+ ? closeWindowWithLastTab
+ : !window.toolbar.visible ||
+ Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
+
+ if (closeWindow) {
+ // We've already called beforeunload on all the relevant tabs if we get here,
+ // so avoid calling it again:
+ window.skipNextCanClose = true;
+ }
+
+ // Closing the tab and replacing it with a blank one is notably slower
+ // than closing the window right away. If the caller opts in, take
+ // the fast path.
+ if (closeWindow && closeWindowFastpath && !this._removingTabs.length) {
+ // This call actually closes the window, unless the user
+ // cancels the operation. We are finished here in both cases.
+ this._windowIsClosing = window.closeWindow(
+ true,
+ window.warnAboutClosingWindow
+ );
+ return false;
+ }
+
+ newTab = true;
+ }
+ aTab._endRemoveArgs = [closeWindow, newTab];
+
+ // swapBrowsersAndCloseOther will take care of closing the window without animation.
+ if (closeWindow && adoptedByTab) {
+ // Remove the tab's filter and progress listener to avoid leaking.
+ if (aTab.linkedPanel) {
+ const filter = this._tabFilters.get(aTab);
+ browser.webProgress.removeProgressListener(filter);
+ const listener = this._tabListeners.get(aTab);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+ this._tabListeners.delete(aTab);
+ this._tabFilters.delete(aTab);
+ }
+ return true;
+ }
+
+ if (!aTab._fullyOpen) {
+ // If the opening tab animation hasn't finished before we start closing the
+ // tab, decrement the animation count since _handleNewTab will not get called.
+ this.tabAnimationsInProgress--;
+ }
+
+ this.tabAnimationsInProgress++;
+
+ // Mute audio immediately to improve perceived speed of tab closure.
+ if (!adoptedByTab && aTab.hasAttribute("soundplaying")) {
+ // Don't persist the muted state as this wasn't a user action.
+ // This lets undo-close-tab return it to an unmuted state.
+ aTab.linkedBrowser.mute(true);
+ }
+
+ aTab.closing = true;
+ this._removingTabs.push(aTab);
+ this._invalidateCachedTabs();
+
+ // Invalidate hovered tab state tracking for this closing tab.
+ if (this.tabContainer._hoveredTab == aTab) {
+ aTab._mouseleave();
+ }
+
+ if (newTab) {
+ this.addTrustedTab(BROWSER_NEW_TAB_URL, {
+ skipAnimation: true,
+ });
+ } else {
+ TabBarVisibility.update();
+ }
+
+ // Splice this tab out of any lines of succession before any events are
+ // dispatched.
+ this.replaceInSuccession(aTab, aTab.successor);
+ this.setSuccessor(aTab, null);
+
+ // We're committed to closing the tab now.
+ // Dispatch a notification.
+ // We dispatch it before any teardown so that event listeners can
+ // inspect the tab that's about to close.
+ let evt = new CustomEvent("TabClose", {
+ bubbles: true,
+ detail: { adoptedBy: adoptedByTab },
+ });
+ aTab.dispatchEvent(evt);
+
+ if (this.tabs.length == 2) {
+ // We're closing one of our two open tabs, inform the other tab that its
+ // sibling is going away.
+ this.tabs[0].linkedBrowser.sendMessageToActor(
+ "Browser:HasSiblings",
+ false,
+ "BrowserTab"
+ );
+ this.tabs[1].linkedBrowser.sendMessageToActor(
+ "Browser:HasSiblings",
+ false,
+ "BrowserTab"
+ );
+ }
+
+ if (aTab.linkedPanel) {
+ if (!adoptedByTab && !gMultiProcessBrowser) {
+ // Prevent this tab from showing further dialogs, since we're closing it
+ browser.contentWindow.windowUtils.disableDialogs();
+ }
+
+ // Remove the tab's filter and progress listener.
+ const filter = this._tabFilters.get(aTab);
+
+ browser.webProgress.removeProgressListener(filter);
+
+ const listener = this._tabListeners.get(aTab);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+ }
+
+ if (browser.registeredOpenURI && !adoptedByTab) {
+ let userContextId = browser.getAttribute("usercontextid") || 0;
+ this.UrlbarProviderOpenTabs.unregisterOpenTab(
+ browser.registeredOpenURI.spec,
+ userContextId
+ );
+ delete browser.registeredOpenURI;
+ }
+
+ // We are no longer the primary content area.
+ browser.removeAttribute("primary");
+
+ // Remove this tab as the owner of any other tabs, since it's going away.
+ for (let tab of this.tabs) {
+ if ("owner" in tab && tab.owner == aTab) {
+ // |tab| is a child of the tab we're removing, make it an orphan
+ tab.owner = null;
+ }
+ }
+
+ return true;
+ },
+
+ _endRemoveTab(aTab) {
+ if (!aTab || !aTab._endRemoveArgs) {
+ return;
+ }
+
+ var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
+ aTab._endRemoveArgs = null;
+
+ if (this._windowIsClosing) {
+ aCloseWindow = false;
+ aNewTab = false;
+ }
+
+ this.tabAnimationsInProgress--;
+
+ this._lastRelatedTabMap = new WeakMap();
+
+ // update the UI early for responsiveness
+ aTab.collapsed = true;
+ this._blurTab(aTab);
+
+ this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1);
+
+ if (aCloseWindow) {
+ this._windowIsClosing = true;
+ while (this._removingTabs.length) {
+ this._endRemoveTab(this._removingTabs[0]);
+ }
+ } else if (!this._windowIsClosing) {
+ if (aNewTab) {
+ gURLBar.select();
+ }
+
+ // workaround for bug 345399
+ this.tabContainer.arrowScrollbox._updateScrollButtonsDisabledState();
+ }
+
+ // We're going to remove the tab and the browser now.
+ this._tabFilters.delete(aTab);
+ this._tabListeners.delete(aTab);
+
+ var browser = this.getBrowserForTab(aTab);
+
+ if (aTab.linkedPanel) {
+ // Because of the fact that we are setting JS properties on
+ // the browser elements, and we have code in place
+ // to preserve the JS objects for any elements that have
+ // JS properties set on them, the browser element won't be
+ // destroyed until the document goes away. So we force a
+ // cleanup ourselves.
+ // This has to happen before we remove the child since functions
+ // like `getBrowserContainer` expect the browser to be parented.
+ browser.destroy();
+ }
+
+ var wasPinned = aTab.pinned;
+
+ // Remove the tab ...
+ aTab.remove();
+ this._invalidateCachedTabs();
+
+ // Update hashiddentabs if this tab was hidden.
+ if (aTab.hidden) {
+ this.tabContainer._updateHiddenTabsStatus();
+ }
+
+ // ... and fix up the _tPos properties immediately.
+ for (let i = aTab._tPos; i < this.tabs.length; i++) {
+ this.tabs[i]._tPos = i;
+ }
+
+ if (!this._windowIsClosing) {
+ if (wasPinned) {
+ this.tabContainer._positionPinnedTabs();
+ }
+
+ // update tab close buttons state
+ this.tabContainer._updateCloseButtons();
+
+ setTimeout(
+ function(tabs) {
+ tabs._lastTabClosedByMouse = false;
+ },
+ 0,
+ this.tabContainer
+ );
+ }
+
+ // update tab positional properties and attributes
+ this.selectedTab._selected = true;
+ this.tabContainer._setPositionalAttributes();
+
+ // Removing the panel requires fixing up selectedPanel immediately
+ // (see below), which would be hindered by the potentially expensive
+ // browser removal. So we remove the browser and the panel in two
+ // steps.
+
+ var panel = this.getPanel(browser);
+
+ // In the multi-process case, it's possible an asynchronous tab switch
+ // is still underway. If so, then it's possible that the last visible
+ // browser is the one we're in the process of removing. There's the
+ // risk of displaying preloaded browsers that are at the end of the
+ // deck if we remove the browser before the switch is complete, so
+ // we alert the switcher in order to show a spinner instead.
+ if (this._switcher) {
+ this._switcher.onTabRemoved(aTab);
+ }
+
+ // This will unload the document. An unload handler could remove
+ // dependant tabs, so it's important that the tabbrowser is now in
+ // a consistent state (tab removed, tab positions updated, etc.).
+ browser.remove();
+
+ // Release the browser in case something is erroneously holding a
+ // reference to the tab after its removal.
+ this._tabForBrowser.delete(aTab.linkedBrowser);
+ aTab.linkedBrowser = null;
+
+ panel.remove();
+
+ // closeWindow might wait an arbitrary length of time if we're supposed
+ // to warn about closing the window, so we'll just stop the tab close
+ // stopwatches here instead.
+ TelemetryStopwatch.finish(
+ "FX_TAB_CLOSE_TIME_ANIM_MS",
+ aTab,
+ true /* aCanceledOkay */
+ );
+ TelemetryStopwatch.finish(
+ "FX_TAB_CLOSE_TIME_NO_ANIM_MS",
+ aTab,
+ true /* aCanceledOkay */
+ );
+
+ if (aCloseWindow) {
+ this._windowIsClosing = closeWindow(
+ true,
+ window.warnAboutClosingWindow
+ );
+ }
+ },
+
+ /**
+ * Finds the tab that we will blur to if we blur aTab.
+ * @param aTab
+ * The tab we would blur
+ * @param aExcludeTabs
+ * Tabs to exclude from our search (i.e., because they are being
+ * closed along with aTab)
+ */
+ _findTabToBlurTo(aTab, aExcludeTabs = []) {
+ if (!aTab.selected) {
+ return null;
+ }
+
+ let excludeTabs = new Set(aExcludeTabs);
+
+ // If this tab has a successor, it should be selectable, since
+ // hiding or closing a tab removes that tab as a successor.
+ if (aTab.successor && !excludeTabs.has(aTab.successor)) {
+ return aTab.successor;
+ }
+
+ if (
+ aTab.owner &&
+ !aTab.owner.hidden &&
+ !aTab.owner.closing &&
+ !excludeTabs.has(aTab.owner) &&
+ Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
+ ) {
+ return aTab.owner;
+ }
+
+ // Switch to a visible tab unless there aren't any others remaining
+ let remainingTabs = this.visibleTabs;
+ let numTabs = remainingTabs.length;
+ if (numTabs == 0 || (numTabs == 1 && remainingTabs[0] == aTab)) {
+ remainingTabs = Array.prototype.filter.call(
+ this.tabs,
+ tab => !tab.closing && !excludeTabs.has(tab)
+ );
+ }
+
+ // Try to find a remaining tab that comes after the given tab
+ let tab = this.tabContainer.findNextTab(aTab, {
+ direction: 1,
+ filter: _tab => remainingTabs.includes(_tab),
+ });
+
+ if (!tab) {
+ tab = this.tabContainer.findNextTab(aTab, {
+ direction: -1,
+ filter: _tab => remainingTabs.includes(_tab),
+ });
+ }
+
+ return tab;
+ },
+
+ _blurTab(aTab) {
+ this.selectedTab = this._findTabToBlurTo(aTab);
+ },
+
+ /**
+ * @returns {boolean}
+ * False if swapping isn't permitted, true otherwise.
+ */
+ swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (
+ PrivateBrowsingUtils.isWindowPrivate(window) !=
+ PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal)
+ ) {
+ return false;
+ }
+
+ // Do not allow transfering a useRemoteSubframes tab to a
+ // non-useRemoteSubframes window and vice versa.
+ if (gFissionBrowser != aOtherTab.ownerGlobal.gFissionBrowser) {
+ return false;
+ }
+
+ let ourBrowser = this.getBrowserForTab(aOurTab);
+ let otherBrowser = aOtherTab.linkedBrowser;
+
+ // Can't swap between chrome and content processes.
+ if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) {
+ return false;
+ }
+
+ // Keep the userContextId if set on other browser
+ if (otherBrowser.hasAttribute("usercontextid")) {
+ ourBrowser.setAttribute(
+ "usercontextid",
+ otherBrowser.getAttribute("usercontextid")
+ );
+ }
+
+ // That's gBrowser for the other window, not the tab's browser!
+ var remoteBrowser = aOtherTab.ownerGlobal.gBrowser;
+ var isPending = aOtherTab.hasAttribute("pending");
+
+ let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
+ let stateFlags = 0;
+ if (otherTabListener) {
+ stateFlags = otherTabListener.mStateFlags;
+ }
+
+ // Expedite the removal of the icon if it was already scheduled.
+ if (aOtherTab._soundPlayingAttrRemovalTimer) {
+ clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
+ aOtherTab._soundPlayingAttrRemovalTimer = 0;
+ aOtherTab.removeAttribute("soundplaying");
+ remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
+ }
+
+ // First, start teardown of the other browser. Make sure to not
+ // fire the beforeunload event in the process. Close the other
+ // window if this was its last tab.
+ if (
+ !remoteBrowser._beginRemoveTab(aOtherTab, {
+ adoptedByTab: aOurTab,
+ closeWindowWithLastTab: true,
+ })
+ ) {
+ return false;
+ }
+
+ // If this is the last tab of the window, hide the window
+ // immediately without animation before the docshell swap, to avoid
+ // about:blank being painted.
+ let [closeWindow] = aOtherTab._endRemoveArgs;
+ if (closeWindow) {
+ let win = aOtherTab.ownerGlobal;
+ win.windowUtils.suppressAnimation(true);
+ // Only suppressing window animations isn't enough to avoid
+ // an empty content area being painted.
+ let baseWin = win.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
+ baseWin.visibility = false;
+ }
+
+ let modifiedAttrs = [];
+ if (aOtherTab.hasAttribute("muted")) {
+ aOurTab.setAttribute("muted", "true");
+ aOurTab.muteReason = aOtherTab.muteReason;
+ ourBrowser.mute();
+ modifiedAttrs.push("muted");
+ }
+ if (aOtherTab.hasAttribute("soundplaying")) {
+ aOurTab.setAttribute("soundplaying", "true");
+ modifiedAttrs.push("soundplaying");
+ }
+ if (aOtherTab.hasAttribute("usercontextid")) {
+ aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
+ modifiedAttrs.push("usercontextid");
+ }
+ if (aOtherTab.hasAttribute("sharing")) {
+ aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
+ modifiedAttrs.push("sharing");
+ aOurTab._sharingState = aOtherTab._sharingState;
+ webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
+ }
+
+ SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);
+
+ // If the other tab is pending (i.e. has not been restored, yet)
+ // then do not switch docShells but retrieve the other tab's state
+ // and apply it to our tab.
+ if (isPending) {
+ SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
+
+ // Make sure to unregister any open URIs.
+ this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
+ } else {
+ // Workarounds for bug 458697
+ // Icon might have been set on DOMLinkAdded, don't override that.
+ if (!ourBrowser.mIconURL && otherBrowser.mIconURL) {
+ this.setIcon(aOurTab, otherBrowser.mIconURL);
+ }
+ var isBusy = aOtherTab.hasAttribute("busy");
+ if (isBusy) {
+ aOurTab.setAttribute("busy", "true");
+ modifiedAttrs.push("busy");
+ if (aOurTab.selected) {
+ this._isBusy = true;
+ }
+ }
+
+ this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags);
+ }
+
+ // Unregister the previously opened URI
+ if (otherBrowser.registeredOpenURI) {
+ let userContextId = otherBrowser.getAttribute("usercontextid") || 0;
+ this.UrlbarProviderOpenTabs.unregisterOpenTab(
+ otherBrowser.registeredOpenURI.spec,
+ userContextId
+ );
+ delete otherBrowser.registeredOpenURI;
+ }
+
+ // Handle findbar data (if any)
+ let otherFindBar = aOtherTab._findBar;
+ if (otherFindBar && otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
+ let oldValue = otherFindBar._findField.value;
+ let wasHidden = otherFindBar.hidden;
+ let ourFindBarPromise = this.getFindBar(aOurTab);
+ ourFindBarPromise.then(ourFindBar => {
+ if (!ourFindBar) {
+ return;
+ }
+ ourFindBar._findField.value = oldValue;
+ if (!wasHidden) {
+ ourFindBar.onFindCommand();
+ }
+ });
+ }
+
+ // Finish tearing down the tab that's going away.
+ if (closeWindow) {
+ aOtherTab.ownerGlobal.close();
+ } else {
+ remoteBrowser._endRemoveTab(aOtherTab);
+ }
+
+ this.setTabTitle(aOurTab);
+
+ // If the tab was already selected (this happens in the scenario
+ // of replaceTabWithWindow), notify onLocationChange, etc.
+ if (aOurTab.selected) {
+ this.updateCurrentBrowser(true);
+ }
+
+ if (modifiedAttrs.length) {
+ this._tabAttrModified(aOurTab, modifiedAttrs);
+ }
+
+ return true;
+ },
+
+ swapBrowsers(aOurTab, aOtherTab) {
+ let otherBrowser = aOtherTab.linkedBrowser;
+ let otherTabBrowser = otherBrowser.getTabBrowser();
+
+ // We aren't closing the other tab so, we also need to swap its tablisteners.
+ let filter = otherTabBrowser._tabFilters.get(aOtherTab);
+ let tabListener = otherTabBrowser._tabListeners.get(aOtherTab);
+ otherBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(tabListener);
+
+ // Perform the docshell swap through the common mechanism.
+ this._swapBrowserDocShells(aOurTab, otherBrowser);
+
+ // Restore the listeners for the swapped in tab.
+ tabListener = new otherTabBrowser.ownerGlobal.TabProgressListener(
+ aOtherTab,
+ otherBrowser,
+ false,
+ false
+ );
+ otherTabBrowser._tabListeners.set(aOtherTab, tabListener);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ otherBrowser.webProgress.addProgressListener(filter, notifyAll);
+ },
+
+ _swapBrowserDocShells(aOurTab, aOtherBrowser, aStateFlags) {
+ // aOurTab's browser needs to be inserted now if it hasn't already.
+ this._insertBrowser(aOurTab);
+
+ // Unhook our progress listener
+ const filter = this._tabFilters.get(aOurTab);
+ let tabListener = this._tabListeners.get(aOurTab);
+ let ourBrowser = this.getBrowserForTab(aOurTab);
+ ourBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(tabListener);
+
+ // Make sure to unregister any open URIs.
+ this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
+
+ let remoteBrowser = aOtherBrowser.ownerGlobal.gBrowser;
+
+ // If switcher is active, it will intercept swap events and
+ // react as needed.
+ if (!this._switcher) {
+ aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(
+ ourBrowser
+ );
+ }
+
+ // Swap the docshells
+ ourBrowser.swapDocShells(aOtherBrowser);
+
+ // Swap permanentKey properties.
+ let ourPermanentKey = ourBrowser.permanentKey;
+ ourBrowser.permanentKey = aOtherBrowser.permanentKey;
+ aOtherBrowser.permanentKey = ourPermanentKey;
+ aOurTab.permanentKey = ourBrowser.permanentKey;
+ if (remoteBrowser) {
+ let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
+ if (otherTab) {
+ otherTab.permanentKey = aOtherBrowser.permanentKey;
+ }
+ }
+
+ // Restore the progress listener
+ tabListener = new TabProgressListener(
+ aOurTab,
+ ourBrowser,
+ false,
+ false,
+ aStateFlags
+ );
+ this._tabListeners.set(aOurTab, tabListener);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ ourBrowser.webProgress.addProgressListener(filter, notifyAll);
+ },
+
+ _swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) {
+ // Swap the registeredOpenURI properties of the two browsers
+ let tmp = aOurBrowser.registeredOpenURI;
+ delete aOurBrowser.registeredOpenURI;
+ if (aOtherBrowser.registeredOpenURI) {
+ aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
+ delete aOtherBrowser.registeredOpenURI;
+ }
+ if (tmp) {
+ aOtherBrowser.registeredOpenURI = tmp;
+ }
+ },
+
+ announceWindowCreated(browser, userContextId) {
+ let tab = this.getTabForBrowser(browser);
+ if (tab) {
+ if (userContextId) {
+ ContextualIdentityService.telemetry(userContextId);
+ tab.setUserContextId(userContextId);
+ }
+
+ browser.sendMessageToActor(
+ "Browser:AppTab",
+ { isAppTab: tab.pinned },
+ "BrowserTab"
+ );
+ }
+
+ // We don't want to update the container icon and identifier if
+ // this is not the selected browser.
+ if (browser == gBrowser.selectedBrowser) {
+ updateUserContextUIIndicator();
+ }
+ },
+
+ reloadMultiSelectedTabs() {
+ this.reloadTabs(this.selectedTabs);
+ },
+
+ reloadTabs(tabs) {
+ for (let tab of tabs) {
+ try {
+ this.getBrowserForTab(tab).reload();
+ } catch (e) {
+ // ignore failure to reload so others will be reloaded
+ }
+ }
+ },
+
+ reloadTab(aTab) {
+ let browser = this.getBrowserForTab(aTab);
+ // Reset temporary permissions on the current tab. This is done here
+ // because we only want to reset permissions on user reload.
+ SitePermissions.clearTemporaryPermissions(browser);
+ // Also reset DOS mitigations for the basic auth prompt on reload.
+ delete browser.authPromptAbuseCounter;
+ gIdentityHandler.hidePopup();
+ browser.reload();
+ },
+
+ addProgressListener(aListener) {
+ if (arguments.length != 1) {
+ Cu.reportError(
+ "gBrowser.addProgressListener was " +
+ "called with a second argument, " +
+ "which is not supported. See bug " +
+ "608628. Call stack: " +
+ new Error().stack
+ );
+ }
+
+ this.mProgressListeners.push(aListener);
+ },
+
+ removeProgressListener(aListener) {
+ this.mProgressListeners = this.mProgressListeners.filter(
+ l => l != aListener
+ );
+ },
+
+ addTabsProgressListener(aListener) {
+ this.mTabsProgressListeners.push(aListener);
+ },
+
+ removeTabsProgressListener(aListener) {
+ this.mTabsProgressListeners = this.mTabsProgressListeners.filter(
+ l => l != aListener
+ );
+ },
+
+ getBrowserForTab(aTab) {
+ return aTab.linkedBrowser;
+ },
+
+ showOnlyTheseTabs(aTabs) {
+ for (let tab of this.tabs) {
+ if (!aTabs.includes(tab)) {
+ this.hideTab(tab);
+ } else {
+ this.showTab(tab);
+ }
+ }
+
+ this.tabContainer._updateHiddenTabsStatus();
+ this.tabContainer._handleTabSelect(true);
+ },
+
+ showTab(aTab) {
+ if (aTab.hidden) {
+ aTab.removeAttribute("hidden");
+ this._invalidateCachedTabs();
+
+ this.tabContainer._updateCloseButtons();
+ this.tabContainer._updateHiddenTabsStatus();
+
+ this.tabContainer._setPositionalAttributes();
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabShow", true, false);
+ aTab.dispatchEvent(event);
+ SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
+ }
+ },
+
+ hideTab(aTab, aSource) {
+ if (
+ aTab.hidden ||
+ aTab.pinned ||
+ aTab.selected ||
+ aTab.closing ||
+ // Tabs that are sharing the screen, microphone or camera cannot be hidden.
+ (aTab._sharingState && aTab._sharingState.webRTC)
+ ) {
+ return;
+ }
+ aTab.setAttribute("hidden", "true");
+ this._invalidateCachedTabs();
+
+ this.tabContainer._updateCloseButtons();
+ this.tabContainer._updateHiddenTabsStatus();
+
+ this.tabContainer._setPositionalAttributes();
+
+ // Splice this tab out of any lines of succession before any events are
+ // dispatched.
+ this.replaceInSuccession(aTab, aTab.successor);
+ this.setSuccessor(aTab, null);
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabHide", true, false);
+ aTab.dispatchEvent(event);
+ if (aSource) {
+ SessionStore.setCustomTabValue(aTab, "hiddenBy", aSource);
+ }
+ },
+
+ selectTabAtIndex(aIndex, aEvent) {
+ let tabs = this.visibleTabs;
+
+ // count backwards for aIndex < 0
+ if (aIndex < 0) {
+ aIndex += tabs.length;
+ // clamp at index 0 if still negative.
+ if (aIndex < 0) {
+ aIndex = 0;
+ }
+ } else if (aIndex >= tabs.length) {
+ // clamp at right-most tab if out of range.
+ aIndex = tabs.length - 1;
+ }
+
+ this.selectedTab = tabs[aIndex];
+
+ if (aEvent) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ },
+
+ /**
+ * Moves a tab to a new browser window, unless it's already the only tab
+ * in the current window, in which case this will do nothing.
+ */
+ replaceTabWithWindow(aTab, aOptions) {
+ if (this.tabs.length == 1) {
+ return null;
+ }
+
+ var options = "chrome,dialog=no,all";
+ for (var name in aOptions) {
+ options += "," + name + "=" + aOptions[name];
+ }
+
+ // Play the tab closing animation to give immediate feedback while
+ // waiting for the new window to appear.
+ // content area when the docshells are swapped.
+ if (!gReduceMotion) {
+ aTab.style.maxWidth = ""; // ensure that fade-out transition happens
+ aTab.removeAttribute("fadein");
+ }
+
+ // tell a new window to take the "dropped" tab
+ return window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ options,
+ aTab
+ );
+ },
+
+ /**
+ * Move contextTab (or selected tabs in a mutli-select context)
+ * to a new browser window, unless it is (they are) already the only tab(s)
+ * in the current window, in which case this will do nothing.
+ */
+ replaceTabsWithWindow(contextTab, aOptions) {
+ let tabs;
+ if (contextTab.multiselected) {
+ tabs = this.selectedTabs;
+ } else {
+ tabs = [contextTab];
+ }
+
+ if (this.tabs.length == tabs.length) {
+ return null;
+ }
+
+ if (tabs.length == 1) {
+ return this.replaceTabWithWindow(tabs[0], aOptions);
+ }
+
+ // Play the closing animation for all selected tabs to give
+ // immediate feedback while waiting for the new window to appear.
+ if (!gReduceMotion) {
+ for (let tab of tabs) {
+ tab.style.maxWidth = ""; // ensure that fade-out transition happens
+ tab.removeAttribute("fadein");
+ }
+ }
+
+ // Create a new window and make it adopt the tabs, preserving their relative order.
+ // The initial tab of the new window will be selected, so it should adopt the
+ // selected tab of the original window, if applicable, or else the first moving tab.
+ // This avoids tab-switches in the new window, preserving tab laziness.
+ // However, to avoid multiple tab-switches in the original window, the other tabs
+ // should be adopted before the selected one.
+ let selectedTabIndex = Math.max(0, tabs.indexOf(gBrowser.selectedTab));
+ let selectedTab = tabs[selectedTabIndex];
+ let win = this.replaceTabWithWindow(selectedTab, aOptions);
+ win.addEventListener(
+ "before-initial-tab-adopted",
+ () => {
+ for (let i = 0; i < tabs.length; ++i) {
+ if (i != selectedTabIndex) {
+ win.gBrowser.adoptTab(tabs[i], i);
+ }
+ }
+ // Restore tab selection
+ let winVisibleTabs = win.gBrowser.visibleTabs;
+ let winTabLength = winVisibleTabs.length;
+ win.gBrowser.addRangeToMultiSelectedTabs(
+ winVisibleTabs[0],
+ winVisibleTabs[winTabLength - 1]
+ );
+ win.gBrowser.lockClearMultiSelectionOnce();
+ },
+ { once: true }
+ );
+ return win;
+ },
+
+ _updateTabsAfterInsert() {
+ for (let i = 0; i < this.tabs.length; i++) {
+ this.tabs[i]._tPos = i;
+ this.tabs[i]._selected = false;
+ }
+
+ // If we're in the midst of an async tab switch while calling
+ // moveTabTo, we can get into a case where _visuallySelected
+ // is set to true on two different tabs.
+ //
+ // What we want to do in moveTabTo is to remove logical selection
+ // from all tabs, and then re-add logical selection to selectedTab
+ // (and visual selection as well if we're not running with e10s, which
+ // setting _selected will do automatically).
+ //
+ // If we're running with e10s, then the visual selection will not
+ // be changed, which is fine, since if we weren't in the midst of a
+ // tab switch, the previously visually selected tab should still be
+ // correct, and if we are in the midst of a tab switch, then the async
+ // tab switcher will set the visually selected tab once the tab switch
+ // has completed.
+ this.selectedTab._selected = true;
+ },
+
+ moveTabTo(aTab, aIndex, aKeepRelatedTabs) {
+ var oldPosition = aTab._tPos;
+ if (oldPosition == aIndex) {
+ return;
+ }
+
+ // Don't allow mixing pinned and unpinned tabs.
+ if (aTab.pinned) {
+ aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
+ } else {
+ aIndex = Math.max(aIndex, this._numPinnedTabs);
+ }
+ if (oldPosition == aIndex) {
+ return;
+ }
+
+ if (!aKeepRelatedTabs) {
+ this._lastRelatedTabMap = new WeakMap();
+ }
+
+ let wasFocused = document.activeElement == this.selectedTab;
+
+ aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1;
+
+ let neighbor = this.tabs[aIndex] || null;
+ this._invalidateCachedTabs();
+ this.tabContainer.insertBefore(aTab, neighbor);
+ this._updateTabsAfterInsert();
+
+ if (wasFocused) {
+ this.selectedTab.focus();
+ }
+
+ this.tabContainer._handleTabSelect(true);
+
+ if (aTab.pinned) {
+ this.tabContainer._positionPinnedTabs();
+ }
+
+ this.tabContainer._setPositionalAttributes();
+
+ var evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabMove", true, false, window, oldPosition);
+ aTab.dispatchEvent(evt);
+ },
+
+ moveTabForward() {
+ let nextTab = this.tabContainer.findNextTab(this.selectedTab, {
+ direction: 1,
+ filter: tab => !tab.hidden,
+ });
+
+ if (nextTab) {
+ this.moveTabTo(this.selectedTab, nextTab._tPos);
+ } else if (this.arrowKeysShouldWrap) {
+ this.moveTabToStart();
+ }
+ },
+
+ /**
+ * Adopts a tab from another browser window, and inserts it at aIndex
+ *
+ * @returns {object}
+ * The new tab in the current window, null if the tab couldn't be adopted.
+ */
+ adoptTab(aTab, aIndex, aSelectTab) {
+ // Swap the dropped tab with a new one we create and then close
+ // it in the other window (making it seem to have moved between
+ // windows). We also ensure that the tab we create to swap into has
+ // the same remote type and process as the one we're swapping in.
+ // This makes sure we don't get a short-lived process for the new tab.
+ let linkedBrowser = aTab.linkedBrowser;
+ let createLazyBrowser = !aTab.linkedPanel;
+ let params = {
+ eventDetail: { adoptedTab: aTab },
+ preferredRemoteType: linkedBrowser.remoteType,
+ initialBrowsingContextGroupId: linkedBrowser.browsingContext?.group.id,
+ skipAnimation: true,
+ index: aIndex,
+ createLazyBrowser,
+ allowInheritPrincipal: createLazyBrowser,
+ };
+
+ let numPinned = this._numPinnedTabs;
+ if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
+ params.pinned = true;
+ }
+
+ if (aTab.hasAttribute("usercontextid")) {
+ // new tab must have the same usercontextid as the old one
+ params.userContextId = aTab.getAttribute("usercontextid");
+ }
+ let newTab = this.addWebTab("about:blank", params);
+ let newBrowser = this.getBrowserForTab(newTab);
+
+ aTab.container._finishAnimateTabMove();
+
+ if (!createLazyBrowser) {
+ // Stop the about:blank load.
+ newBrowser.stop();
+ // Make sure it has a docshell.
+ newBrowser.docShell;
+ }
+
+ if (!this.swapBrowsersAndCloseOther(newTab, aTab)) {
+ // Swapping wasn't permitted. Bail out.
+ this.removeTab(newTab);
+ return null;
+ }
+
+ if (aSelectTab) {
+ this.selectedTab = newTab;
+ }
+
+ return newTab;
+ },
+
+ moveTabBackward() {
+ let previousTab = this.tabContainer.findNextTab(this.selectedTab, {
+ direction: -1,
+ filter: tab => !tab.hidden,
+ });
+
+ if (previousTab) {
+ this.moveTabTo(this.selectedTab, previousTab._tPos);
+ } else if (this.arrowKeysShouldWrap) {
+ this.moveTabToEnd();
+ }
+ },
+
+ moveTabToStart() {
+ let tabPos = this.selectedTab._tPos;
+ if (tabPos > 0) {
+ this.moveTabTo(this.selectedTab, 0);
+ }
+ },
+
+ moveTabToEnd() {
+ let tabPos = this.selectedTab._tPos;
+ if (tabPos < this.browsers.length - 1) {
+ this.moveTabTo(this.selectedTab, this.browsers.length - 1);
+ }
+ },
+
+ moveTabOver(aEvent) {
+ if (
+ (!RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
+ (RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_LEFT)
+ ) {
+ this.moveTabForward();
+ } else {
+ this.moveTabBackward();
+ }
+ },
+
+ /**
+ * @param aTab
+ * Can be from a different window as well
+ * @param aRestoreTabImmediately
+ * Can defer loading of the tab contents
+ * @param aOptions
+ * The new index of the tab
+ */
+ duplicateTab(aTab, aRestoreTabImmediately, aOptions) {
+ return SessionStore.duplicateTab(
+ window,
+ aTab,
+ 0,
+ aRestoreTabImmediately,
+ aOptions
+ );
+ },
+
+ addToMultiSelectedTabs(aTab) {
+ if (aTab.multiselected) {
+ return;
+ }
+
+ aTab.setAttribute("multiselected", "true");
+ aTab.setAttribute("aria-selected", "true");
+ this._multiSelectedTabsSet.add(aTab);
+ this._startMultiSelectChange();
+ if (this._multiSelectChangeRemovals.has(aTab)) {
+ this._multiSelectChangeRemovals.delete(aTab);
+ } else {
+ this._multiSelectChangeAdditions.add(aTab);
+ }
+ },
+
+ /**
+ * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
+ */
+ addRangeToMultiSelectedTabs(aTab1, aTab2) {
+ if (aTab1 == aTab2) {
+ return;
+ }
+
+ const tabs = this.visibleTabs;
+ const indexOfTab1 = tabs.indexOf(aTab1);
+ const indexOfTab2 = tabs.indexOf(aTab2);
+
+ const [lowerIndex, higherIndex] =
+ indexOfTab1 < indexOfTab2
+ ? [indexOfTab1, indexOfTab2]
+ : [indexOfTab2, indexOfTab1];
+
+ for (let i = lowerIndex; i <= higherIndex; i++) {
+ this.addToMultiSelectedTabs(tabs[i]);
+ }
+ },
+
+ removeFromMultiSelectedTabs(aTab) {
+ if (!aTab.multiselected) {
+ return;
+ }
+ aTab.removeAttribute("multiselected");
+ aTab.removeAttribute("aria-selected");
+ this._multiSelectedTabsSet.delete(aTab);
+ this._startMultiSelectChange();
+ if (this._multiSelectChangeAdditions.has(aTab)) {
+ this._multiSelectChangeAdditions.delete(aTab);
+ } else {
+ this._multiSelectChangeRemovals.add(aTab);
+ }
+ },
+
+ clearMultiSelectedTabs() {
+ if (this._clearMultiSelectionLocked) {
+ if (this._clearMultiSelectionLockedOnce) {
+ this._clearMultiSelectionLockedOnce = false;
+ this._clearMultiSelectionLocked = false;
+ }
+ return;
+ }
+
+ if (this.multiSelectedTabsCount < 1) {
+ return;
+ }
+
+ for (let tab of this.selectedTabs) {
+ this.removeFromMultiSelectedTabs(tab);
+ }
+ this._lastMultiSelectedTabRef = null;
+ },
+
+ selectAllTabs() {
+ let visibleTabs = this.visibleTabs;
+ gBrowser.addRangeToMultiSelectedTabs(
+ visibleTabs[0],
+ visibleTabs[visibleTabs.length - 1]
+ );
+ },
+
+ allTabsSelected() {
+ return (
+ this.visibleTabs.length == 1 ||
+ this.visibleTabs.every(t => t.multiselected)
+ );
+ },
+
+ lockClearMultiSelectionOnce() {
+ this._clearMultiSelectionLockedOnce = true;
+ this._clearMultiSelectionLocked = true;
+ },
+
+ unlockClearMultiSelection() {
+ this._clearMultiSelectionLockedOnce = false;
+ this._clearMultiSelectionLocked = false;
+ },
+
+ /**
+ * Remove a tab from the multiselection if it's the only one left there.
+ *
+ * In fact, some scenario may lead to only one single tab multi-selected,
+ * this is something to avoid (Chrome does the same)
+ * Consider 4 tabs A,B,C,D with A having the focus
+ * 1. select C with Ctrl
+ * 2. Right-click on B and "Close Tabs to The Right"
+ *
+ * Expected result
+ * C and D closing
+ * A being the only multi-selected tab, selection should be cleared
+ *
+ *
+ * Single selected tab could even happen with a none-focused tab.
+ * For exemple with the menu "Close other tabs", it could happen
+ * with a multi-selected pinned tab.
+ * For illustration, consider 4 tabs A,B,C,D with B active
+ * 1. pin A and Ctrl-select it
+ * 2. Ctrl-select C
+ * 3. right-click on D and click "Close Other Tabs"
+ *
+ * Expected result
+ * B and C closing
+ * A[pinned] being the only multi-selected tab, selection should be cleared.
+ */
+ avoidSingleSelectedTab() {
+ if (this.multiSelectedTabsCount == 1) {
+ this.clearMultiSelectedTabs();
+ }
+ },
+
+ switchToNextMultiSelectedTab() {
+ this._clearMultiSelectionLocked = true;
+
+ // Guarantee that _clearMultiSelectionLocked lock gets released.
+ try {
+ let lastMultiSelectedTab = gBrowser.lastMultiSelectedTab;
+ if (lastMultiSelectedTab != gBrowser.selectedTab) {
+ gBrowser.selectedTab = lastMultiSelectedTab;
+ } else {
+ let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(
+ this._multiSelectedTabsSet
+ ).filter(tab => tab.isConnected && !tab.closing);
+ let length = selectedTabs.length;
+ gBrowser.selectedTab = selectedTabs[length - 1];
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ this._clearMultiSelectionLocked = false;
+ },
+
+ set selectedTabs(tabs) {
+ this.clearMultiSelectedTabs();
+ this.selectedTab = tabs[0];
+ if (tabs.length > 1) {
+ for (let tab of tabs) {
+ this.addToMultiSelectedTabs(tab);
+ }
+ }
+ },
+
+ get selectedTabs() {
+ let { selectedTab, _multiSelectedTabsSet } = this;
+ let tabs = ChromeUtils.nondeterministicGetWeakSetKeys(
+ _multiSelectedTabsSet
+ ).filter(tab => tab.isConnected && !tab.closing);
+ if (!_multiSelectedTabsSet.has(selectedTab)) {
+ tabs.push(selectedTab);
+ }
+ return tabs.sort((a, b) => a._tPos > b._tPos);
+ },
+
+ get multiSelectedTabsCount() {
+ return ChromeUtils.nondeterministicGetWeakSetKeys(
+ this._multiSelectedTabsSet
+ ).filter(tab => tab.isConnected && !tab.closing).length;
+ },
+
+ get lastMultiSelectedTab() {
+ let tab = this._lastMultiSelectedTabRef
+ ? this._lastMultiSelectedTabRef.get()
+ : null;
+ if (tab && tab.isConnected && this._multiSelectedTabsSet.has(tab)) {
+ return tab;
+ }
+ let selectedTab = gBrowser.selectedTab;
+ this.lastMultiSelectedTab = selectedTab;
+ return selectedTab;
+ },
+
+ set lastMultiSelectedTab(aTab) {
+ this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
+ },
+
+ _startMultiSelectChange() {
+ if (!this._multiSelectChangeStarted) {
+ this._multiSelectChangeStarted = true;
+ Promise.resolve().then(() => this._endMultiSelectChange());
+ }
+ },
+
+ _endMultiSelectChange() {
+ let noticeable = false;
+ let { selectedTab } = this;
+ if (this._multiSelectChangeAdditions.size) {
+ if (!selectedTab.multiselected) {
+ this.addToMultiSelectedTabs(selectedTab);
+ }
+ noticeable = true;
+ }
+ if (this._multiSelectChangeRemovals.size) {
+ if (this._multiSelectChangeRemovals.has(selectedTab)) {
+ this.switchToNextMultiSelectedTab();
+ }
+ this.avoidSingleSelectedTab();
+ noticeable = true;
+ }
+ this._multiSelectChangeStarted = false;
+ if (noticeable || this._multiSelectChangeSelected) {
+ this._multiSelectChangeSelected = false;
+ this._multiSelectChangeAdditions.clear();
+ this._multiSelectChangeRemovals.clear();
+ if (noticeable) {
+ this.tabContainer._setPositionalAttributes();
+ }
+ this.dispatchEvent(
+ new CustomEvent("TabMultiSelect", { bubbles: true })
+ );
+ }
+ },
+
+ toggleMuteAudioOnMultiSelectedTabs(aTab) {
+ let tabsToToggle;
+ if (aTab.activeMediaBlocked) {
+ tabsToToggle = this.selectedTabs.filter(
+ tab => tab.activeMediaBlocked || tab.linkedBrowser.audioMuted
+ );
+ } else {
+ let tabMuted = aTab.linkedBrowser.audioMuted;
+ tabsToToggle = this.selectedTabs.filter(
+ tab =>
+ // When a user is looking to mute selected tabs, then media-blocked tabs
+ // should not be toggled. Otherwise those media-blocked tabs are going into a
+ // playing and unmuted state.
+ (tab.linkedBrowser.audioMuted == tabMuted &&
+ !tab.activeMediaBlocked) ||
+ (tab.activeMediaBlocked && tabMuted)
+ );
+ }
+ for (let tab of tabsToToggle) {
+ tab.toggleMuteAudio();
+ }
+ },
+
+ pinMultiSelectedTabs() {
+ for (let tab of this.selectedTabs) {
+ this.pinTab(tab);
+ }
+ },
+
+ unpinMultiSelectedTabs() {
+ // The selectedTabs getter returns the tabs
+ // in visual order. We need to unpin in reverse
+ // order to maintain visual order.
+ let selectedTabs = this.selectedTabs;
+ for (let i = selectedTabs.length - 1; i >= 0; i--) {
+ let tab = selectedTabs[i];
+ this.unpinTab(tab);
+ }
+ },
+
+ activateBrowserForPrintPreview(aBrowser) {
+ this._printPreviewBrowsers.add(aBrowser);
+ if (this._switcher) {
+ this._switcher.activateBrowserForPrintPreview(aBrowser);
+ }
+ aBrowser.docShellIsActive = true;
+ },
+
+ deactivatePrintPreviewBrowsers() {
+ let browsers = this._printPreviewBrowsers;
+ this._printPreviewBrowsers = new Set();
+ for (let browser of browsers) {
+ browser.docShellIsActive = this.shouldActivateDocShell(browser);
+ }
+ },
+
+ /**
+ * Returns true if a given browser's docshell should be active.
+ */
+ shouldActivateDocShell(aBrowser) {
+ if (this._switcher) {
+ return this._switcher.shouldActivateDocShell(aBrowser);
+ }
+ return (
+ (aBrowser == this.selectedBrowser &&
+ window.windowState != window.STATE_MINIMIZED &&
+ !window.isFullyOccluded) ||
+ this._printPreviewBrowsers.has(aBrowser)
+ );
+ },
+
+ _getSwitcher() {
+ if (!this._switcher) {
+ this._switcher = new this.AsyncTabSwitcher(this);
+ }
+ return this._switcher;
+ },
+
+ warmupTab(aTab) {
+ if (gMultiProcessBrowser) {
+ this._getSwitcher().warmupTab(aTab);
+ }
+ },
+
+ _handleKeyDownEvent(aEvent) {
+ if (!aEvent.isTrusted) {
+ // Don't let untrusted events mess with tabs.
+ return;
+ }
+
+ // Skip this only if something has explicitly cancelled it.
+ if (aEvent.defaultCancelled) {
+ return;
+ }
+
+ // Don't check if the event was already consumed because tab
+ // navigation should always work for better user experience.
+
+ switch (ShortcutUtils.getSystemActionForEvent(aEvent)) {
+ case ShortcutUtils.MOVE_TAB_BACKWARD:
+ this.moveTabBackward();
+ aEvent.preventDefault();
+ return;
+ case ShortcutUtils.MOVE_TAB_FORWARD:
+ this.moveTabForward();
+ aEvent.preventDefault();
+ return;
+ case ShortcutUtils.CLOSE_TAB:
+ if (gBrowser.multiSelectedTabsCount) {
+ gBrowser.removeMultiSelectedTabs();
+ } else if (!this.selectedTab.pinned) {
+ this.removeCurrentTab({ animate: true });
+ }
+ aEvent.preventDefault();
+ }
+ },
+
+ toggleCaretBrowsing() {
+ const kPrefShortcutEnabled =
+ "accessibility.browsewithcaret_shortcut.enabled";
+ const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
+ const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
+
+ var isEnabled = Services.prefs.getBoolPref(kPrefShortcutEnabled);
+ if (!isEnabled) {
+ return;
+ }
+
+ // Toggle browse with caret mode
+ var browseWithCaretOn = Services.prefs.getBoolPref(
+ kPrefCaretBrowsingOn,
+ false
+ );
+ var warn = Services.prefs.getBoolPref(kPrefWarnOnEnable, true);
+ if (warn && !browseWithCaretOn) {
+ var checkValue = { value: false };
+ var promptService = Services.prompt;
+
+ var buttonPressed = promptService.confirmEx(
+ window,
+ gTabBrowserBundle.GetStringFromName(
+ "browsewithcaret.checkWindowTitle"
+ ),
+ gTabBrowserBundle.GetStringFromName("browsewithcaret.checkLabel"),
+ // Make "No" the default:
+ promptService.STD_YES_NO_BUTTONS | promptService.BUTTON_POS_1_DEFAULT,
+ null,
+ null,
+ null,
+ gTabBrowserBundle.GetStringFromName("browsewithcaret.checkMsg"),
+ checkValue
+ );
+ if (buttonPressed != 0) {
+ if (checkValue.value) {
+ try {
+ Services.prefs.setBoolPref(kPrefShortcutEnabled, false);
+ } catch (ex) {}
+ }
+ return;
+ }
+ if (checkValue.value) {
+ try {
+ Services.prefs.setBoolPref(kPrefWarnOnEnable, false);
+ } catch (ex) {}
+ }
+ }
+
+ // Toggle the pref
+ try {
+ Services.prefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
+ } catch (ex) {}
+ },
+
+ _handleKeyPressEvent(aEvent) {
+ if (!aEvent.isTrusted) {
+ // Don't let untrusted events mess with tabs.
+ return;
+ }
+
+ // Skip this only if something has explicitly cancelled it.
+ if (aEvent.defaultCancelled) {
+ return;
+ }
+
+ switch (ShortcutUtils.getSystemActionForEvent(aEvent, { rtl: RTL_UI })) {
+ case ShortcutUtils.TOGGLE_CARET_BROWSING:
+ if (!aEvent.defaultPrevented) {
+ this.toggleCaretBrowsing();
+ }
+ break;
+
+ case ShortcutUtils.NEXT_TAB:
+ if (AppConstants.platform == "macosx") {
+ this.tabContainer.advanceSelectedTab(1, true);
+ aEvent.preventDefault();
+ }
+ break;
+ case ShortcutUtils.PREVIOUS_TAB:
+ if (AppConstants.platform == "macosx") {
+ this.tabContainer.advanceSelectedTab(-1, true);
+ aEvent.preventDefault();
+ }
+ break;
+ }
+ },
+
+ getTabTooltip(tab, includeLabel = true) {
+ let label = "";
+ if (includeLabel) {
+ label = tab._fullLabel || tab.getAttribute("label");
+ }
+ if (
+ Services.prefs.getBoolPref(
+ "browser.tabs.tooltipsShowPidAndActiveness",
+ false
+ )
+ ) {
+ if (tab.linkedBrowser) {
+ // When enabled, show the PID of the content process, and if
+ // we're running with fission enabled, try to include PIDs for
+ // every remote subframe.
+ let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
+ tab.linkedBrowser,
+ gFissionBrowser
+ );
+ if (contentPid) {
+ label += " (pid " + contentPid + ")";
+ if (gFissionBrowser) {
+ label += " [F";
+ if (framePids.length) {
+ label += " " + framePids.join(", ");
+ }
+ label += "]";
+ }
+ }
+ if (tab.linkedBrowser.docShellIsActive) {
+ label += " [A]";
+ }
+ }
+ }
+ if (tab.userContextId) {
+ label = gTabBrowserBundle.formatStringFromName(
+ "tabs.containers.tooltip",
+ [
+ label,
+ ContextualIdentityService.getUserContextLabel(tab.userContextId),
+ ]
+ );
+ }
+ return label;
+ },
+
+ createTooltip(event) {
+ event.stopPropagation();
+ let tab = document.tooltipNode
+ ? document.tooltipNode.closest("tab")
+ : null;
+ if (!tab) {
+ event.preventDefault();
+ return;
+ }
+
+ let stringWithShortcut = (stringId, keyElemId, pluralCount) => {
+ let keyElem = document.getElementById(keyElemId);
+ let shortcut = ShortcutUtils.prettifyShortcut(keyElem);
+ return PluralForm.get(
+ pluralCount,
+ gTabBrowserBundle.GetStringFromName(stringId)
+ )
+ .replace("%S", shortcut)
+ .replace("#1", pluralCount);
+ };
+
+ let label;
+ const selectedTabs = this.selectedTabs;
+ const contextTabInSelection = selectedTabs.includes(tab);
+ const affectedTabsLength = contextTabInSelection
+ ? selectedTabs.length
+ : 1;
+ if (tab.mOverCloseButton) {
+ label = tab.selected
+ ? stringWithShortcut(
+ "tabs.closeTabs.tooltip",
+ "key_close",
+ affectedTabsLength
+ )
+ : PluralForm.get(
+ affectedTabsLength,
+ gTabBrowserBundle.GetStringFromName("tabs.closeTabs.tooltip")
+ ).replace("#1", affectedTabsLength);
+ }
+ // When Picture-in-Picture is open, we repurpose '.tab-icon-sound' as
+ // an inert Picture-in-Picture indicator, so we should display
+ // the default tooltip
+ else if (tab._overPlayingIcon && !tab.pictureinpicture) {
+ let stringID;
+ if (tab.selected) {
+ stringID = tab.linkedBrowser.audioMuted
+ ? "tabs.unmuteAudio2.tooltip"
+ : "tabs.muteAudio2.tooltip";
+ label = stringWithShortcut(
+ stringID,
+ "key_toggleMute",
+ affectedTabsLength
+ );
+ } else {
+ if (tab.hasAttribute("activemedia-blocked")) {
+ stringID = "tabs.unblockAudio2.tooltip";
+ } else {
+ stringID = tab.linkedBrowser.audioMuted
+ ? "tabs.unmuteAudio2.background.tooltip"
+ : "tabs.muteAudio2.background.tooltip";
+ }
+
+ label = PluralForm.get(
+ affectedTabsLength,
+ gTabBrowserBundle.GetStringFromName(stringID)
+ ).replace("#1", affectedTabsLength);
+ }
+ } else {
+ label = this.getTabTooltip(tab);
+ }
+
+ event.target.setAttribute("label", label);
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "keydown":
+ this._handleKeyDownEvent(aEvent);
+ break;
+ case "keypress":
+ this._handleKeyPressEvent(aEvent);
+ break;
+ case "framefocusrequested": {
+ let tab = this.getTabForBrowser(aEvent.target);
+ if (!tab || tab == this.selectedTab) {
+ // Let the focus manager try to do its thing by not calling
+ // preventDefault(). It will still raise the window if appropriate.
+ break;
+ }
+ this.selectedTab = tab;
+ window.focus();
+ aEvent.preventDefault();
+ break;
+ }
+ case "sizemodechange":
+ case "occlusionstatechange":
+ if (aEvent.target == window && !this._switcher) {
+ this.selectedBrowser.preserveLayers(
+ window.windowState == window.STATE_MINIMIZED ||
+ window.isFullyOccluded
+ );
+ this.selectedBrowser.docShellIsActive = this.shouldActivateDocShell(
+ this.selectedBrowser
+ );
+ }
+ break;
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "contextual-identity-updated": {
+ let identity = aSubject.wrappedJSObject;
+ for (let tab of this.tabs) {
+ if (tab.getAttribute("usercontextid") == identity.userContextId) {
+ ContextualIdentityService.setTabStyle(tab);
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ refreshBlocked(actor, browser, data) {
+ // The data object is expected to contain the following properties:
+ // - URI (string)
+ // The URI that a page is attempting to refresh or redirect to.
+ // - delay (int)
+ // The delay (in milliseconds) before the page was going to
+ // reload or redirect.
+ // - sameURI (bool)
+ // true if we're refreshing the page. false if we're redirecting.
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ let message = gNavigatorBundle.getFormattedString(
+ "refreshBlocked." + (data.sameURI ? "refreshLabel" : "redirectLabel"),
+ [brandShortName]
+ );
+
+ let notificationBox = this.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue(
+ "refresh-blocked"
+ );
+
+ if (notification) {
+ notification.label = message;
+ } else {
+ let refreshButtonText = gNavigatorBundle.getString(
+ "refreshBlocked.goButton"
+ );
+ let refreshButtonAccesskey = gNavigatorBundle.getString(
+ "refreshBlocked.goButton.accesskey"
+ );
+
+ let buttons = [
+ {
+ label: refreshButtonText,
+ accessKey: refreshButtonAccesskey,
+ callback() {
+ actor.sendAsyncMessage("RefreshBlocker:Refresh", data);
+ },
+ },
+ ];
+
+ notificationBox.appendNotification(
+ message,
+ "refresh-blocked",
+ "chrome://browser/skin/notification-icons/popup.svg",
+ notificationBox.PRIORITY_INFO_MEDIUM,
+ buttons
+ );
+ }
+ },
+
+ _generateUniquePanelID() {
+ if (!this._uniquePanelIDCounter) {
+ this._uniquePanelIDCounter = 0;
+ }
+
+ let outerID = window.docShell.outerWindowID;
+
+ // We want panel IDs to be globally unique, that's why we include the
+ // window ID. We switched to a monotonic counter as Date.now() lead
+ // to random failures because of colliding IDs.
+ return "panel-" + outerID + "-" + ++this._uniquePanelIDCounter;
+ },
+
+ destroy() {
+ this.tabContainer.destroy();
+ Services.obs.removeObserver(this, "contextual-identity-updated");
+
+ for (let tab of this.tabs) {
+ let browser = tab.linkedBrowser;
+ if (browser.registeredOpenURI) {
+ let userContextId = browser.getAttribute("usercontextid") || 0;
+ this.UrlbarProviderOpenTabs.unregisterOpenTab(
+ browser.registeredOpenURI.spec,
+ userContextId
+ );
+ delete browser.registeredOpenURI;
+ }
+
+ let filter = this._tabFilters.get(tab);
+ if (filter) {
+ browser.webProgress.removeProgressListener(filter);
+
+ let listener = this._tabListeners.get(tab);
+ if (listener) {
+ filter.removeProgressListener(listener);
+ listener.destroy();
+ }
+
+ this._tabFilters.delete(tab);
+ this._tabListeners.delete(tab);
+ }
+ }
+
+ Services.els.removeSystemEventListener(document, "keydown", this, false);
+ if (AppConstants.platform == "macosx") {
+ Services.els.removeSystemEventListener(
+ document,
+ "keypress",
+ this,
+ false
+ );
+ }
+ window.removeEventListener("sizemodechange", this);
+ window.removeEventListener("occlusionstatechange", this);
+ window.removeEventListener("framefocusrequested", this);
+
+ if (gMultiProcessBrowser) {
+ if (this._switcher) {
+ this._switcher.destroy();
+ }
+ }
+ },
+
+ _setupEventListeners() {
+ this.tabpanels.addEventListener("select", event => {
+ if (event.target == this.tabpanels) {
+ this.updateCurrentBrowser();
+ }
+ });
+
+ this.addEventListener("DOMWindowClose", event => {
+ let browser = event.target;
+ if (!browser.isRemoteBrowser) {
+ if (!event.isTrusted) {
+ // If the browser is not remote, then we expect the event to be trusted.
+ // In the remote case, the DOMWindowClose event is captured in content,
+ // a message is sent to the parent, and another DOMWindowClose event
+ // is re-dispatched on the actual browser node. In that case, the event
+ // won't be marked as trusted, since it's synthesized by JavaScript.
+ return;
+ }
+ // In the parent-process browser case, it's possible that the browser
+ // that fired DOMWindowClose is actually a child of another browser. We
+ // want to find the top-most browser to determine whether or not this is
+ // for a tab or not. The chromeEventHandler will be the top-most browser.
+ browser = event.target.docShell.chromeEventHandler;
+ }
+
+ if (this.tabs.length == 1) {
+ // We already did PermitUnload in the content process
+ // for this tab (the only one in the window). So we don't
+ // need to do it again for any tabs.
+ window.skipNextCanClose = true;
+ // In the parent-process browser case, the nsCloseEvent will actually take
+ // care of tearing down the window, but we need to do this ourselves in the
+ // content-process browser case. Doing so in both cases doesn't appear to
+ // hurt.
+ window.close();
+ return;
+ }
+
+ let tab = this.getTabForBrowser(browser);
+ if (tab) {
+ // Skip running PermitUnload since it already happened in
+ // the content process.
+ this.removeTab(tab, { skipPermitUnload: true });
+ // If we don't preventDefault on the DOMWindowClose event, then
+ // in the parent-process browser case, we're telling the platform
+ // to close the entire window. Calling preventDefault is our way of
+ // saying we took care of this close request by closing the tab.
+ event.preventDefault();
+ }
+ });
+
+ this.addEventListener("pagetitlechanged", event => {
+ let browser = event.target;
+ let tab = this.getTabForBrowser(browser);
+ if (!tab || tab.hasAttribute("pending")) {
+ return;
+ }
+
+ // Ignore empty title changes on internal pages. This prevents the title
+ // from changing while Fluent is populating the (initially-empty) title
+ // element.
+ if (
+ !browser.contentTitle &&
+ browser.contentPrincipal.isSystemPrincipal
+ ) {
+ return;
+ }
+
+ let titleChanged = this.setTabTitle(tab);
+ if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
+ tab.setAttribute("titlechanged", "true");
+ }
+ });
+
+ this.addEventListener(
+ "DOMWillOpenModalDialog",
+ event => {
+ if (!event.isTrusted) {
+ return;
+ }
+
+ let targetIsWindow = event.target instanceof Window;
+
+ // We're about to open a modal dialog, so figure out for which tab:
+ // If this is a same-process modal dialog, then we're given its DOM
+ // window as the event's target. For remote dialogs, we're given the
+ // browser, but that's in the originalTarget and not the target,
+ // because it's across the tabbrowser's XBL boundary.
+ let tabForEvent = targetIsWindow
+ ? this.getTabForBrowser(event.target.docShell.chromeEventHandler)
+ : this.getTabForBrowser(event.originalTarget);
+
+ // Focus window for beforeunload dialog so it is seen but don't
+ // steal focus from other applications.
+ if (
+ event.detail &&
+ event.detail.tabPrompt &&
+ event.detail.inPermitUnload &&
+ Services.focus.activeWindow
+ ) {
+ window.focus();
+ }
+
+ // Don't need to act if the tab is already selected or if there isn't
+ // a tab for the event (e.g. for the webextensions options_ui remote
+ // browsers embedded in the "about:addons" page):
+ if (!tabForEvent || tabForEvent.selected) {
+ return;
+ }
+
+ // We always switch tabs for beforeunload tab-modal prompts.
+ if (
+ event.detail &&
+ event.detail.tabPrompt &&
+ !event.detail.inPermitUnload
+ ) {
+ let docPrincipal = targetIsWindow
+ ? event.target.document.nodePrincipal
+ : null;
+ // At least one of these should/will be non-null:
+ let promptPrincipal =
+ event.detail.promptPrincipal ||
+ docPrincipal ||
+ tabForEvent.linkedBrowser.contentPrincipal;
+
+ // For null principals, we bail immediately and don't show the checkbox:
+ if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
+ tabForEvent.setAttribute("attention", "true");
+ this._tabAttrModified(tabForEvent, ["attention"]);
+ return;
+ }
+
+ // For non-system/expanded principals, we bail and show the checkbox
+ if (promptPrincipal.URI && !promptPrincipal.isSystemPrincipal) {
+ let permission = Services.perms.testPermissionFromPrincipal(
+ promptPrincipal,
+ "focus-tab-by-prompt"
+ );
+ if (permission != Services.perms.ALLOW_ACTION) {
+ // Tell the prompt box we want to show the user a checkbox:
+ let tabPrompt = this.getTabModalPromptBox(
+ tabForEvent.linkedBrowser
+ );
+ tabPrompt.onNextPromptShowAllowFocusCheckboxFor(
+ promptPrincipal
+ );
+ tabForEvent.setAttribute("attention", "true");
+ this._tabAttrModified(tabForEvent, ["attention"]);
+ return;
+ }
+ }
+ // ... so system and expanded principals, as well as permitted "normal"
+ // URI-based principals, always get to steal focus for the tab when prompting.
+ }
+
+ // If permissions/origins dictate so, bring tab to the front.
+ this.selectedTab = tabForEvent;
+ },
+ true
+ );
+
+ // When cancelling beforeunload tabmodal dialogs, reset the URL bar to
+ // avoid spoofing risks.
+ this.addEventListener(
+ "DOMModalDialogClosed",
+ event => {
+ if (
+ !event.detail?.wasPermitUnload ||
+ event.detail.areLeaving ||
+ event.target.nodeName != "browser"
+ ) {
+ return;
+ }
+ event.target.userTypedValue = null;
+ if (event.target == this.selectedBrowser) {
+ gURLBar.setURI();
+ }
+ },
+ true
+ );
+
+ let onTabCrashed = event => {
+ if (!event.isTrusted) {
+ return;
+ }
+
+ let browser = event.originalTarget;
+
+ if (!event.isTopFrame) {
+ TabCrashHandler.onSubFrameCrash(browser, event.childID);
+ return;
+ }
+
+ // Preloaded browsers do not actually have any tabs. If one crashes,
+ // it should be released and removed.
+ if (browser === this.preloadedBrowser) {
+ NewTabPagePreloading.removePreloadedBrowser(window);
+ return;
+ }
+
+ let isRestartRequiredCrash =
+ event.type == "oop-browser-buildid-mismatch";
+
+ let icon = browser.mIconURL;
+ let tab = this.getTabForBrowser(browser);
+
+ if (this.selectedBrowser == browser) {
+ TabCrashHandler.onSelectedBrowserCrash(
+ browser,
+ isRestartRequiredCrash
+ );
+ } else {
+ TabCrashHandler.onBackgroundBrowserCrash(
+ browser,
+ isRestartRequiredCrash
+ );
+ }
+
+ tab.removeAttribute("soundplaying");
+ this.setIcon(tab, icon);
+ };
+
+ this.addEventListener("oop-browser-crashed", onTabCrashed);
+ this.addEventListener("oop-browser-buildid-mismatch", onTabCrashed);
+
+ this.addEventListener("DOMAudioPlaybackStarted", event => {
+ var tab = this.getTabFromAudioEvent(event);
+ if (!tab) {
+ return;
+ }
+
+ clearTimeout(tab._soundPlayingAttrRemovalTimer);
+ tab._soundPlayingAttrRemovalTimer = 0;
+
+ let modifiedAttrs = [];
+ if (tab.hasAttribute("soundplaying-scheduledremoval")) {
+ tab.removeAttribute("soundplaying-scheduledremoval");
+ modifiedAttrs.push("soundplaying-scheduledremoval");
+ }
+
+ if (!tab.hasAttribute("soundplaying")) {
+ tab.setAttribute("soundplaying", true);
+ modifiedAttrs.push("soundplaying");
+ }
+
+ if (modifiedAttrs.length) {
+ // Flush style so that the opacity takes effect immediately, in
+ // case the media is stopped before the style flushes naturally.
+ getComputedStyle(tab).opacity;
+ }
+
+ this._tabAttrModified(tab, modifiedAttrs);
+ });
+
+ this.addEventListener("DOMAudioPlaybackStopped", event => {
+ var tab = this.getTabFromAudioEvent(event);
+ if (!tab) {
+ return;
+ }
+
+ if (tab.hasAttribute("soundplaying")) {
+ let removalDelay = Services.prefs.getIntPref(
+ "browser.tabs.delayHidingAudioPlayingIconMS"
+ );
+
+ tab.style.setProperty(
+ "--soundplaying-removal-delay",
+ `${removalDelay - 300}ms`
+ );
+ tab.setAttribute("soundplaying-scheduledremoval", "true");
+ this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
+
+ tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
+ tab.removeAttribute("soundplaying-scheduledremoval");
+ tab.removeAttribute("soundplaying");
+ this._tabAttrModified(tab, [
+ "soundplaying",
+ "soundplaying-scheduledremoval",
+ ]);
+ }, removalDelay);
+ }
+ });
+
+ this.addEventListener("DOMAudioPlaybackBlockStarted", event => {
+ var tab = this.getTabFromAudioEvent(event);
+ if (!tab) {
+ return;
+ }
+
+ if (!tab.hasAttribute("activemedia-blocked")) {
+ tab.setAttribute("activemedia-blocked", true);
+ this._tabAttrModified(tab, ["activemedia-blocked"]);
+ }
+ });
+
+ this.addEventListener("DOMAudioPlaybackBlockStopped", event => {
+ var tab = this.getTabFromAudioEvent(event);
+ if (!tab) {
+ return;
+ }
+
+ if (tab.hasAttribute("activemedia-blocked")) {
+ tab.removeAttribute("activemedia-blocked");
+ this._tabAttrModified(tab, ["activemedia-blocked"]);
+ let hist = Services.telemetry.getHistogramById(
+ "TAB_AUDIO_INDICATOR_USED"
+ );
+ hist.add(2 /* unblockByVisitingTab */);
+ }
+ });
+
+ this.addEventListener("GloballyAutoplayBlocked", event => {
+ let browser = event.originalTarget;
+ let tab = this.getTabForBrowser(browser);
+ if (!tab) {
+ return;
+ }
+
+ SitePermissions.setForPrincipal(
+ browser.contentPrincipal,
+ "autoplay-media",
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_GLOBAL,
+ browser
+ );
+ });
+
+ let tabContextFTLInserter = () => {
+ MozXULElement.insertFTLIfNeeded("browser/tabContextMenu.ftl");
+ // Un-lazify the l10n-ids now that the FTL file has been inserted.
+ document
+ .getElementById("tabContextMenu")
+ .querySelectorAll("[data-lazy-l10n-id]")
+ .forEach(el => {
+ el.setAttribute(
+ "data-l10n-id",
+ el.getAttribute("data-lazy-l10n-id")
+ );
+ el.removeAttribute("data-lazy-l10n-id");
+ });
+ this.tabContainer.removeEventListener(
+ "contextmenu",
+ tabContextFTLInserter,
+ true
+ );
+ this.tabContainer.removeEventListener(
+ "mouseover",
+ tabContextFTLInserter
+ );
+ this.tabContainer.removeEventListener(
+ "focus",
+ tabContextFTLInserter,
+ true
+ );
+ };
+ this.tabContainer.addEventListener(
+ "contextmenu",
+ tabContextFTLInserter,
+ true
+ );
+ this.tabContainer.addEventListener("mouseover", tabContextFTLInserter);
+ this.tabContainer.addEventListener("focus", tabContextFTLInserter, true);
+
+ // Fired when Gecko has decided a <browser> element will change
+ // remoteness. This allows persisting some state on this element across
+ // process switches.
+ this.addEventListener("WillChangeBrowserRemoteness", event => {
+ let browser = event.originalTarget;
+ let tab = this.getTabForBrowser(browser);
+ if (!tab) {
+ return;
+ }
+
+ // Dispatch the `BeforeTabRemotenessChange` event, allowing other code
+ // to react to this tab's process switch.
+ let evt = document.createEvent("Events");
+ evt.initEvent("BeforeTabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ let wasActive = document.activeElement == browser;
+
+ // Unhook our progress listener.
+ let filter = this._tabFilters.get(tab);
+ let oldListener = this._tabListeners.get(tab);
+ browser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(oldListener);
+ let stateFlags = oldListener.mStateFlags;
+ let requestCount = oldListener.mRequestCount;
+
+ // We'll be creating a new listener, so destroy the old one.
+ oldListener.destroy();
+
+ let oldDroppedLinkHandler = browser.droppedLinkHandler;
+ let oldUserTypedValue = browser.userTypedValue;
+ let hadStartedLoad = browser.didStartLoadSinceLastUserTyping();
+
+ let didChange = didChangeEvent => {
+ browser.userTypedValue = oldUserTypedValue;
+ if (hadStartedLoad) {
+ browser.urlbarChangeTracker.startedLoad();
+ }
+
+ browser.droppedLinkHandler = oldDroppedLinkHandler;
+
+ // This shouldn't really be necessary (it should always set the same
+ // value as activeness is correctly preserved across remoteness changes).
+ // However, this has the side effect of sending MozLayerTreeReady /
+ // MozLayerTreeCleared events for remote frames, which the tab switcher
+ // depends on.
+ browser.docShellIsActive = this.shouldActivateDocShell(browser);
+
+ // Create a new tab progress listener for the new browser we just
+ // injected, since tab progress listeners have logic for handling the
+ // initial about:blank load
+ let listener = new TabProgressListener(
+ tab,
+ browser,
+ false,
+ false,
+ stateFlags,
+ requestCount
+ );
+ this._tabListeners.set(tab, listener);
+ filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ // Restore the progress listener.
+ browser.webProgress.addProgressListener(
+ filter,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+
+ let cbEvent = browser.getContentBlockingEvents();
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners(
+ browser,
+ "onContentBlockingEvent",
+ [browser.webProgress, null, cbEvent, true],
+ true,
+ false
+ );
+
+ if (browser.isRemoteBrowser) {
+ // Switching the browser to be remote will connect to a new child
+ // process so the browser can no longer be considered to be
+ // crashed.
+ tab.removeAttribute("crashed");
+ } else {
+ browser.sendMessageToActor(
+ "Browser:AppTab",
+ { isAppTab: tab.pinned },
+ "BrowserTab"
+ );
+ }
+
+ if (wasActive) {
+ browser.focus();
+ }
+
+ if (this.isFindBarInitialized(tab)) {
+ this.getCachedFindBar(tab).browser = browser;
+ }
+
+ browser.sendMessageToActor(
+ "Browser:HasSiblings",
+ this.tabs.length > 1,
+ "BrowserTab"
+ );
+
+ evt = document.createEvent("Events");
+ evt.initEvent("TabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+ };
+ browser.addEventListener("DidChangeBrowserRemoteness", didChange, {
+ once: true,
+ });
+ });
+ },
+
+ setSuccessor(aTab, successorTab) {
+ if (aTab.ownerGlobal != window) {
+ throw new Error("Cannot set the successor of another window's tab");
+ }
+ if (successorTab == aTab) {
+ successorTab = null;
+ }
+ if (successorTab && successorTab.ownerGlobal != window) {
+ throw new Error("Cannot set the successor to another window's tab");
+ }
+ if (aTab.successor) {
+ aTab.successor.predecessors.delete(aTab);
+ }
+ aTab.successor = successorTab;
+ if (successorTab) {
+ if (!successorTab.predecessors) {
+ successorTab.predecessors = new Set();
+ }
+ successorTab.predecessors.add(aTab);
+ }
+ },
+
+ /**
+ * For all tabs with aTab as a successor, set the successor to aOtherTab
+ * instead.
+ */
+ replaceInSuccession(aTab, aOtherTab) {
+ if (aTab.predecessors) {
+ for (const predecessor of Array.from(aTab.predecessors)) {
+ this.setSuccessor(predecessor, aOtherTab);
+ }
+ }
+ },
+ };
+
+ /**
+ * A web progress listener object definition for a given tab.
+ */
+ class TabProgressListener {
+ constructor(
+ aTab,
+ aBrowser,
+ aStartsBlank,
+ aWasPreloadedBrowser,
+ aOrigStateFlags,
+ aOrigRequestCount
+ ) {
+ let stateFlags = aOrigStateFlags || 0;
+ // Initialize mStateFlags to non-zero e.g. when creating a progress
+ // listener for preloaded browsers as there was no progress listener
+ // around when the content started loading. If the content didn't
+ // quite finish loading yet, mStateFlags will very soon be overridden
+ // with the correct value and end up at STATE_STOP again.
+ if (aWasPreloadedBrowser) {
+ stateFlags =
+ Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_REQUEST;
+ }
+
+ this.mTab = aTab;
+ this.mBrowser = aBrowser;
+ this.mBlank = aStartsBlank;
+
+ // cache flags for correct status UI update after tab switching
+ this.mStateFlags = stateFlags;
+ this.mStatus = 0;
+ this.mMessage = "";
+ this.mTotalProgress = 0;
+
+ // count of open requests (should always be 0 or 1)
+ this.mRequestCount = aOrigRequestCount || 0;
+ }
+
+ destroy() {
+ delete this.mTab;
+ delete this.mBrowser;
+ }
+
+ _callProgressListeners(...args) {
+ args.unshift(this.mBrowser);
+ return gBrowser._callProgressListeners.apply(gBrowser, args);
+ }
+
+ _shouldShowProgress(aRequest) {
+ if (this.mBlank) {
+ return false;
+ }
+
+ // Don't show progress indicators in tabs for about: URIs
+ // pointing to local resources.
+ if (
+ aRequest instanceof Ci.nsIChannel &&
+ aRequest.originalURI.schemeIs("about")
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
+ if (!this.mBlank || !aWebProgress.isTopLevel) {
+ return false;
+ }
+
+ // If the state has STATE_STOP, and no requests were in flight, then this
+ // must be the initial "stop" for the initial about:blank document.
+ if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ this.mRequestCount == 0 &&
+ !aLocation
+ ) {
+ return true;
+ }
+
+ let location = aLocation ? aLocation.spec : "";
+ return location == "about:blank";
+ }
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ this.mTotalProgress = aMaxTotalProgress
+ ? aCurTotalProgress / aMaxTotalProgress
+ : 0;
+
+ if (!this._shouldShowProgress(aRequest)) {
+ return;
+ }
+
+ if (this.mTotalProgress && this.mTab.hasAttribute("busy")) {
+ this.mTab.setAttribute("progress", "true");
+ gBrowser._tabAttrModified(this.mTab, ["progress"]);
+ }
+
+ this._callProgressListeners("onProgressChange", [
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress,
+ ]);
+ }
+
+ onProgressChange64(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ return this.onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ );
+ }
+
+ /* eslint-disable complexity */
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (!aRequest) {
+ return;
+ }
+
+ let location, originalLocation;
+ try {
+ aRequest.QueryInterface(Ci.nsIChannel);
+ location = aRequest.URI;
+ originalLocation = aRequest.originalURI;
+ } catch (ex) {}
+
+ let ignoreBlank = this._isForInitialAboutBlank(
+ aWebProgress,
+ aStateFlags,
+ location
+ );
+
+ const {
+ STATE_START,
+ STATE_STOP,
+ STATE_IS_NETWORK,
+ } = Ci.nsIWebProgressListener;
+
+ // If we were ignoring some messages about the initial about:blank, and we
+ // got the STATE_STOP for it, we'll want to pay attention to those messages
+ // from here forward. Similarly, if we conclude that this state change
+ // is one that we shouldn't be ignoring, then stop ignoring.
+ if (
+ (ignoreBlank &&
+ aStateFlags & STATE_STOP &&
+ aStateFlags & STATE_IS_NETWORK) ||
+ (!ignoreBlank && this.mBlank)
+ ) {
+ this.mBlank = false;
+ }
+
+ if (aStateFlags & STATE_START && aStateFlags & STATE_IS_NETWORK) {
+ this.mRequestCount++;
+
+ if (aWebProgress.isTopLevel) {
+ // Need to use originalLocation rather than location because things
+ // like about:home and about:privatebrowsing arrive with nsIRequest
+ // pointing to their resolved jar: or file: URIs.
+ if (
+ !(
+ originalLocation &&
+ gInitialPages.includes(originalLocation.spec) &&
+ originalLocation != "about:blank" &&
+ this.mBrowser.initialPageLoadedFromUserAction !=
+ originalLocation.spec &&
+ this.mBrowser.currentURI &&
+ this.mBrowser.currentURI.spec == "about:blank"
+ )
+ ) {
+ // Indicating that we started a load will allow the location
+ // bar to be cleared when the load finishes.
+ // In order to not overwrite user-typed content, we avoid it
+ // (see if condition above) in a very specific case:
+ // If the load is of an 'initial' page (e.g. about:privatebrowsing,
+ // about:newtab, etc.), was not explicitly typed in the location
+ // bar by the user, is not about:blank (because about:blank can be
+ // loaded by websites under their principal), and the current
+ // page in the browser is about:blank (indicating it is a newly
+ // created or re-created browser, e.g. because it just switched
+ // remoteness or is a new tab/window).
+ this.mBrowser.urlbarChangeTracker.startedLoad();
+ }
+ delete this.mBrowser.initialPageLoadedFromUserAction;
+ // If the browser is loading it must not be crashed anymore
+ this.mTab.removeAttribute("crashed");
+ }
+
+ if (this._shouldShowProgress(aRequest)) {
+ if (
+ !(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) &&
+ aWebProgress &&
+ aWebProgress.isTopLevel
+ ) {
+ this.mTab.setAttribute("busy", "true");
+ gBrowser._tabAttrModified(this.mTab, ["busy"]);
+ this.mTab._notselectedsinceload = !this.mTab.selected;
+ gBrowser.syncThrobberAnimations(this.mTab);
+ }
+
+ if (this.mTab.selected) {
+ gBrowser._isBusy = true;
+ }
+ }
+ } else if (aStateFlags & STATE_STOP && aStateFlags & STATE_IS_NETWORK) {
+ // since we (try to) only handle STATE_STOP of the last request,
+ // the count of open requests should now be 0
+ this.mRequestCount = 0;
+
+ let modifiedAttrs = [];
+ if (this.mTab.hasAttribute("busy")) {
+ this.mTab.removeAttribute("busy");
+ modifiedAttrs.push("busy");
+
+ // Only animate the "burst" indicating the page has loaded if
+ // the top-level page is the one that finished loading.
+ if (
+ aWebProgress.isTopLevel &&
+ !aWebProgress.isLoadingDocument &&
+ Components.isSuccessCode(aStatus) &&
+ !gBrowser.tabAnimationsInProgress &&
+ !gReduceMotion
+ ) {
+ if (this.mTab._notselectedsinceload) {
+ this.mTab.setAttribute("notselectedsinceload", "true");
+ } else {
+ this.mTab.removeAttribute("notselectedsinceload");
+ }
+
+ this.mTab.setAttribute("bursting", "true");
+ }
+ }
+
+ if (this.mTab.hasAttribute("progress")) {
+ this.mTab.removeAttribute("progress");
+ modifiedAttrs.push("progress");
+ }
+
+ if (modifiedAttrs.length) {
+ gBrowser._tabAttrModified(this.mTab, modifiedAttrs);
+ }
+
+ if (aWebProgress.isTopLevel) {
+ let isSuccessful = Components.isSuccessCode(aStatus);
+ if (!isSuccessful && !this.mTab.isEmpty) {
+ // Restore the current document's location in case the
+ // request was stopped (possibly from a content script)
+ // before the location changed.
+
+ this.mBrowser.userTypedValue = null;
+
+ let isNavigating = this.mBrowser.isNavigating;
+ if (this.mTab.selected && !isNavigating) {
+ gURLBar.setURI();
+ }
+ } else if (isSuccessful) {
+ this.mBrowser.urlbarChangeTracker.finishedLoad();
+ }
+ }
+
+ // If we don't already have an icon for this tab then clear the tab's
+ // icon. Don't do this on the initial about:blank load to prevent
+ // flickering. Don't clear the icon if we already set it from one of the
+ // known defaults. Note we use the original URL since about:newtab
+ // redirects to a prerendered page.
+ if (
+ !this.mBrowser.mIconURL &&
+ !ignoreBlank &&
+ !(originalLocation.spec in FAVICON_DEFAULTS)
+ ) {
+ this.mTab.removeAttribute("image");
+ }
+
+ // For keyword URIs clear the user typed value since they will be changed into real URIs
+ if (location.scheme == "keyword") {
+ this.mBrowser.userTypedValue = null;
+ }
+
+ if (this.mTab.selected) {
+ gBrowser._isBusy = false;
+ }
+ }
+
+ if (ignoreBlank) {
+ this._callProgressListeners(
+ "onUpdateCurrentBrowser",
+ [aStateFlags, aStatus, "", 0],
+ true,
+ false
+ );
+ } else {
+ this._callProgressListeners(
+ "onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ true,
+ false
+ );
+ }
+
+ this._callProgressListeners(
+ "onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ false
+ );
+
+ if (aStateFlags & (STATE_START | STATE_STOP)) {
+ // reset cached temporary values at beginning and end
+ this.mMessage = "";
+ this.mTotalProgress = 0;
+ }
+ this.mStateFlags = aStateFlags;
+ this.mStatus = aStatus;
+ }
+ /* eslint-enable complexity */
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // OnLocationChange is called for both the top-level content
+ // and the subframes.
+ let topLevel = aWebProgress.isTopLevel;
+
+ let isSameDocument = !!(
+ aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
+ );
+ if (topLevel) {
+ let isReload = !!(
+ aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
+ );
+ let isErrorPage = !!(
+ aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE
+ );
+
+ // We need to clear the typed value
+ // if the document failed to load, to make sure the urlbar reflects the
+ // failed URI (particularly for SSL errors). However, don't clear the value
+ // if the error page's URI is about:blank, because that causes complete
+ // loss of urlbar contents for invalid URI errors (see bug 867957).
+ // Another reason to clear the userTypedValue is if this was an anchor
+ // navigation initiated by the user.
+ // Finally, we do insert the URL if this is a same-document navigation
+ // and the user cleared the URL manually.
+ if (
+ this.mBrowser.didStartLoadSinceLastUserTyping() ||
+ (isErrorPage && aLocation.spec != "about:blank") ||
+ (isSameDocument && this.mBrowser.isNavigating) ||
+ (isSameDocument && !this.mBrowser.userTypedValue)
+ ) {
+ this.mBrowser.userTypedValue = null;
+ }
+
+ // If the tab has been set to "busy" outside the stateChange
+ // handler below (e.g. by sessionStore.navigateAndRestore), and
+ // the load results in an error page, it's possible that there
+ // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
+ // attribute being removed. In this case we should remove the
+ // attribute here.
+ if (isErrorPage && this.mTab.hasAttribute("busy")) {
+ this.mTab.removeAttribute("busy");
+ gBrowser._tabAttrModified(this.mTab, ["busy"]);
+ }
+
+ if (!isSameDocument) {
+ // If the browser was playing audio, we should remove the playing state.
+ if (this.mTab.hasAttribute("soundplaying")) {
+ clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
+ this.mTab._soundPlayingAttrRemovalTimer = 0;
+ this.mTab.removeAttribute("soundplaying");
+ gBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
+ }
+
+ // If the browser was previously muted, we should restore the muted state.
+ if (this.mTab.hasAttribute("muted")) {
+ this.mTab.linkedBrowser.mute();
+ }
+
+ if (gBrowser.isFindBarInitialized(this.mTab)) {
+ let findBar = gBrowser.getCachedFindBar(this.mTab);
+
+ // Close the Find toolbar if we're in old-style TAF mode
+ if (findBar.findMode != findBar.FIND_NORMAL) {
+ findBar.close();
+ }
+ }
+
+ // Note that we're not updating for same-document loads, despite
+ // the `title` argument to `history.pushState/replaceState`. For
+ // context, see https://bugzilla.mozilla.org/show_bug.cgi?id=585653
+ // and https://github.com/whatwg/html/issues/2174
+ if (!isReload) {
+ gBrowser.setTabTitle(this.mTab);
+ }
+
+ // Don't clear the favicon if this tab is in the pending
+ // state, as SessionStore will have set the icon for us even
+ // though we're pointed at an about:blank. Also don't clear it
+ // if the tab is in customize mode, to keep the one set by
+ // gCustomizeMode.setTab (bug 1551239). Also don't clear it
+ // if onLocationChange was triggered by a pushState or a
+ // replaceState (bug 550565) or a hash change (bug 408415).
+ if (
+ !this.mTab.hasAttribute("pending") &&
+ !this.mTab.hasAttribute("customizemode") &&
+ aWebProgress.isLoadingDocument
+ ) {
+ // Removing the tab's image here causes flickering, wait until the
+ // load is complete.
+ this.mBrowser.mIconURL = null;
+ }
+ }
+
+ let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
+ if (this.mBrowser.registeredOpenURI) {
+ let uri = this.mBrowser.registeredOpenURI;
+ gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab(
+ uri.spec,
+ userContextId
+ );
+ delete this.mBrowser.registeredOpenURI;
+ }
+ // Tabs in private windows aren't registered as "Open" so
+ // that they don't appear as switch-to-tab candidates.
+ if (
+ !isBlankPageURL(aLocation.spec) &&
+ (!PrivateBrowsingUtils.isWindowPrivate(window) ||
+ PrivateBrowsingUtils.permanentPrivateBrowsing)
+ ) {
+ gBrowser.UrlbarProviderOpenTabs.registerOpenTab(
+ aLocation.spec,
+ userContextId
+ );
+ this.mBrowser.registeredOpenURI = aLocation;
+ }
+
+ if (this.mTab != gBrowser.selectedTab) {
+ let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this.mTab);
+ if (tabCacheIndex != -1) {
+ gBrowser._tabLayerCache.splice(tabCacheIndex, 1);
+ gBrowser._getSwitcher().cleanUpTabAfterEviction(this.mTab);
+ }
+ }
+ }
+
+ if (!this.mBlank || this.mBrowser.hasContentOpener) {
+ this._callProgressListeners("onLocationChange", [
+ aWebProgress,
+ aRequest,
+ aLocation,
+ aFlags,
+ ]);
+ if (topLevel && !isSameDocument) {
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners("onContentBlockingEvent", [
+ aWebProgress,
+ null,
+ 0,
+ true,
+ ]);
+ }
+ }
+
+ if (topLevel) {
+ this.mBrowser.lastURI = aLocation;
+ this.mBrowser.lastLocationChange = Date.now();
+ }
+ }
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (this.mBlank) {
+ return;
+ }
+
+ this._callProgressListeners("onStatusChange", [
+ aWebProgress,
+ aRequest,
+ aStatus,
+ aMessage,
+ ]);
+
+ this.mMessage = aMessage;
+ }
+
+ onSecurityChange(aWebProgress, aRequest, aState) {
+ this._callProgressListeners("onSecurityChange", [
+ aWebProgress,
+ aRequest,
+ aState,
+ ]);
+ }
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
+ this._callProgressListeners("onContentBlockingEvent", [
+ aWebProgress,
+ aRequest,
+ aEvent,
+ ]);
+ }
+
+ onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
+ return this._callProgressListeners("onRefreshAttempted", [
+ aWebProgress,
+ aURI,
+ aDelay,
+ aSameURI,
+ ]);
+ }
+ }
+ TabProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsIWebProgressListener2",
+ "nsISupportsWeakReference",
+ ]);
+} // end private scope for gBrowser
+
+var StatusPanel = {
+ get panel() {
+ delete this.panel;
+ return (this.panel = document.getElementById("statuspanel"));
+ },
+
+ get isVisible() {
+ return !this.panel.hasAttribute("inactive");
+ },
+
+ update() {
+ if (BrowserHandler.kiosk) {
+ return;
+ }
+ let text;
+ let type;
+ let types = ["overLink"];
+ if (XULBrowserWindow.busyUI) {
+ types.push("status");
+ }
+ types.push("defaultStatus");
+ for (type of types) {
+ if ((text = XULBrowserWindow[type])) {
+ break;
+ }
+ }
+
+ // If it's a long data: URI that uses base64 encoding, truncate to
+ // a reasonable length rather than trying to display the entire thing.
+ // We can't shorten arbitrary URIs like this, as bidi etc might mean
+ // we need the trailing characters for display. But a base64-encoded
+ // data-URI is plain ASCII, so this is OK for status panel display.
+ // (See bug 1484071.)
+ let textCropped = false;
+ if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) {
+ text = text.substring(0, 500) + "\u2026";
+ textCropped = true;
+ }
+
+ if (this._labelElement.value != text || (text && !this.isVisible)) {
+ this.panel.setAttribute("previoustype", this.panel.getAttribute("type"));
+ this.panel.setAttribute("type", type);
+ this._label = text;
+ this._labelElement.setAttribute(
+ "crop",
+ type == "overLink" && !textCropped ? "center" : "end"
+ );
+ }
+ },
+
+ get _labelElement() {
+ delete this._labelElement;
+ return (this._labelElement = document.getElementById("statuspanel-label"));
+ },
+
+ set _label(val) {
+ if (!this.isVisible) {
+ this.panel.removeAttribute("mirror");
+ this.panel.removeAttribute("sizelimit");
+ }
+
+ if (
+ this.panel.getAttribute("type") == "status" &&
+ this.panel.getAttribute("previoustype") == "status"
+ ) {
+ // Before updating the label, set the panel's current width as its
+ // min-width to let the panel grow but not shrink and prevent
+ // unnecessary flicker while loading pages. We only care about the
+ // panel's width once it has been painted, so we can do this
+ // without flushing layout.
+ this.panel.style.minWidth =
+ window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px";
+ } else {
+ this.panel.style.minWidth = "";
+ }
+
+ if (val) {
+ this._labelElement.value = val;
+ this.panel.removeAttribute("inactive");
+ MousePosTracker.addListener(this);
+ } else {
+ this.panel.setAttribute("inactive", "true");
+ MousePosTracker.removeListener(this);
+ }
+
+ return val;
+ },
+
+ getMouseTargetRect() {
+ let container = this.panel.parentNode;
+ let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel);
+ let containerRect = window.windowUtils.getBoundsWithoutFlushing(container);
+
+ return {
+ top: panelRect.top,
+ bottom: panelRect.bottom,
+ left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left,
+ right: RTL_UI
+ ? containerRect.right
+ : containerRect.left + panelRect.width,
+ };
+ },
+
+ onMouseEnter() {
+ this._mirror();
+ },
+
+ onMouseLeave() {
+ this._mirror();
+ },
+
+ _mirror() {
+ if (this.panel.hasAttribute("mirror")) {
+ this.panel.removeAttribute("mirror");
+ } else {
+ this.panel.setAttribute("mirror", "true");
+ }
+
+ if (!this.panel.hasAttribute("sizelimit")) {
+ this.panel.setAttribute("sizelimit", "true");
+ }
+ },
+};
+
+var TabBarVisibility = {
+ _initialUpdateDone: false,
+
+ update() {
+ let toolbar = document.getElementById("TabsToolbar");
+ let collapse = false;
+ if (
+ !gBrowser /* gBrowser isn't initialized yet */ ||
+ gBrowser.tabs.length - gBrowser._removingTabs.length == 1
+ ) {
+ collapse = !window.toolbar.visible;
+ }
+
+ if (collapse == toolbar.collapsed && this._initialUpdateDone) {
+ return;
+ }
+ this._initialUpdateDone = true;
+
+ toolbar.collapsed = collapse;
+ let navbar = document.getElementById("nav-bar");
+ navbar.setAttribute("tabs-hidden", collapse);
+
+ document.getElementById("menu_closeWindow").hidden = collapse;
+ document
+ .getElementById("menu_close")
+ .setAttribute(
+ "label",
+ gTabBrowserBundle.GetStringFromName(
+ collapse ? "tabs.close" : "tabs.closeTab"
+ )
+ );
+
+ TabsInTitlebar.allowedBy("tabs-visible", !collapse);
+ },
+};
+
+var TabContextMenu = {
+ contextTab: null,
+ _updateToggleMuteMenuItems(aTab, aConditionFn) {
+ ["muted", "soundplaying"].forEach(attr => {
+ if (!aConditionFn || aConditionFn(attr)) {
+ if (aTab.hasAttribute(attr)) {
+ aTab.toggleMuteMenuItem.setAttribute(attr, "true");
+ aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true");
+ } else {
+ aTab.toggleMuteMenuItem.removeAttribute(attr);
+ aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr);
+ }
+ }
+ });
+ },
+ updateContextMenu(aPopupMenu) {
+ let tab =
+ aPopupMenu.triggerNode &&
+ (aPopupMenu.triggerNode.tab || aPopupMenu.triggerNode.closest("tab"));
+
+ this.contextTab = tab || gBrowser.selectedTab;
+
+ let disabled = gBrowser.tabs.length == 1;
+ let multiselectionContext = this.contextTab.multiselected;
+ let tabCountInfo = JSON.stringify({
+ tabCount: (multiselectionContext && gBrowser.multiSelectedTabsCount) || 1,
+ });
+
+ var menuItems = aPopupMenu.getElementsByAttribute(
+ "tbattr",
+ "tabbrowser-multiple"
+ );
+ for (let menuItem of menuItems) {
+ menuItem.disabled = disabled;
+ }
+
+ if (this.contextTab.hasAttribute("customizemode")) {
+ document.getElementById("context_openTabInWindow").disabled = true;
+ }
+
+ disabled = gBrowser.visibleTabs.length == 1;
+ menuItems = aPopupMenu.getElementsByAttribute(
+ "tbattr",
+ "tabbrowser-multiple-visible"
+ );
+ for (let menuItem of menuItems) {
+ menuItem.disabled = disabled;
+ }
+
+ // Session store
+ document.getElementById("context_undoCloseTab").disabled =
+ SessionStore.getClosedTabCount(window) == 0;
+
+ // Only one of Reload_Tab/Reload_Selected_Tabs should be visible.
+ document.getElementById("context_reloadTab").hidden = multiselectionContext;
+ document.getElementById(
+ "context_reloadSelectedTabs"
+ ).hidden = !multiselectionContext;
+
+ // Only one of pin/unpin/multiselect-pin/multiselect-unpin should be visible
+ let contextPinTab = document.getElementById("context_pinTab");
+ contextPinTab.hidden = this.contextTab.pinned || multiselectionContext;
+ let contextUnpinTab = document.getElementById("context_unpinTab");
+ contextUnpinTab.hidden = !this.contextTab.pinned || multiselectionContext;
+ let contextPinSelectedTabs = document.getElementById(
+ "context_pinSelectedTabs"
+ );
+ contextPinSelectedTabs.hidden =
+ this.contextTab.pinned || !multiselectionContext;
+ let contextUnpinSelectedTabs = document.getElementById(
+ "context_unpinSelectedTabs"
+ );
+ contextUnpinSelectedTabs.hidden =
+ !this.contextTab.pinned || !multiselectionContext;
+
+ let contextMoveTabOptions = document.getElementById(
+ "context_moveTabOptions"
+ );
+ contextMoveTabOptions.setAttribute("data-l10n-args", tabCountInfo);
+ contextMoveTabOptions.disabled = gBrowser.allTabsSelected();
+ let selectedTabs = gBrowser.selectedTabs;
+ let contextMoveTabToEnd = document.getElementById("context_moveToEnd");
+ let allSelectedTabsAdjacent = selectedTabs.every(
+ (element, index, array) => {
+ return array.length > index + 1
+ ? element._tPos + 1 == array[index + 1]._tPos
+ : true;
+ }
+ );
+ let contextTabIsSelected = this.contextTab.multiselected;
+ let visibleTabs = gBrowser.visibleTabs;
+ let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
+ let tabsToMove = contextTabIsSelected ? selectedTabs : [this.contextTab];
+ let lastTabToMove = tabsToMove[tabsToMove.length - 1];
+
+ let isLastPinnedTab = false;
+ if (lastTabToMove.pinned) {
+ let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove);
+ isLastPinnedTab = !sibling || !sibling.pinned;
+ }
+ contextMoveTabToEnd.disabled =
+ (lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
+ allSelectedTabsAdjacent;
+ let contextMoveTabToStart = document.getElementById("context_moveToStart");
+ let isFirstTab =
+ tabsToMove[0] == visibleTabs[0] ||
+ tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
+ contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
+
+ // Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
+ document.getElementById(
+ "context_duplicateTab"
+ ).hidden = multiselectionContext;
+ document.getElementById(
+ "context_duplicateTabs"
+ ).hidden = !multiselectionContext;
+
+ // Disable "Close Tabs to the Right" if there are no tabs
+ // following it.
+ document.getElementById(
+ "context_closeTabsToTheEnd"
+ ).disabled = !gBrowser.getTabsToTheEndFrom(this.contextTab).length;
+
+ // Disable "Close other Tabs" if there are no unpinned tabs.
+ let unpinnedTabsToClose = multiselectionContext
+ ? gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length
+ : gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned)
+ .length;
+ document.getElementById("context_closeOtherTabs").disabled =
+ unpinnedTabsToClose < 1;
+
+ // Update the close item with how many tabs will close.
+ document
+ .getElementById("context_closeTab")
+ .setAttribute("data-l10n-args", tabCountInfo);
+
+ // Hide "Bookmark Tab" for multiselection.
+ // Update its state if visible.
+ let bookmarkTab = document.getElementById("context_bookmarkTab");
+ bookmarkTab.hidden = multiselectionContext;
+
+ // Show "Bookmark Selected Tabs" in a multiselect context and hide it otherwise.
+ let bookmarkMultiSelectedTabs = document.getElementById(
+ "context_bookmarkSelectedTabs"
+ );
+ bookmarkMultiSelectedTabs.hidden = !multiselectionContext;
+
+ let toggleMute = document.getElementById("context_toggleMuteTab");
+ let toggleMultiSelectMute = document.getElementById(
+ "context_toggleMuteSelectedTabs"
+ );
+
+ // Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible
+ toggleMute.hidden = multiselectionContext;
+ toggleMultiSelectMute.hidden = !multiselectionContext;
+
+ // Adjust the state of the toggle mute menu item.
+ if (this.contextTab.hasAttribute("activemedia-blocked")) {
+ toggleMute.label = gNavigatorBundle.getString("playTab.label");
+ toggleMute.accessKey = gNavigatorBundle.getString("playTab.accesskey");
+ } else if (this.contextTab.hasAttribute("muted")) {
+ toggleMute.label = gNavigatorBundle.getString("unmuteTab.label");
+ toggleMute.accessKey = gNavigatorBundle.getString("unmuteTab.accesskey");
+ } else {
+ toggleMute.label = gNavigatorBundle.getString("muteTab.label");
+ toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey");
+ }
+
+ // Adjust the state of the toggle mute menu item for multi-selected tabs.
+ if (this.contextTab.hasAttribute("activemedia-blocked")) {
+ toggleMultiSelectMute.label = gNavigatorBundle.getString(
+ "playTabs.label"
+ );
+ toggleMultiSelectMute.accessKey = gNavigatorBundle.getString(
+ "playTabs.accesskey"
+ );
+ } else if (this.contextTab.hasAttribute("muted")) {
+ toggleMultiSelectMute.label = gNavigatorBundle.getString(
+ "unmuteSelectedTabs2.label"
+ );
+ toggleMultiSelectMute.accessKey = gNavigatorBundle.getString(
+ "unmuteSelectedTabs2.accesskey"
+ );
+ } else {
+ toggleMultiSelectMute.label = gNavigatorBundle.getString(
+ "muteSelectedTabs2.label"
+ );
+ toggleMultiSelectMute.accessKey = gNavigatorBundle.getString(
+ "muteSelectedTabs2.accesskey"
+ );
+ }
+
+ this.contextTab.toggleMuteMenuItem = toggleMute;
+ this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute;
+ this._updateToggleMuteMenuItems(this.contextTab);
+
+ let selectAllTabs = document.getElementById("context_selectAllTabs");
+ selectAllTabs.disabled = gBrowser.allTabsSelected();
+
+ this.contextTab.addEventListener("TabAttrModified", this);
+ aPopupMenu.addEventListener("popuphiding", this);
+
+ gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
+
+ document.getElementById("context_reopenInContainer").hidden =
+ !Services.prefs.getBoolPref("privacy.userContext.enabled", false) ||
+ PrivateBrowsingUtils.isWindowPrivate(window);
+ },
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "popuphiding":
+ gBrowser.removeEventListener("TabAttrModified", this);
+ aEvent.target.removeEventListener("popuphiding", this);
+ break;
+ case "TabAttrModified":
+ let tab = aEvent.target;
+ this._updateToggleMuteMenuItems(tab, attr =>
+ aEvent.detail.changed.includes(attr)
+ );
+ break;
+ }
+ },
+ createReopenInContainerMenu(event) {
+ createUserContextMenu(event, {
+ isContextMenu: true,
+ excludeUserContextId: this.contextTab.getAttribute("usercontextid"),
+ });
+ },
+ duplicateSelectedTabs() {
+ let tabsToDuplicate = gBrowser.selectedTabs;
+ let newIndex = tabsToDuplicate[tabsToDuplicate.length - 1]._tPos + 1;
+ for (let tab of tabsToDuplicate) {
+ let newTab = SessionStore.duplicateTab(window, tab);
+ gBrowser.moveTabTo(newTab, newIndex++);
+ }
+ },
+ reopenInContainer(event) {
+ let userContextId = parseInt(
+ event.target.getAttribute("data-usercontextid")
+ );
+ let reopenedTabs = this.contextTab.multiselected
+ ? gBrowser.selectedTabs
+ : [this.contextTab];
+
+ for (let tab of reopenedTabs) {
+ if (tab.getAttribute("usercontextid") == userContextId) {
+ continue;
+ }
+
+ /* Create a triggering principal that is able to load the new tab
+ For content principals that are about: chrome: or resource: we need system to load them.
+ Anything other than system principal needs to have the new userContextId.
+ */
+ let triggeringPrincipal;
+
+ if (tab.linkedPanel) {
+ triggeringPrincipal = tab.linkedBrowser.contentPrincipal;
+ } else {
+ // For lazy tab browsers, get the original principal
+ // from SessionStore
+ let tabState = JSON.parse(SessionStore.getTabState(tab));
+ try {
+ triggeringPrincipal = E10SUtils.deserializePrincipal(
+ tabState.triggeringPrincipal_base64
+ );
+ } catch (ex) {
+ continue;
+ }
+ }
+
+ if (!triggeringPrincipal || triggeringPrincipal.isNullPrincipal) {
+ // Ensure that we have a null principal if we couldn't
+ // deserialize it (for lazy tab browsers) ...
+ // This won't always work however is safe to use.
+ triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal(
+ { userContextId }
+ );
+ } else if (triggeringPrincipal.isContentPrincipal) {
+ triggeringPrincipal = Services.scriptSecurityManager.principalWithOA(
+ triggeringPrincipal,
+ {
+ userContextId,
+ }
+ );
+ }
+
+ let newTab = gBrowser.addTab(tab.linkedBrowser.currentURI.spec, {
+ userContextId,
+ pinned: tab.pinned,
+ index: tab._tPos + 1,
+ triggeringPrincipal,
+ });
+
+ if (gBrowser.selectedTab == tab) {
+ gBrowser.selectedTab = newTab;
+ }
+ if (tab.muted && !newTab.muted) {
+ newTab.toggleMuteAudio(tab.muteReason);
+ }
+ }
+ },
+
+ closeContextTabs(event) {
+ if (this.contextTab.multiselected) {
+ gBrowser.removeMultiSelectedTabs();
+ } else {
+ gBrowser.removeTab(this.contextTab, { animate: true });
+ }
+ },
+};
diff --git a/browser/base/content/test/about/.eslintrc.js b/browser/base/content/test/about/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/about/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/about/POSTSearchEngine.xml b/browser/base/content/test/about/POSTSearchEngine.xml
new file mode 100644
index 0000000000..f2f884cf51
--- /dev/null
+++ b/browser/base/content/test/about/POSTSearchEngine.xml
@@ -0,0 +1,6 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>POST Search</ShortName>
+ <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/base/content/test/about/print_postdata.sjs">
+ <Param name="searchterms" value="{searchTerms}"/>
+ </Url>
+</OpenSearchDescription>
diff --git a/browser/base/content/test/about/browser.ini b/browser/base/content/test/about/browser.ini
new file mode 100644
index 0000000000..a5e9048843
--- /dev/null
+++ b/browser/base/content/test/about/browser.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+support-files =
+ head.js
+ print_postdata.sjs
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine.xml
+ slow_loading_page.sjs
+ POSTSearchEngine.xml
+ dummy_page.html
+prefs =
+ browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
+
+[browser_aboutCertError.js]
+[browser_aboutCertError_clockSkew.js]
+[browser_aboutCertError_exception.js]
+[browser_aboutCertError_mitm.js]
+[browser_aboutCertError_multiple_errors.js]
+[browser_aboutCertError_noSubjectAltName.js]
+[browser_aboutCertError_telemetry.js]
+[browser_aboutDialog_distribution.js]
+[browser_aboutHome_search_POST.js]
+[browser_aboutHome_search_composing.js]
+[browser_aboutHome_search_searchbar.js]
+[browser_aboutHome_search_suggestion.js]
+skip-if = os == "mac" || (os == "linux" && (!debug || bits == 64)) || (os == 'win' && os_version == '10.0' && bits == 64 && !debug) # Bug 1399648, bug 1402502
+[browser_aboutHome_search_telemetry.js]
+[browser_aboutNetError.js]
+[browser_aboutNetError_csp_iframe.js]
+support-files =
+ iframe_page_csp.html
+ csp_iframe.sjs
+[browser_aboutNetError_xfo_iframe.js]
+support-files =
+ iframe_page_xfo.html
+ xfo_iframe.sjs
+[browser_aboutNewTab_bookmarksToolbar.js]
+[browser_aboutNewTab_bookmarksToolbarEmpty.js]
+skip-if = tsan # Bug 1676326, highly frequent on TSan
+[browser_aboutNewTab_bookmarksToolbarNewWindow.js]
+skip-if = fission && tsan # Bug 1674948, perma on Fission+TSan
+[browser_aboutNewTab_bookmarksToolbarPrefs.js]
+[browser_aboutNewTab_defaultBrowserNotification.js]
+skip-if = debug || asan || tsan || ccov # Default browser checks are skipped on debug builds, bug 1660723
+[browser_aboutStopReload.js]
+[browser_aboutSupport.js]
+[browser_aboutSupport_newtab_security_state.js]
+[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..d0ab759831
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError.js
@@ -0,0 +1,604 @@
+/* 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.import(
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+
+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) {
+ Assert.equal(
+ returnButton.getAttribute("autofocus"),
+ "true",
+ "returnButton has autofocus"
+ );
+ }
+ // 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.loadURI(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.loadURI(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;
+ let shortDescText = doc.getElementById("errorShortDescText");
+ Assert.ok(
+ shortDescText.textContent.includes("expired.example.com"),
+ "Should list hostname in error message."
+ );
+
+ 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 el;
+ await ContentTaskUtils.waitForCondition(() => {
+ el = doc.getElementById("errorCode");
+ return el.textContent != "";
+ }, "error code has been set inside the advanced button panel");
+
+ return { textContent: el.textContent, tagName: el.tagName };
+ });
+ is(
+ message.textContent,
+ "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Correct error message found"
+ );
+ is(message.tagName, "a", "Error message is a link");
+
+ message = await SpecialPowers.spawn(bc, [], async function() {
+ let doc = content.document;
+ let errorCode = doc.getElementById("errorCode");
+ errorCode.click();
+ let div = doc.getElementById("certificateErrorDebugInformation");
+ let text = doc.getElementById("certificateErrorText");
+
+ let serhelper = Cc[
+ "@mozilla.org/network/serialization-helper;1"
+ ].getService(Ci.nsISerializationHelper);
+ let serializable = content.docShell.failedChannel.securityInfo
+ .QueryInterface(Ci.nsITransportSecurityInfo)
+ .QueryInterface(Ci.nsISerializable);
+ let serializedSecurityInfo = serhelper.serializeToString(serializable);
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: text.textContent,
+ securityInfoAsString: serializedSecurityInfo,
+ };
+ });
+ 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 = getCertChain(message.securityInfoAsString);
+ ok(message.text.includes(certChain), "Found certificate chain");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function checkAdvancedDetailsForHSTS() {
+ info(
+ "Loading a bad STS cert page and verifying the advanced details section"
+ );
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_STS_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ let message = await SpecialPowers.spawn(bc, [], async function() {
+ let doc = content.document;
+ let advancedButton = doc.getElementById("advancedButton");
+ advancedButton.click();
+
+ // Wait until fluent sets the errorCode inner text.
+ let ec;
+ await ContentTaskUtils.waitForCondition(() => {
+ ec = doc.getElementById("errorCode");
+ return ec.textContent != "";
+ }, "error code has been set inside the advanced button panel");
+
+ let cdl = doc.getElementById("cert_domain_link");
+ return {
+ ecTextContent: ec.textContent,
+ ecTagName: ec.tagName,
+ cdlTextContent: cdl.textContent,
+ cdlTagName: cdl.tagName,
+ };
+ });
+
+ const badStsUri = Services.io.newURI(BAD_STS_CERT);
+ is(
+ message.ecTextContent,
+ "SSL_ERROR_BAD_CERT_DOMAIN",
+ "Correct error message found"
+ );
+ is(message.ecTagName, "a", "Error message is a link");
+ const url = badStsUri.prePath.slice(badStsUri.prePath.indexOf(".") + 1);
+ is(message.cdlTextContent, url, "Correct cert_domain_link contents found");
+ is(message.cdlTagName, "a", "cert_domain_link is a link");
+
+ message = await SpecialPowers.spawn(bc, [], async function() {
+ let doc = content.document;
+
+ let errorCode = doc.getElementById("errorCode");
+ errorCode.click();
+ let div = doc.getElementById("certificateErrorDebugInformation");
+ let text = doc.getElementById("certificateErrorText");
+
+ let serhelper = Cc[
+ "@mozilla.org/network/serialization-helper;1"
+ ].getService(Ci.nsISerializationHelper);
+ let serializable = content.docShell.failedChannel.securityInfo
+ .QueryInterface(Ci.nsITransportSecurityInfo)
+ .QueryInterface(Ci.nsISerializable);
+ let serializedSecurityInfo = serhelper.serializeToString(serializable);
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: text.textContent,
+ securityInfoAsString: serializedSecurityInfo,
+ };
+ });
+ 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 = getCertChain(message.securityInfoAsString);
+ 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 checkCautionClass() {
+ info("Checking that are potentially more dangerous get a 'caution' class");
+ 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];
+ }
+
+ await SpecialPowers.spawn(bc, [useFrame], async function(subFrame) {
+ Assert.equal(
+ content.document.body.classList.contains("caution"),
+ !subFrame,
+ `Cert error body has ${subFrame ? "no" : ""} caution class`
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ tab = await openErrorPage(BAD_STS_CERT, useFrame);
+ bc = tab.linkedBrowser.browsingContext;
+ await SpecialPowers.spawn(bc, [], async function() {
+ Assert.ok(
+ !content.document.body.classList.contains("caution"),
+ "Cert error body has no caution class"
+ );
+ });
+
+ 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 => {
+ let titleText = content.document.querySelector(".title-text");
+ await ContentTaskUtils.waitForCondition(
+ () => titleText.textContent,
+ "Error page title is initialized"
+ );
+ let titleContent = titleText.textContent;
+ if (_useFrame) {
+ ok(
+ titleContent.endsWith("Security Issue"),
+ "Did Not Connect: Potential Security Issue"
+ );
+ } else {
+ ok(
+ titleContent.endsWith("Risk Ahead"),
+ "Warning: Potential Security Risk Ahead"
+ );
+ }
+ });
+ 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;
+
+ // aboutNetError.js 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.
+ await ContentTaskUtils.waitForCondition(() => {
+ let elements = [
+ doc.querySelector(".title-text"),
+ doc.getElementById("errorCode"),
+ ];
+
+ return elements.every(elem => !!elem.textContent.trim().length);
+ });
+
+ let titleText = doc.querySelector(".title-text");
+ Assert.ok(
+ titleText.textContent.endsWith("Security Issue"),
+ "Title shows Did Not Connect: Potential Security Issue"
+ );
+
+ let el = doc.getElementById("errorCode");
+
+ Assert.equal(
+ el.textContent,
+ "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Correct error message found"
+ );
+ Assert.equal(el.tagName, "a", "Error message is 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;
+
+ // Wait until fluent sets the errorCode inner text.
+ let el;
+ await ContentTaskUtils.waitForCondition(() => {
+ el = doc.getElementById("errorCode");
+ return el.textContent != "";
+ }, "error code has been set inside the advanced button panel");
+ Assert.equal(
+ el.textContent,
+ "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Correct error message found"
+ );
+ Assert.equal(el.tagName, "a", "Error message is a link");
+
+ let titleText = doc.querySelector(".title-text");
+ Assert.equal(
+ titleText.textContent,
+ "Warning: Potential Security Risk Ahead"
+ );
+
+ let shortDescText = doc.getElementById("errorShortDescText");
+ Assert.ok(
+ shortDescText.textContent.includes("expired.example.com"),
+ "Should list hostname in error message."
+ );
+
+ let whatToDoText = doc.getElementById("errorWhatToDoText");
+ Assert.ok(
+ whatToDoText.textContent.includes("expired.example.com"),
+ "Should list hostname in what to do text."
+ );
+ });
+
+ 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..51e7c288ed
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_clockSkew.js
@@ -0,0 +1,155 @@
+/* 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("errorShortDescText");
+ let systemDateDiv = doc.getElementById("wrongSystemTime_systemDate1");
+ 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,
+ systemDate: systemDateDiv.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)
+ );
+
+ let formatter = new Intl.DateTimeFormat("default");
+
+ // 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 = formatter.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.systemDate.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..cb6a3de3eb
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_exception.js
@@ -0,0 +1,222 @@
+/* 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 securityInfoAsString = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function() {
+ let doc = content.document;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ exceptionButton.click();
+ let serhelper = Cc[
+ "@mozilla.org/network/serialization-helper;1"
+ ].getService(Ci.nsISerializationHelper);
+ let serializable = content.docShell.failedChannel.securityInfo
+ .QueryInterface(Ci.nsITransportSecurityInfo)
+ .QueryInterface(Ci.nsISerializable);
+ return serhelper.serializeToString(serializable);
+ }
+ );
+
+ 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 cert = getSecurityInfo(securityInfoAsString).serverCert;
+ let hasException = certOverrideService.hasMatchingOverride(
+ "expired.example.com",
+ -1,
+ cert,
+ {},
+ isTemporary
+ );
+ ok(hasException, "Has stored an exception for the page.");
+ is(
+ isTemporary.value,
+ !permanentOverride,
+ `Has stored a ${
+ permanentOverride ? "permanent" : "temporary"
+ } exception for the page.`
+ );
+
+ certOverrideService.clearValidityOverride("expired.example.com", -1);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ Services.prefs.clearUserPref(PREF_PERMANENT_OVERRIDE);
+});
+
+add_task(async function checkBadStsCert() {
+ info("Loading a badStsCert and making sure exception button doesn't show up");
+
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_STS_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [{ frame: useFrame }], async function({
+ frame,
+ }) {
+ let doc = frame
+ ? content.document.querySelector("iframe").contentDocument
+ : content.document;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ ok(
+ ContentTaskUtils.is_hidden(exceptionButton),
+ "Exception button is hidden."
+ );
+ });
+
+ let message = await SpecialPowers.spawn(
+ browser,
+ [{ frame: useFrame }],
+ async function({ frame }) {
+ let doc = frame
+ ? content.document.querySelector("iframe").contentDocument
+ : content.document;
+ let advancedButton = doc.getElementById("advancedButton");
+ advancedButton.click();
+
+ // aboutNetError.js 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.querySelector(
+ ".exceptionDialogButtonContainer"
+ );
+ ok(
+ ContentTaskUtils.is_hidden(exceptionButton),
+ "Exception button is hidden."
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ Services.prefs.clearUserPref("security.certerror.hideAddException");
+});
+
+add_task(async function checkhideAddExceptionButtonInFrames() {
+ info("Loading a bad cert page in a frame and verifying it's hidden.");
+ let tab = await openErrorPage(BAD_CERT, true);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ let doc = content.document.querySelector("iframe").contentDocument;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ ok(
+ ContentTaskUtils.is_hidden(exceptionButton),
+ "Exception button is hidden."
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/about/browser_aboutCertError_mitm.js b/browser/base/content/test/about/browser_aboutCertError_mitm.js
new file mode 100644
index 0000000000..98a36900d3
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_mitm.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PREF_MITM_PRIMING = "security.certerrors.mitm.priming.enabled";
+const PREF_MITM_PRIMING_ENDPOINT = "security.certerrors.mitm.priming.endpoint";
+const PREF_MITM_CANARY_ISSUER = "security.pki.mitm_canary_issuer";
+const PREF_MITM_AUTO_ENABLE_ENTERPRISE_ROOTS =
+ "security.certerrors.mitm.auto_enable_enterprise_roots";
+const PREF_ENTERPRISE_ROOTS = "security.enterprise_roots.enabled";
+
+const UNKNOWN_ISSUER = "https://untrusted.example.com";
+
+// Check that basic MitM priming works and the MitM error page is displayed successfully.
+add_task(async function checkMitmPriming() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_MITM_PRIMING, true],
+ [PREF_MITM_PRIMING_ENDPOINT, UNKNOWN_ISSUER],
+ ],
+ });
+
+ let browser;
+ let certErrorLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, UNKNOWN_ISSUER);
+ browser = gBrowser.selectedBrowser;
+ // The page will reload by itself after the initial canary request, so we wait
+ // until the AboutNetErrorLoad event has happened twice.
+ certErrorLoaded = new Promise(resolve => {
+ let loaded = 0;
+ let removeEventListener = BrowserTestUtils.addContentEventListener(
+ browser,
+ "AboutNetErrorLoad",
+ () => {
+ if (++loaded == 2) {
+ removeEventListener();
+ resolve();
+ }
+ },
+ { capture: false, wantUntrusted: true }
+ );
+ });
+ },
+ false
+ );
+
+ await certErrorLoaded;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ is(
+ content.document.body.getAttribute("code"),
+ "MOZILLA_PKIX_ERROR_MITM_DETECTED",
+ "MitM error page has loaded."
+ );
+ });
+
+ ok(true, "Successfully loaded the MitM error page.");
+
+ is(
+ Services.prefs.getStringPref(PREF_MITM_CANARY_ISSUER),
+ "CN=Unknown CA",
+ "Stored the correct issuer"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ let mitmName1 = content.document.querySelector(
+ "#errorShortDescText .mitm-name"
+ );
+ ok(
+ ContentTaskUtils.is_visible(mitmName1),
+ "Potential man in the middle is displayed"
+ );
+ is(mitmName1.textContent, "Unknown CA", "Shows the name of the issuer.");
+
+ let mitmName2 = content.document.querySelector(
+ "#errorWhatToDoText .mitm-name"
+ );
+ ok(
+ ContentTaskUtils.is_visible(mitmName2),
+ "Potential man in the middle is displayed"
+ );
+ is(mitmName2.textContent, "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_multiple_errors.js b/browser/base/content/test/about/browser_aboutCertError_multiple_errors.js
new file mode 100644
index 0000000000..4460b52b3e
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_multiple_errors.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const EXPIRED_CERT = "https://expired.example.com/";
+const BAD_CERT = "https://mismatch.badcertdomain.example.com/";
+
+const kErrors = [
+ "MOZILLA_PKIX_ERROR_MITM_DETECTED",
+ "SEC_ERROR_UNKNOWN_ISSUER",
+ "SEC_ERROR_CA_CERT_INVALID",
+ "SEC_ERROR_UNTRUSTED_ISSUER",
+ "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED",
+ "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE",
+ "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT",
+ "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED",
+];
+
+/**
+ * This is a quasi-unit test style test to check what happens
+ * when we encounter certificates with multiple problems.
+ *
+ * We should consistently display the most urgent message first.
+ */
+add_task(async function test_expired_bad_cert() {
+ let tab = await openErrorPage(EXPIRED_CERT);
+ const kExpiryLabel = "cert-error-expired-now";
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [kExpiryLabel, kErrors], async function(
+ knownExpiryLabel,
+ errorCodes
+ ) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ !!content.document.querySelectorAll(`#badCertTechnicalInfo label`)
+ .length
+ );
+ // First check that for this real cert, which is simply expired and nothing else,
+ // we show the expiry info:
+ let rightLabel = content.document.querySelector(
+ `#badCertTechnicalInfo label[data-l10n-id="${knownExpiryLabel}"]`
+ );
+ ok(rightLabel, "Expected a label with the right contents.");
+ info(content.document.querySelector("#badCertTechnicalInfo").innerHTML);
+
+ const kExpiredErrors = errorCodes.map(err => {
+ return Cu.cloneInto(
+ {
+ errorCodeString: err,
+ isUntrusted: true,
+ isNotValidAtThisTime: true,
+ validNotBefore: 0,
+ validNotAfter: 86400000,
+ },
+ content.window
+ );
+ });
+ for (let err of kExpiredErrors) {
+ // Test hack: invoke the content-privileged helper method with the fake cert info.
+ await Cu.waiveXrays(content.window).setTechnicalDetailsOnCertError(err);
+ let allLabels = content.document.querySelectorAll(
+ "#badCertTechnicalInfo label"
+ );
+ ok(
+ allLabels.length,
+ "There should be an advanced technical description for " +
+ err.errorCodeString
+ );
+ for (let label of allLabels) {
+ let id = label.getAttribute("data-l10n-id");
+ ok(
+ id,
+ `Label for ${err.errorCodeString} should have data-l10n-id (was: ${id}).`
+ );
+ isnot(
+ id,
+ knownExpiryLabel,
+ `Label for ${err.errorCodeString} should not be about expiry.`
+ );
+ }
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * The same as above, but now for alt-svc domain mismatch certs.
+ */
+add_task(async function test_alt_svc_bad_cert() {
+ let tab = await openErrorPage(BAD_CERT);
+ const kErrKnownPrefix = "cert-error-domain-mismatch";
+ const kErrKnownAlt = "cert-error-domain-mismatch-single-nolink";
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(
+ browser,
+ [kErrKnownAlt, kErrKnownPrefix, kErrors],
+ async function(knownAlt, knownAltPrefix, errorCodes) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ !!content.document.querySelectorAll(`#badCertTechnicalInfo label`)
+ .length
+ );
+ // First check that for this real cert, which is simply for the wrong domain and nothing else,
+ // we show the alt-svc info:
+ let rightLabel = content.document.querySelector(
+ `#badCertTechnicalInfo label[data-l10n-id="${knownAlt}"]`
+ );
+ ok(rightLabel, "Expected a label with the right contents.");
+ info(content.document.querySelector("#badCertTechnicalInfo").innerHTML);
+
+ const kAltSvcErrors = errorCodes.map(err => {
+ return Cu.cloneInto(
+ {
+ errorCodeString: err,
+ isUntrusted: true,
+ isDomainMismatch: true,
+ },
+ content.window
+ );
+ });
+ for (let err of kAltSvcErrors) {
+ // Test hack: invoke the content-privileged helper method with the fake cert info.
+ await Cu.waiveXrays(content.window).setTechnicalDetailsOnCertError(err);
+ let allLabels = content.document.querySelectorAll(
+ "#badCertTechnicalInfo label"
+ );
+ ok(
+ allLabels.length,
+ "There should be an advanced technical description for " +
+ err.errorCodeString
+ );
+ for (let label of allLabels) {
+ let id = label.getAttribute("data-l10n-id");
+ ok(
+ id,
+ `Label for ${err.errorCodeString} should have data-l10n-id (was: ${id}).`
+ );
+ isnot(
+ id,
+ knownAlt,
+ `Label for ${err.errorCodeString} should not mention other domains.`
+ );
+ ok(
+ !id.startsWith(knownAltPrefix),
+ `Label shouldn't start with ${knownAltPrefix}`
+ );
+ }
+ }
+ }
+ );
+ await BrowserTestUtils.removeTab(tab);
+});
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_telemetry.js b/browser/base/content/test/about/browser_aboutCertError_telemetry.js
new file mode 100644
index 0000000000..5f75e2b0d3
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_telemetry.js
@@ -0,0 +1,160 @@
+/* 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..253b5025fa
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutDialog_distribution.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../../../../toolkit/mozapps/update/tests/browser/head.js */
+
+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();
+
+ await BrowserTestUtils.waitForCondition(
+ () => aboutDialog.document.getElementById("currentChannel").value != "",
+ "Waiting for init to complete"
+ );
+
+ let distroIdField = aboutDialog.document.getElementById("distributionId");
+
+ is(distroIdField.value, "");
+ isnot(distroIdField.style.display, "block");
+
+ let distroField = aboutDialog.document.getElementById("distribution");
+ 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();
+
+ await BrowserTestUtils.waitForCondition(
+ () => aboutDialog.document.getElementById("currentChannel").value != "",
+ "Waiting for init to complete"
+ );
+
+ 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..8fb777e5f0
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_POST.js
@@ -0,0 +1,88 @@
+/* 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");
+
+ let currEngine = await Services.search.getDefault();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async browser => {
+ let observerPromise = new Promise(resolve => {
+ let searchObserver = async function search_observer(
+ subject,
+ topic,
+ data
+ ) {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ info("Observer: " + data + " for " + engine.name);
+
+ if (data != "engine-added") {
+ return;
+ }
+
+ if (engine.name != "POST Search") {
+ return;
+ }
+
+ Services.obs.removeObserver(
+ searchObserver,
+ "browser-search-engine-modified"
+ );
+
+ resolve(engine);
+ };
+
+ Services.obs.addObserver(
+ searchObserver,
+ "browser-search-engine-modified"
+ );
+ });
+
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ Services.search.addOpenSearchEngine(
+ "http://test:80/browser/browser/base/content/test/about/POSTSearchEngine.xml",
+ null
+ );
+
+ engine = await observerPromise;
+ Services.search.setDefault(engine);
+ 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);
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {}
+ }
+ );
+});
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..7668e8a371
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_composing.js
@@ -0,0 +1,128 @@
+/* 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 BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async function(browser) {
+ // Add a test engine that provides suggestions and switch to it.
+ let currEngine = await Services.search.getDefault();
+
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ engine = await promiseNewEngine("searchSuggestionEngine.xml");
+ await Services.search.setDefault(engine);
+ return engine.name;
+ });
+
+ // Clear any search history results
+ await new Promise((resolve, reject) => {
+ FormHistory.update(
+ { op: "remove" },
+ {
+ handleError(error) {
+ reject(error);
+ },
+ handleCompletion(reason) {
+ if (!reason) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ }
+ );
+ });
+
+ 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 new Promise(resolve => {
+ let observer = new content.MutationObserver(() => {
+ if (input.getAttribute("aria-expanded") == "true") {
+ observer.disconnect();
+ ok(!table.hidden, "Search suggestion table unhidden");
+ resolve();
+ }
+ });
+ observer.observe(input, {
+ attributes: true,
+ attributeFilter: ["aria-expanded"],
+ });
+ });
+
+ 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;
+
+ Services.search.setDefault(currEngine);
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {}
+ }
+ );
+});
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..d6250191f8
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_searchbar.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ChromeUtils.import(
+ "resource://testing-common/CustomizableUITestUtils.jsm",
+ this
+);
+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..6fe9aa22f0
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_suggestion.js
@@ -0,0 +1,76 @@
+/* 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 BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async function(browser) {
+ // Add a test engine that provides suggestions and switch to it.
+ let currEngine = await Services.search.getDefault();
+
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ engine = await promiseNewEngine("searchSuggestionEngine.xml");
+ await Services.search.setDefault(engine);
+ 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 new Promise(resolve => {
+ let observer = new content.MutationObserver(() => {
+ if (input.getAttribute("aria-expanded") == "true") {
+ observer.disconnect();
+ ok(!table.hidden, "Search suggestion table unhidden");
+ resolve();
+ }
+ });
+ observer.observe(input, {
+ attributes: true,
+ attributeFilter: ["aria-expanded"],
+ });
+ });
+ });
+
+ // 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 Services.search.setDefault(currEngine);
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {}
+ }
+ );
+});
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..e53bb31ed9
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutHome_search_telemetry.js
@@ -0,0 +1,92 @@
+/* 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 BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:home" },
+ async function(browser) {
+ let currEngine = await Services.search.getDefault();
+
+ let engine;
+ await promiseContentSearchChange(browser, async () => {
+ engine = await promiseNewEngine("searchSuggestionEngine.xml");
+ await Services.search.setDefault(engine);
+ 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 Services.search.setDefault(currEngine);
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {}
+ }
+ );
+});
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..c265a5eb52
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError.js
@@ -0,0 +1,302 @@
+/* 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/";
+
+// This includes all the cipher suite prefs we have.
+const CIPHER_SUITE_PREFS = [
+ "security.ssl3.dhe_rsa_aes_128_sha",
+ "security.ssl3.dhe_rsa_aes_256_sha",
+ "security.ssl3.ecdhe_ecdsa_aes_128_gcm_sha256",
+ "security.ssl3.ecdhe_ecdsa_aes_128_sha",
+ "security.ssl3.ecdhe_ecdsa_aes_256_gcm_sha384",
+ "security.ssl3.ecdhe_ecdsa_aes_256_sha",
+ "security.ssl3.ecdhe_ecdsa_chacha20_poly1305_sha256",
+ "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256",
+ "security.ssl3.ecdhe_rsa_aes_128_sha",
+ "security.ssl3.ecdhe_rsa_aes_256_gcm_sha384",
+ "security.ssl3.ecdhe_rsa_aes_256_sha",
+ "security.ssl3.ecdhe_rsa_chacha20_poly1305_sha256",
+ "security.ssl3.rsa_aes_128_sha",
+ "security.ssl3.rsa_aes_256_sha",
+ "security.ssl3.rsa_aes_128_gcm_sha256",
+ "security.ssl3.rsa_aes_256_gcm_sha384",
+ "security.ssl3.rsa_des_ede3_sha",
+ "security.tls13.aes_128_gcm_sha256",
+ "security.tls13.aes_256_gcm_sha384",
+ "security.tls13.chacha20_poly1305_sha256",
+];
+
+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("security.certerrors.tls.version.show-override");
+}
+
+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"
+ );
+
+ // Just twiddling version will trigger the TLS 1.0 offer. So to test the
+ // broader UX, disable all cipher suites to trigger SSL_ERROR_SSL_DISABLED.
+ // This can be removed when security.tls.version.enable-deprecated is.
+ CIPHER_SUITE_PREFS.forEach(suitePref => {
+ Services.prefs.setBoolPref(suitePref, false);
+ });
+
+ // 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");
+ ok(
+ ContentTaskUtils.is_visible(prefResetButton),
+ "prefResetButton should be visible"
+ );
+ is(
+ prefResetButton.getAttribute("autofocus"),
+ "true",
+ "prefResetButton has autofocus"
+ );
+ prefResetButton.click();
+ });
+
+ info("Waiting for the page to load after the click");
+ await finalLoadComplete;
+
+ CIPHER_SUITE_PREFS.forEach(suitePref => {
+ Services.prefs.clearUserPref(suitePref);
+ });
+ 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 learnMoreLink = doc.getElementById("learnMoreLink");
+ ok(
+ ContentTaskUtils.is_visible(learnMoreLink),
+ "Learn More link is visible"
+ );
+ is(learnMoreLink.getAttribute("href"), _baseURL + "connection-not-secure");
+ });
+
+ resetPrefs();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function checkEnable10() {
+ info(
+ "Load a page with a deprecated TLS version, an option to enable TLS 1.0 is offered and it works"
+ );
+
+ Services.prefs.setIntPref("security.tls.version.min", 3);
+ // Disable TLS 1.3 so that we trigger a SSL_ERROR_UNSUPPORTED_VERSION.
+ // As NSS generates an alert rather than negotiating a lower version
+ // if we use the supported_versions extension from TLS 1.3.
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+
+ 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;
+
+ // Setup an observer for the target page.
+ const finalLoadComplete = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ TLS10_PAGE
+ );
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+
+ const enableTls10Button = doc.getElementById("enableTls10Button");
+ ok(
+ ContentTaskUtils.is_visible(enableTls10Button),
+ "Option to re-enable TLS 1.0 is visible"
+ );
+ enableTls10Button.click();
+
+ // It should not also offer to reset preferences instead.
+ const prefResetButton = doc.getElementById("prefResetButton");
+ ok(
+ !ContentTaskUtils.is_visible(prefResetButton),
+ "prefResetButton should NOT be visible"
+ );
+ });
+
+ info("Waiting for the TLS 1.0 page to load after the click");
+ await finalLoadComplete;
+
+ resetPrefs();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function dontOffer10WhenAlreadyEnabled() {
+ info("An option to enable TLS 1.0 is not offered if already enabled");
+
+ Services.prefs.setIntPref("security.tls.version.min", 3);
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+ Services.prefs.setBoolPref("security.tls.version.enable-deprecated", true);
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, SSL3_PAGE);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ info("Loading and waiting for the net error");
+ await pageLoaded;
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+
+ const enableTls10Button = doc.getElementById("enableTls10Button");
+ ok(
+ !ContentTaskUtils.is_visible(enableTls10Button),
+ "Option to re-enable TLS 1.0 is not visible"
+ );
+
+ // It should offer to reset preferences instead.
+ const prefResetButton = doc.getElementById("prefResetButton");
+ ok(
+ ContentTaskUtils.is_visible(prefResetButton),
+ "prefResetButton should be visible"
+ );
+ });
+
+ resetPrefs();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function overrideUIPref() {
+ info("TLS 1.0 override option isn't shown when the pref is set to false");
+
+ Services.prefs.setIntPref("security.tls.version.min", 3);
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+ Services.prefs.setBoolPref(
+ "security.certerrors.tls.version.show-override",
+ false
+ );
+
+ 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 ContentTask.spawn(browser, null, async function() {
+ const doc = content.document;
+ ok(
+ doc.documentURI.startsWith("about:neterror"),
+ "Should be showing error page"
+ );
+
+ const enableTls10Button = doc.getElementById("enableTls10Button");
+ ok(
+ !ContentTaskUtils.is_visible(enableTls10Button),
+ "Option to re-enable TLS 1.0 is not visible"
+ );
+ });
+
+ resetPrefs();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
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..8b94c71aa6
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError_csp_iframe.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BLOCKED_PAGE =
+ "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",
+ "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.loadURI(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(() =>
+ iframe.contentDocument.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.js 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_xfo_iframe.js b/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js
new file mode 100644
index 0000000000..617fe3561c
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BLOCKED_PAGE =
+ "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",
+ "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.loadURI(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(() =>
+ iframe.contentDocument.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..3f9bff724c
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbar.js
@@ -0,0 +1,349 @@
+/* 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() {
+ for (let featureEnabled of [true, false]) {
+ info(
+ "Testing with the feature " + (featureEnabled ? "enabled" : "disabled")
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.2h2020", featureEnabled]],
+ });
+ 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
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible on newtab if enabled",
+ });
+ }
+ is(
+ isBookmarksToolbarVisible(),
+ featureEnabled,
+ "Toolbar should be visible on newtab if enabled"
+ );
+
+ // 2: Test that the toolbar is hidden when the browser is navigated away from newtab
+ BrowserTestUtils.loadURI(newtab.linkedBrowser, "https://example.com");
+ await BrowserTestUtils.browserLoaded(newtab.linkedBrowser);
+ if (featureEnabled) {
+ 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.loadURI(newtab.linkedBrowser, "about:newtab");
+ await BrowserTestUtils.browserLoaded(newtab.linkedBrowser);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible on newtab",
+ });
+ }
+ is(
+ isBookmarksToolbarVisible(),
+ featureEnabled,
+ "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);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible with switch to newtab if enabled",
+ });
+ }
+ is(
+ isBookmarksToolbarVisible(),
+ featureEnabled,
+ "Toolbar is visible with switch to newtab if enabled"
+ );
+
+ // 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.loadURI(example.linkedBrowser, "about:newtab");
+ await BrowserTestUtils.browserLoaded(example.linkedBrowser);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar is visible with newtab load if enabled",
+ });
+ }
+ is(
+ isBookmarksToolbarVisible(),
+ featureEnabled,
+ "Toolbar is visible with newtab load if enabled"
+ );
+
+ // 8: Switching back and forth between two browsers showing about:newtab will still show the toolbar
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ is(
+ isBookmarksToolbarVisible(),
+ featureEnabled,
+ "Toolbar is visible with switch to newtab if enabled"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ is(
+ isBookmarksToolbarVisible(),
+ featureEnabled,
+ "Toolbar is visible with switch to example(newtab) if enabled"
+ );
+
+ // 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.loadURI(example.linkedBrowser, AboutNewTab.newTabURL);
+ await BrowserTestUtils.browserLoaded(example.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({ visible: true });
+ }
+ is(
+ isBookmarksToolbarVisible(),
+ featureEnabled,
+ "Toolbar is visible with switch to custom newtab if enabled"
+ );
+
+ await BrowserTestUtils.removeTab(newtab);
+ await BrowserTestUtils.removeTab(example);
+ AboutNewTab.newTabURL = oldNewTab;
+ }
+});
+
+add_task(async function bookmarks_toolbar_open_persisted() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.2h2020", true]],
+ });
+ 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");
+ EventUtils.synthesizeMouseAtCenter(bookmarksToolbarMenu, {});
+ 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");
+
+ EventUtils.synthesizeMouseAtCenter(alwaysMenuItem, {});
+
+ await waitForBookmarksToolbarVisibility({ visible: true });
+ popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ menuButton,
+ { type: "contextmenu" },
+ window
+ );
+ await popupShown;
+ bookmarksToolbarMenu = document.querySelector("#toggle_PersonalToolbar");
+ EventUtils.synthesizeMouseAtCenter(bookmarksToolbarMenu, {});
+ subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(bookmarksToolbarMenu, {});
+ 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");
+ EventUtils.synthesizeMouseAtCenter(bookmarksToolbarMenu, {});
+ subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(bookmarksToolbarMenu, {});
+ 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");
+ EventUtils.synthesizeMouseAtCenter(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.toolbars.bookmarks.2h2020", true],
+ ["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",
+ });
+ 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;
+ is(newtab.linkedBrowser.currentURI.spec, "about:blank", "blank is loaded");
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar is not visible with NTP disabled",
+ });
+ await BrowserTestUtils.removeTab(newtab);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.enabled", true]],
+ });
+});
+
+add_task(async function test_history_pushstate() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.2h2020", true]],
+ });
+ 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..c3fc32db4f
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarEmpty.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const bookmarksInfo = [
+ {
+ title: "firefox",
+ url: "http://example.com",
+ },
+ {
+ title: "rules",
+ url: "http://example.com/2",
+ },
+ {
+ title: "yo",
+ url: "http://example.com/2",
+ },
+];
+
+add_task(async function setup() {
+ 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() {
+ for (let featureEnabled of [true, false]) {
+ info(
+ "Testing with the feature " + (featureEnabled ? "enabled" : "disabled")
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.2h2020", featureEnabled]],
+ });
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarksInfo,
+ });
+ let example = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "https://example.com",
+ });
+ let newtab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ });
+ let emptyMessage = document.getElementById("personal-toolbar-empty");
+
+ // 1: Test that the toolbar is shown in a newly opened foreground about:newtab
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message: "Toolbar should be visible on newtab if enabled",
+ });
+ ok(emptyMessage.hidden, "Empty message is hidden with toolbar populated");
+ }
+
+ // 2: Toolbar should get hidden when switching tab to example.com
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await waitForBookmarksToolbarVisibility({
+ visible: false,
+ message: "Toolbar should be hidden on example.com",
+ });
+
+ // 3: Remove all children of the Bookmarks Toolbar and confirm that
+ // the toolbar should not become visible when switching to newtab
+ CustomizableUI.addWidgetToArea(
+ "personal-bookmarks",
+ CustomizableUI.AREA_TABSTRIP
+ );
+ CustomizableUI.removeWidgetFromArea("import-button");
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message:
+ "Toolbar is visible when there are no items in the toolbar area",
+ });
+ ok(!emptyMessage.hidden, "Empty message is shown with toolbar empty");
+ // Click the link and check we open the library:
+ let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ EventUtils.synthesizeMouseAtCenter(
+ emptyMessage.querySelector(".text-link"),
+ {}
+ );
+ let libraryWin = await winPromise;
+ is(
+ libraryWin.document.location.href,
+ "chrome://browser/content/places/places.xhtml",
+ "Should have opened library."
+ );
+ await BrowserTestUtils.closeWindow(libraryWin);
+ }
+
+ // 4: Put personal-bookmarks back in the toolbar and confirm the toolbar is visible now
+ CustomizableUI.addWidgetToArea(
+ "personal-bookmarks",
+ CustomizableUI.AREA_BOOKMARKS
+ );
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message:
+ "Toolbar should be visible with Bookmarks Toolbar Items restored",
+ });
+ ok(emptyMessage.hidden, "Empty message is hidden with toolbar populated");
+ }
+
+ // 5: Remove all the bookmarks in the toolbar and confirm that the toolbar
+ // is hidden on the New Tab now
+ await PlacesUtils.bookmarks.remove(bookmarks);
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message:
+ "Toolbar is visible when there are no items or nested bookmarks in the toolbar area",
+ });
+ ok(!emptyMessage.hidden, "Empty message is shown with toolbar empty");
+ }
+
+ // 6: Add a toolbarbutton and make sure that the toolbar appears when the button is visible
+ CustomizableUI.addWidgetToArea(
+ "characterencoding-button",
+ CustomizableUI.AREA_BOOKMARKS
+ );
+ await BrowserTestUtils.switchTab(gBrowser, example);
+ await BrowserTestUtils.switchTab(gBrowser, newtab);
+ if (featureEnabled) {
+ await waitForBookmarksToolbarVisibility({
+ visible: true,
+ message:
+ "Toolbar is visible when there is a visible button in the toolbar",
+ });
+ ok(emptyMessage.hidden, "Empty message is hidden with button in toolbar");
+ }
+
+ await BrowserTestUtils.removeTab(newtab);
+ await BrowserTestUtils.removeTab(example);
+ CustomizableUI.reset();
+ }
+});
diff --git a/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js
new file mode 100644
index 0000000000..db75376aac
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarNewWindow.js
@@ -0,0 +1,57 @@
+/* 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 url =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "slow_loading_page.sjs";
+ for (let featureEnabled of [true, false]) {
+ for (let newTabEnabled of [true, false]) {
+ info(
+ `Testing with the feature ${
+ featureEnabled ? "enabled" : "disabled"
+ } and newtab ${newTabEnabled ? "enabled" : "disabled"}`
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.toolbars.bookmarks.2h2020", featureEnabled],
+ ["browser.newtabpage.enabled", newTabEnabled],
+ ],
+ });
+
+ let newWindowOpened = BrowserTestUtils.domWindowOpened();
+ let triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ openUILinkIn(url, "window", { triggeringPrincipal });
+
+ let newWin = await newWindowOpened;
+
+ let exitConditions = {
+ visible: true,
+ };
+ let slowSiteLoaded = BrowserTestUtils.firstBrowserLoaded(newWin, false);
+ slowSiteLoaded.then(result => {
+ exitConditions.earlyExit = result;
+ });
+
+ let result = await waitForBookmarksToolbarVisibilityWithExitConditions({
+ win: newWin,
+ exitConditions,
+ message: "Toolbar should not become visible when loading slow site",
+ });
+
+ // The visibility promise will resolve to a Boolean whereas the browser
+ // load promise will resolve to an Event object.
+ ok(
+ typeof result != "boolean",
+ "The bookmarks toolbar should not become visible before the site is loaded"
+ );
+ ok(!isBookmarksToolbarVisible(newWin), "Toolbar hidden on slow site");
+
+ await BrowserTestUtils.closeWindow(newWin);
+ }
+ }
+});
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..76f603a025
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_bookmarksToolbarPrefs.js
@@ -0,0 +1,88 @@
+/* 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 bookmarksFeatureStates = [
+ ["browser.toolbars.bookmarks.2h2020", true],
+ ["browser.toolbars.bookmarks.2h2020", false],
+ ];
+ let bookmarksToolbarVisibilityStates = [
+ ["browser.toolbars.bookmarks.visibility", "newtab"],
+ ["browser.toolbars.bookmarks.visibility", "always"],
+ ["browser.toolbars.bookmarks.visibility", "never"],
+ ];
+ for (let featureState of bookmarksFeatureStates) {
+ for (let visibilityState of bookmarksToolbarVisibilityStates) {
+ await SpecialPowers.pushPrefEnv({
+ set: [featureState, visibilityState],
+ });
+
+ for (let privateWin of [true, false]) {
+ info(
+ `Testing with ${featureState} and ${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,
+ });
+ }
+
+ if (featureState[1]) {
+ 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],
+ });
+ } else {
+ 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: "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_aboutNewTab_defaultBrowserNotification.js b/browser/base/content/test/about/browser_aboutNewTab_defaultBrowserNotification.js
new file mode 100644
index 0000000000..dbda75a21b
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutNewTab_defaultBrowserNotification.js
@@ -0,0 +1,344 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let { DefaultBrowserNotification } = ChromeUtils.import(
+ "resource:///actors/AboutNewTabParent.jsm",
+ {}
+);
+
+add_task(async function notification_shown_on_first_newtab_when_not_default() {
+ await test_with_mock_shellservice({ isDefault: false }, async function() {
+ ok(
+ !gBrowser.getNotificationBox(gBrowser.selectedBrowser)
+ .currentNotification,
+ "There shouldn't be a notification when the test starts"
+ );
+ let firstTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ let notification = await TestUtils.waitForCondition(
+ () =>
+ firstTab.linkedBrowser &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser) &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser).currentNotification,
+ "waiting for notification"
+ );
+ ok(notification, "A notification should be shown on the new tab page");
+ is(
+ notification.getAttribute("value"),
+ "default-browser",
+ "Notification should be default browser"
+ );
+
+ let secondTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+
+ ok(
+ secondTab.linkedBrowser &&
+ gBrowser.getNotificationBox(secondTab.linkedBrowser) &&
+ !gBrowser.getNotificationBox(secondTab.linkedBrowser)
+ .currentNotification,
+ "A notification should not be shown on the second new tab page"
+ );
+
+ gBrowser.removeTab(firstTab);
+ gBrowser.removeTab(secondTab);
+ });
+});
+
+add_task(async function notification_bar_removes_itself_on_navigation() {
+ await test_with_mock_shellservice({ isDefault: false }, async function() {
+ let firstTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ let notification = await TestUtils.waitForCondition(
+ () =>
+ firstTab.linkedBrowser &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser) &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser).currentNotification,
+ "waiting for notification"
+ );
+ ok(notification, "A notification should be shown on the new tab page");
+ is(
+ notification.getAttribute("value"),
+ "default-browser",
+ "Notification should be default browser"
+ );
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "https://example.com");
+
+ let notificationRemoved = await TestUtils.waitForCondition(
+ () =>
+ firstTab.linkedBrowser &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser) &&
+ !gBrowser.getNotificationBox(firstTab.linkedBrowser)
+ .currentNotification,
+ "waiting for notification to get removed"
+ );
+ ok(
+ notificationRemoved,
+ "A notification should not be shown after navigation"
+ );
+
+ gBrowser.removeTab(firstTab);
+ });
+});
+
+add_task(async function notification_appears_on_first_navigation_to_homepage() {
+ await test_with_mock_shellservice({ isDefault: false }, async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ });
+ ok(
+ tab.linkedBrowser &&
+ gBrowser.getNotificationBox(tab.linkedBrowser) &&
+ !gBrowser.getNotificationBox(tab.linkedBrowser).currentNotification,
+ "a notification should not be shown on about:robots"
+ );
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:home");
+
+ let notification = await TestUtils.waitForCondition(
+ () =>
+ tab.linkedBrowser &&
+ gBrowser.getNotificationBox(tab.linkedBrowser) &&
+ gBrowser.getNotificationBox(tab.linkedBrowser).currentNotification,
+ "waiting for notification to appear on about:home after coming from about:robots"
+ );
+ ok(
+ notification,
+ "A notification should be shown after navigation to about:home"
+ );
+ is(
+ notification.getAttribute("value"),
+ "default-browser",
+ "Notification should be default browser"
+ );
+
+ gBrowser.removeTab(tab);
+ });
+});
+
+add_task(async function clicking_button_on_notification_calls_setAsDefault() {
+ await test_with_mock_shellservice({ isDefault: false }, async function() {
+ let firstTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ let notification = await TestUtils.waitForCondition(
+ () =>
+ firstTab.linkedBrowser &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser) &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser).currentNotification,
+ "waiting for notification"
+ );
+ ok(notification, "A notification should be shown on the new tab page");
+ is(
+ notification.getAttribute("value"),
+ "default-browser",
+ "Notification should be default browser"
+ );
+
+ let shellService = window.getShellService();
+ ok(
+ !shellService.isDefaultBrowser(),
+ "should not be default prior to clicking button"
+ );
+ let button = notification.querySelector(".notification-button");
+ button.click();
+ ok(
+ shellService.isDefaultBrowser(),
+ "should be default after clicking button"
+ );
+
+ gBrowser.removeTab(firstTab);
+ });
+});
+
+add_task(async function notification_not_displayed_on_private_window() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await test_with_mock_shellservice(
+ { win: privateWin, isDefault: false },
+ async function() {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: privateWin.gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ ok(
+ !privateWin.gBrowser.getNotificationBox(
+ privateWin.gBrowser.selectedBrowser
+ ).currentNotification,
+ "There shouldn't be a notification in the private window"
+ );
+ await BrowserTestUtils.closeWindow(privateWin);
+ }
+ );
+});
+
+add_task(async function notification_displayed_on_perm_private_window() {
+ SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await test_with_mock_shellservice(
+ { win: privateWin, isDefault: false },
+ async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: privateWin.gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ ok(
+ PrivateBrowsingUtils.isBrowserPrivate(
+ privateWin.gBrowser.selectedBrowser
+ ),
+ "Browser should be private"
+ );
+ let notification = await TestUtils.waitForCondition(
+ () =>
+ tab.linkedBrowser &&
+ gBrowser.getNotificationBox(tab.linkedBrowser) &&
+ gBrowser.getNotificationBox(tab.linkedBrowser).currentNotification,
+ "waiting for notification"
+ );
+ ok(notification, "A notification should be shown on the new tab page");
+ is(
+ notification.getAttribute("value"),
+ "default-browser",
+ "Notification should be default browser"
+ );
+ await BrowserTestUtils.closeWindow(privateWin);
+ }
+ );
+});
+
+add_task(async function clicking_dismiss_disables_default_browser_checking() {
+ await test_with_mock_shellservice({ isDefault: false }, async function() {
+ let firstTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ let notification = await TestUtils.waitForCondition(
+ () =>
+ firstTab.linkedBrowser &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser) &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser).currentNotification,
+ "waiting for notification"
+ );
+ ok(notification, "A notification should be shown on the new tab page");
+ is(
+ notification.getAttribute("value"),
+ "default-browser",
+ "Notification should be default browser"
+ );
+
+ let closeButton = notification.querySelector(".close-icon");
+ closeButton.click();
+ ok(
+ !Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"),
+ "checkDefaultBrowser bar pref should be false after dismissing notification"
+ );
+
+ gBrowser.removeTab(firstTab);
+ });
+});
+
+add_task(async function notification_not_shown_on_first_newtab_when_default() {
+ await test_with_mock_shellservice({ isDefault: true }, async function() {
+ ok(
+ !gBrowser.getNotificationBox(gBrowser.selectedBrowser)
+ .currentNotification,
+ "There shouldn't be a notification when the test starts"
+ );
+ let firstTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+ ok(
+ firstTab.linkedBrowser &&
+ gBrowser.getNotificationBox(firstTab.linkedBrowser) &&
+ !gBrowser.getNotificationBox(firstTab.linkedBrowser)
+ .currentNotification,
+ "No notification on first tab when browser is default"
+ );
+
+ gBrowser.removeTab(firstTab);
+ });
+});
+
+add_task(async function modal_notification_shown_when_bar_disabled() {
+ await test_with_mock_shellservice({ useModal: true }, async function() {
+ let modalOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel");
+
+ // This method is called during startup. Call it now so we don't have to test startup.
+ let { BrowserGlue } = ChromeUtils.import(
+ "resource:///modules/BrowserGlue.jsm",
+ {}
+ );
+ BrowserGlue.prototype._maybeShowDefaultBrowserPrompt();
+
+ await modalOpenPromise;
+ });
+});
+
+async function test_with_mock_shellservice(options, testFn) {
+ let win = options.win || window;
+ let oldShellService = win.getShellService;
+ let mockShellService = {
+ _isDefault: !!options.isDefault,
+ canSetDesktopBackground() {},
+ isDefaultBrowserOptOut() {
+ return false;
+ },
+ get shouldCheckDefaultBrowser() {
+ return true;
+ },
+ isDefaultBrowser() {
+ return this._isDefault;
+ },
+ setAsDefault() {
+ this.setDefaultBrowser();
+ },
+ setDefaultBrowser() {
+ this._isDefault = true;
+ },
+ };
+ win.getShellService = function() {
+ return mockShellService;
+ };
+ let prefs = {
+ set: [
+ ["browser.shell.checkDefaultBrowser", true],
+ ["browser.defaultbrowser.notificationbar", !options.useModal],
+ ["browser.defaultbrowser.notificationbar.checkcount", 0],
+ ],
+ };
+ if (options.useModal) {
+ prefs.set.push(["browser.shell.skipDefaultBrowserCheckOnFirstRun", false]);
+ }
+ await SpecialPowers.pushPrefEnv(prefs);
+
+ // Reset the state so the notification can be shown multiple times in one session
+ DefaultBrowserNotification.reset();
+
+ await testFn();
+
+ win.getShellService = oldShellService;
+}
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..40e26238fd
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutStopReload.js
@@ -0,0 +1,169 @@
+async function waitForNoAnimation(elt) {
+ return TestUtils.waitForCondition(() => !elt.hasAttribute("animate"));
+}
+
+async function getAnimatePromise(elt) {
+ return BrowserTestUtils.waitForAttribute("animate", elt).then(() =>
+ Assert.ok(true, `${elt.id} should animate`)
+ );
+}
+
+function stopReloadMutationCallback() {
+ Assert.ok(
+ false,
+ "stop-reload's animate attribute should not have been mutated"
+ );
+}
+
+// Force-enable the animation
+gReduceMotionOverride = false;
+
+add_task(async function checkDontShowStopOnNewTab() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+ let stopReloadContainerObserver = new MutationObserver(
+ stopReloadMutationCallback
+ );
+
+ await waitForNoAnimation(stopReloadContainer);
+ stopReloadContainerObserver.observe(stopReloadContainer, {
+ attributeFilter: ["animate"],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ waitForStateStop: true,
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ Assert.ok(
+ true,
+ "Test finished: stop-reload does not animate when navigating to local URI on new tab"
+ );
+ stopReloadContainerObserver.disconnect();
+});
+
+add_task(async function checkDontShowStopFromLocalURI() {
+ let stopReloadContainer = document.getElementById("stop-reload-button");
+ let stopReloadContainerObserver = new MutationObserver(
+ stopReloadMutationCallback
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ waitForStateStop: true,
+ });
+ await waitForNoAnimation(stopReloadContainer);
+ stopReloadContainerObserver.observe(stopReloadContainer, {
+ attributeFilter: ["animate"],
+ });
+ BrowserTestUtils.loadURI(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.loadURI(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.loadURI(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.loadURI(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..8ea27a7c88
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutSupport.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about: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");
+ }
+ );
+});
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..1c49608d04
--- /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.loadOneTab("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_bug435325.js b/browser/base/content/test/about/browser_bug435325.js
new file mode 100644
index 0000000000..0edd37b515
--- /dev/null
+++ b/browser/base/content/test/about/browser_bug435325.js
@@ -0,0 +1,57 @@
+/* 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);
+
+ BrowserTestUtils.loadURI(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..0ade48e635
--- /dev/null
+++ b/browser/base/content/test/about/browser_bug633691.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function test() {
+ const URL = "data:text/html,<iframe width='700' height='700'></iframe>";
+ await BrowserTestUtils.withNewTab({ gBrowser, url: URL }, async function(
+ browser
+ ) {
+ let context = await SpecialPowers.spawn(browser, [], function() {
+ let iframe = content.document.querySelector("iframe");
+ iframe.src = "https://expired.example.com/";
+ return BrowsingContext.getFromWindow(iframe.contentWindow);
+ });
+ await TestUtils.waitForCondition(() => {
+ let frame = context.currentWindowGlobal;
+ return frame && frame.documentURI.spec.startsWith("about:certerror");
+ });
+ await SpecialPowers.spawn(context, [], async function() {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "interactive"
+ );
+ let aP = content.document.getElementById("badCertAdvancedPanel");
+ Assert.ok(aP, "Advanced content should exist");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(aP),
+ "Advanced content should not be visible by default"
+ );
+ });
+ });
+});
diff --git a/browser/base/content/test/about/csp_iframe.sjs b/browser/base/content/test/about/csp_iframe.sjs
new file mode 100644
index 0000000000..72aa06920d
--- /dev/null
+++ b/browser/base/content/test/about/csp_iframe.sjs
@@ -0,0 +1,29 @@
+/* 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..60b152fdf9
--- /dev/null
+++ b/browser/base/content/test/about/head.js
@@ -0,0 +1,278 @@
+/* eslint-env mozilla/frame-script */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FormHistory: "resource://gre/modules/FormHistory.jsm",
+});
+
+function getSecurityInfo(securityInfoAsString) {
+ const serhelper = Cc[
+ "@mozilla.org/network/serialization-helper;1"
+ ].getService(Ci.nsISerializationHelper);
+ let securityInfo = serhelper.deserializeObject(securityInfoAsString);
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ return securityInfo;
+}
+
+function getCertChain(securityInfoAsString) {
+ let certChain = "";
+ let securityInfo = getSecurityInfo(securityInfoAsString);
+ for (let cert of securityInfo.failedCertChain) {
+ certChain += getPEMString(cert);
+ }
+ return certChain;
+}
+
+function getPEMString(cert) {
+ var derb64 = cert.getBase64DERString();
+ // 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.loadURI(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+/**
+ * Wait for the search engine to change. searchEngineChangeFn is a function
+ * that will be called to change the search engine.
+ */
+async function promiseContentSearchChange(browser, searchEngineChangeFn) {
+ // Add an event listener manually then perform the action, rather than using
+ // BrowserTestUtils.addContentEventListener as that doesn't add the listener
+ // early enough.
+ await SpecialPowers.spawn(browser, [], async () => {
+ // Store the results in a temporary place.
+ content._searchDetails = {
+ defaultEnginesList: [],
+ listener: event => {
+ if (event.detail.type == "CurrentState") {
+ content._searchDetails.defaultEnginesList.push(
+ content.wrappedJSObject.gContentSearchController.defaultEngine.name
+ );
+ }
+ },
+ };
+
+ // Listen using the system group to ensure that it fires after
+ // the default behaviour.
+ content.addEventListener(
+ "ContentSearchService",
+ content._searchDetails.listener,
+ { mozSystemGroup: true }
+ );
+ });
+
+ let expectedEngineName = await searchEngineChangeFn();
+
+ await SpecialPowers.spawn(
+ browser,
+ [expectedEngineName],
+ async expectedEngineNameChild => {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content._searchDetails.defaultEnginesList &&
+ content._searchDetails.defaultEnginesList[
+ content._searchDetails.defaultEnginesList.length - 1
+ ] == expectedEngineNameChild
+ );
+ content.removeEventListener(
+ "ContentSearchService",
+ content._searchDetails.listener,
+ { mozSystemGroup: true }
+ );
+ delete content._searchDetails;
+ }
+ );
+}
+
+/**
+ * Wait for the search engine to be added.
+ */
+async function promiseNewEngine(basename) {
+ info("Waiting for engine to be added: " + basename);
+ let url = getRootDirectory(gTestPath) + basename;
+ let engine;
+ try {
+ engine = await Services.search.addOpenSearchEngine(url, "");
+ } catch (errCode) {
+ ok(false, "addEngine failed with error code " + errCode);
+ throw errCode;
+ }
+
+ info("Search engine added: " + basename);
+ registerCleanupFunction(async () => {
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {
+ /* Can't remove the engine more than once */
+ }
+ });
+
+ return engine;
+}
+
+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;
+}
+
+async function waitForBookmarksToolbarVisibilityWithExitConditions({
+ win = window,
+ exitConditions,
+ message,
+}) {
+ let result = await TestUtils.waitForCondition(() => {
+ if (exitConditions.earlyExit) {
+ return exitConditions.earlyExit;
+ }
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ return (
+ toolbar &&
+ (exitConditions.visible ? !toolbar.collapsed : toolbar.collapsed)
+ );
+ }, message || "waiting for toolbar to become " + (exitConditions.visible ? "visible" : "hidden"));
+ if (exitConditions.earlyExit) {
+ ok(true, "Early exit condition met");
+ } else {
+ ok(false, message);
+ }
+ return exitConditions.earlyExit || result;
+}
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..4175a24805
--- /dev/null
+++ b/browser/base/content/test/about/print_postdata.sjs
@@ -0,0 +1,22 @@
+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..d7d99e31c3
--- /dev/null
+++ b/browser/base/content/test/about/xfo_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 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/.eslintrc.js b/browser/base/content/test/alerts/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/alerts/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/alerts/browser.ini b/browser/base/content/test/alerts/browser.ini
new file mode 100644
index 0000000000..173fb3c832
--- /dev/null
+++ b/browser/base/content/test/alerts/browser.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files =
+ head.js
+ file_dom_notifications.html
+
+[browser_notification_close.js]
+skip-if = os == 'win' # Bug 1227785
+[browser_notification_do_not_disturb.js]
+[browser_notification_open_settings.js]
+skip-if = os == 'win' # Bug 1411118
+[browser_notification_remove_permission.js]
+skip-if = os == 'win' # Bug 1411118
+[browser_notification_replace.js]
+skip-if = os == 'win' # Bug 1422928
+[browser_notification_tab_switching.js]
+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..6391b05a3e
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_close.js
@@ -0,0 +1,108 @@
+"use strict";
+
+const { PlacesTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+let notificationURL =
+ "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+let oldShowFavicons;
+
+add_task(async function test_notificationClose() {
+ let notificationURI = makeURI(notificationURL);
+ await addNotificationPermission(notificationURL);
+
+ oldShowFavicons = Services.prefs.getBoolPref("alerts.showFavicons");
+ Services.prefs.setBoolPref("alerts.showFavicons", true);
+
+ await PlacesTestUtils.addVisits(notificationURI);
+ let faviconURI = await new Promise(resolve => {
+ let uri = makeURI(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"
+ );
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ notificationURI,
+ uri,
+ true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ uriResult => resolve(uriResult),
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: notificationURL,
+ },
+ async function dummyTabTask(aBrowser) {
+ await openNotification(aBrowser, "showNotification2");
+
+ info("Notification alert showing");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ await closeNotification(aBrowser);
+ return;
+ }
+
+ let alertTitleLabel = alertWindow.document.getElementById(
+ "alertTitleLabel"
+ );
+ is(
+ alertTitleLabel.value,
+ "Test title",
+ "Title text of notification should be present"
+ );
+ let alertTextLabel = alertWindow.document.getElementById(
+ "alertTextLabel"
+ );
+ is(
+ alertTextLabel.textContent,
+ "Test body 2",
+ "Body text of notification should be present"
+ );
+ let alertIcon = alertWindow.document.getElementById("alertIcon");
+ is(
+ alertIcon.src,
+ faviconURI.spec,
+ "Icon of notification should be present"
+ );
+
+ let alertCloseButton = alertWindow.document.querySelector(".close-icon");
+ is(alertCloseButton.localName, "toolbarbutton", "close button found");
+ let promiseBeforeUnloadEvent = BrowserTestUtils.waitForEvent(
+ alertWindow,
+ "beforeunload"
+ );
+ let closedTime = alertWindow.Date.now();
+ alertCloseButton.click();
+ info("Clicked on close button");
+ await promiseBeforeUnloadEvent;
+
+ ok(true, "Alert should close when the close button is clicked");
+ let currentTime = alertWindow.Date.now();
+ // The notification will self-close at 12 seconds, so this checks
+ // that the notification closed before the timeout.
+ ok(
+ currentTime - closedTime < 5000,
+ "Close requested at " +
+ closedTime +
+ ", actually closed at " +
+ currentTime
+ );
+ }
+ );
+});
+
+add_task(async function cleanup() {
+ PermissionTestUtils.remove(notificationURL, "desktop-notification");
+ if (typeof oldShowFavicons == "boolean") {
+ Services.prefs.setBoolPref("alerts.showFavicons", oldShowFavicons);
+ }
+});
diff --git a/browser/base/content/test/alerts/browser_notification_do_not_disturb.js b/browser/base/content/test/alerts/browser_notification_do_not_disturb.js
new file mode 100644
index 0000000000..cfae32e216
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_do_not_disturb.js
@@ -0,0 +1,159 @@
+/* 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 =
+ "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_task(async function setup() {
+ 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..339d236cc5
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_open_settings.js
@@ -0,0 +1,80 @@
+"use strict";
+
+var notificationURL =
+ "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) {
+ // Ensure preferences is loaded before removing the tab.
+ let syncPaneLoadedPromise = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+
+ 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;
+ }
+
+ 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..679b72debf
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_remove_permission.js
@@ -0,0 +1,85 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+var tab;
+var notificationURL =
+ "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..c92777f515
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_replace.js
@@ -0,0 +1,65 @@
+"use strict";
+
+let notificationURL =
+ "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..cc9e57c1c7
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_tab_switching.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+var tab;
+var notification;
+var notificationURL =
+ "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..2c2e04f98d
--- /dev/null
+++ b/browser/base/content/test/alerts/head.js
@@ -0,0 +1,72 @@
+// 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/.eslintrc.js b/browser/base/content/test/backforward/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/backforward/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/backforward/browser.ini b/browser/base/content/test/backforward/browser.ini
new file mode 100644
index 0000000000..292963db3b
--- /dev/null
+++ b/browser/base/content/test/backforward/browser.ini
@@ -0,0 +1 @@
+[browser_longpress_session_history_menu.js]
diff --git a/browser/base/content/test/backforward/browser_longpress_session_history_menu.js b/browser/base/content/test/backforward/browser_longpress_session_history_menu.js
new file mode 100644
index 0000000000..d3ccd6ccdc
--- /dev/null
+++ b/browser/base/content/test/backforward/browser_longpress_session_history_menu.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the session history can be shown by long-pressing the back button.
+// And that middle-click opens one tab (as a regression test for bug 1657992).
+add_task(async function restore_history_entry_by_middle_click() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+
+ await SpecialPowers.spawn(tab1.linkedBrowser, [], () => {
+ content.history.pushState(null, null, "2.html");
+ content.history.pushState(null, null, "3.html");
+ });
+
+ await new Promise(resolve => SessionStore.getSessionHistory(tab1, resolve));
+
+ let backButton = document.getElementById("back-button");
+ // This is the popup (clone of backForwardMenu) from SetClickAndHoldHandlers.
+ let historyMenu = backButton.firstElementChild;
+
+ info("waiting for the history menu to open");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ historyMenu,
+ "popupshown"
+ );
+
+ // Trigger gClickAndHoldListenersOnElement logic in browser.js to open the
+ // history menu that opens after a long press.
+ EventUtils.synthesizeMouseAtCenter(backButton, { type: "mousedown" });
+ let event = await popupShownPromise;
+ EventUtils.synthesizeMouseAtCenter(backButton, { type: "mouseup" });
+
+ info("Waiting for menu items to be populated");
+ await new Promise(resolve => SessionStore.getSessionHistory(tab1, resolve));
+
+ SimpleTest.isDeeply(
+ Array.from(event.target.children, node => node.getAttribute("uri")),
+ [
+ "http://example.com/3.html",
+ "http://example.com/2.html",
+ "http://example.com/",
+ ],
+ "Expected session history items"
+ );
+ let historyMenuItem = event.target.children[1];
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ historyMenu,
+ "popuphidden"
+ );
+
+ let tabRestoredPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "SSTabRestored"
+ );
+
+ await EventUtils.sendMouseEvent(
+ { type: "click", button: 1 },
+ historyMenuItem
+ );
+
+ info("Waiting for history menu to be hidden");
+ await popupHiddenPromise;
+ info("Waiting for history item to be restored in a new tab");
+ let newTab = (await tabRestoredPromise).target;
+ is(newTab.linkedBrowser.currentURI.spec, "http://example.com/2.html");
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/base/content/test/caps/.eslintrc.js b/browser/base/content/test/caps/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/caps/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/caps/browser.ini b/browser/base/content/test/caps/browser.ini
new file mode 100644
index 0000000000..966101e8cb
--- /dev/null
+++ b/browser/base/content/test/caps/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+
+[browser_principalSerialization_version1.js]
+[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.
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..04afed1e2a
--- /dev/null
+++ b/browser/base/content/test/caps/browser_principalSerialization_json.js
@@ -0,0 +1,164 @@
+"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 = atob(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 = [
+ {
+ input: { uri: "http://example.com/", OA: {} },
+ expected: `{"${contentId}":{"${content}":"http://example.com/"}}`,
+ },
+ {
+ input: { uri: "http://mozilla1.com/", OA: {} },
+ expected: `{"${contentId}":{"${content}":"http://mozilla1.com/"}}`,
+ },
+ {
+ input: { uri: "http://mozilla2.com/", OA: { userContextId: 0 } },
+ expected: `{"${contentId}":{"${content}":"http://mozilla2.com/"}}`,
+ },
+ {
+ input: { uri: "http://mozilla3.com/", OA: { userContextId: 2 } },
+ expected: `{"${contentId}":{"${content}":"http://mozilla3.com/","${suffix}":"^userContextId=2"}}`,
+ },
+ {
+ input: { uri: "http://mozilla4.com/", OA: { privateBrowsingId: 1 } },
+ expected: `{"${contentId}":{"${content}":"http://mozilla4.com/","${suffix}":"^privateBrowsingId=1"}}`,
+ },
+ {
+ 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,
+ atob(sp),
+ "Expected serialized object for " + test.input.uri
+ );
+ is(
+ btoa(test.expected),
+ sp,
+ "Expected serialized string 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, atob(sp), "Expected serialized object for system principal");
+ is(btoa(expected), sp, "Expected serialized string 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/.eslintrc.js b/browser/base/content/test/captivePortal/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/captivePortal/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/captivePortal/browser.ini b/browser/base/content/test/captivePortal/browser.ini
new file mode 100644
index 0000000000..bc6d1b0258
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser.ini
@@ -0,0 +1,11 @@
+[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_captivePortal_certErrorUI.js]
+[browser_captivePortalTabReference.js]
+[browser_closeCapPortalTabCanonicalURL.js]
diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
new file mode 100644
index 0000000000..aeafae21d8
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
@@ -0,0 +1,125 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+// Bug 1318389 - This test does a lot of window and tab manipulation,
+// causing it to take a long time on debug.
+requestLongerTimeout(2);
+
+add_task(setupPrefsAndRecentWindowBehavior);
+
+// Each of the test cases below is run twice: once for login-success and once
+// for login-abort (aSuccess set to true and false respectively).
+let testCasesForBothSuccessAndAbort = [
+ /**
+ * A portal is detected when there's no browser window, then a browser
+ * window is opened, then the portal is freed.
+ * The portal tab should be added and focused when the window is
+ * opened, and closed automatically when the success event is fired.
+ * The captive portal notification should be shown when the window is
+ * opened, and closed automatically when the success event is fired.
+ */
+ async function test_detectedWithNoBrowserWindow_Open(aSuccess) {
+ await portalDetected();
+ let win = await focusWindowAndWaitForPortalUI();
+ await freePortal(aSuccess);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * A portal is detected when multiple browser windows are open but none
+ * have focus. A browser window is focused, then the portal is freed.
+ * The portal tab should be added and focused when the window is
+ * focused, and closed automatically when the success event is fired.
+ * The captive portal notification should be shown in all windows upon
+ * detection, and closed automatically when the success event is fired.
+ */
+ async function test_detectedWithNoBrowserWindow_Focused(aSuccess) {
+ let win1 = await openWindowAndWaitForFocus();
+ let win2 = await openWindowAndWaitForFocus();
+ // Defocus both windows.
+ await SimpleTest.promiseFocus(window);
+
+ await portalDetected();
+
+ // Notification should be shown in both windows.
+ ensurePortalNotification(win1);
+ ensureNoPortalTab(win1);
+ ensurePortalNotification(win2);
+ ensureNoPortalTab(win2);
+
+ await focusWindowAndWaitForPortalUI(false, win2);
+
+ await freePortal(aSuccess);
+
+ ensureNoPortalNotification(win1);
+ ensureNoPortalTab(win2);
+ ensureNoPortalNotification(win2);
+
+ await closeWindowAndWaitForWindowActivate(win2);
+ // No need to wait for xul-window-visible: after win2 is closed, focus
+ // is restored to the default window and win1 remains in the background.
+ await BrowserTestUtils.closeWindow(win1);
+ },
+
+ /**
+ * A portal is detected when there's no browser window, then a browser
+ * window is opened, then the portal is freed.
+ * The recheck triggered when the browser window is opened takes a
+ * long time. No portal tab should be added.
+ * The captive portal notification should be shown when the window is
+ * opened, and closed automatically when the success event is fired.
+ */
+ async function test_detectedWithNoBrowserWindow_LongRecheck(aSuccess) {
+ await portalDetected();
+ let win = await focusWindowAndWaitForPortalUI(true);
+ await freePortal(aSuccess);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * A portal is detected when there's no browser window, and the
+ * portal is freed before a browser window is opened. No portal
+ * UI should be shown when a browser window is opened.
+ */
+ async function test_detectedWithNoBrowserWindow_GoneBeforeOpen(aSuccess) {
+ await portalDetected();
+ await freePortal(aSuccess);
+ let win = await openWindowAndWaitForFocus();
+ // Wait for a while to make sure no UI is shown.
+ await new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ });
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * A portal is detected when a browser window has focus. No portal tab should
+ * be opened. A notification bar should be displayed in all browser windows.
+ */
+ async function test_detectedWithFocus(aSuccess) {
+ let win1 = await openWindowAndWaitForFocus();
+ let win2 = await openWindowAndWaitForFocus();
+ await portalDetected();
+ ensureNoPortalTab(win1);
+ ensureNoPortalTab(win2);
+ ensurePortalNotification(win1);
+ ensurePortalNotification(win2);
+ await freePortal(aSuccess);
+ ensureNoPortalNotification(win1);
+ ensureNoPortalNotification(win2);
+ await BrowserTestUtils.closeWindow(win2);
+ await BrowserTestUtils.closeWindow(win1);
+ await waitForBrowserWindowActive(window);
+ },
+];
+
+for (let testcase of testCasesForBothSuccessAndAbort) {
+ add_task(testcase.bind(null, true));
+ add_task(testcase.bind(null, false));
+}
diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js
new file mode 100644
index 0000000000..ec19c0cb90
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js
@@ -0,0 +1,106 @@
+"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.loadURI(browser, CANONICAL_URL_REDIRECTED);
+ await loadPromise;
+ await freePortal(true);
+ ensurePortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+
+ /**
+ * Test the various expected behaviors of the "Show Login Page" button
+ * in the captive portal notification. The button should be visible for
+ * all tabs except the captive portal tab, and when clicked, should
+ * ensure a captive portal tab is open and select it.
+ */
+ async function test_showLoginPageButton() {
+ let win = await openWindowAndWaitForFocus();
+ await portalDetected();
+ let notification = ensurePortalNotification(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+
+ function testPortalTabSelectedAndButtonNotVisible() {
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be selected."
+ );
+ testShowLoginPageButtonVisibility(notification, "hidden");
+ }
+
+ let button = notification.querySelector("button.notification-button");
+ async function clickButtonAndExpectNewPortalTab() {
+ let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
+ button.click();
+ let tab = await p;
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be selected."
+ );
+ return tab;
+ }
+
+ // Simulate clicking the button. The portal tab should be opened and
+ // selected and the button should hide.
+ let tab = await clickButtonAndExpectNewPortalTab();
+ testPortalTabSelectedAndButtonNotVisible();
+
+ // Close the tab. The button should become visible.
+ BrowserTestUtils.removeTab(tab);
+ ensureNoPortalTab(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+
+ // When the button is clicked, a new portal tab should be opened and
+ // selected.
+ tab = await clickButtonAndExpectNewPortalTab();
+
+ // Open another arbitrary tab. The button should become visible. When it's clicked,
+ // the portal tab should be selected.
+ let anotherTab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ testShowLoginPageButtonVisibility(notification, "visible");
+ button.click();
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be selected."
+ );
+
+ // Close the portal tab and select the arbitrary tab. The button should become
+ // visible and when it's clicked, a new portal tab should be opened.
+ BrowserTestUtils.removeTab(tab);
+ win.gBrowser.selectedTab = anotherTab;
+ testShowLoginPageButtonVisibility(notification, "visible");
+ tab = await clickButtonAndExpectNewPortalTab();
+
+ BrowserTestUtils.removeTab(anotherTab);
+ await freePortal(true);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ await closeWindowAndWaitForWindowActivate(win);
+ },
+];
+
+for (let testcase of testcases) {
+ add_task(testcase);
+}
diff --git a/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js b/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js
new file mode 100644
index 0000000000..fe463bbaf5
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js
@@ -0,0 +1,125 @@
+/* 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 CPS = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
+ Ci.nsICaptivePortalService
+);
+
+async function setCaptivePortalLoginState() {
+ let captivePortalStatePropagated = TestUtils.topicObserved(
+ "ipc:network:captive-portal-set-state"
+ );
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ info(
+ "Waiting for captive portal login state to propagate to the content process."
+ );
+ await captivePortalStatePropagated;
+ info("Captive Portal login state has been set");
+}
+
+async function openCaptivePortalErrorTab() {
+ // Open a page with a cert error.
+ let certErrorLoaded;
+ let errorTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ let tab = BrowserTestUtils.addTab(gBrowser, BAD_CERT_PAGE);
+ gBrowser.selectedTab = tab;
+ let browser = gBrowser.selectedBrowser;
+ certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ return tab;
+ },
+ false
+ );
+
+ await certErrorLoaded;
+ info("An error page was opened");
+ let browser = errorTab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], 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"
+ );
+ });
+
+ return errorTab;
+}
+
+async function openCaptivePortalLoginTab(errorTab) {
+ let portalTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ CANONICAL_URL
+ );
+
+ 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;
+}
+
+async function checkCaptivePortalTabReference(evt, currState) {
+ await setCaptivePortalLoginState();
+ 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 setCaptivePortalLoginState();
+ ok(CPS.state == CPS.LOCKED_PORTAL, "Captive portal is locked again");
+ errorTab = await openCaptivePortalErrorTab();
+ let portalTab2 = await openCaptivePortalLoginTab(errorTab);
+ ok(
+ portalTab != portalTab2,
+ "waitForNewTab in openCaptivePortalLoginTab should not have completed at this point if references were held to the old captive portal tab after login/abort."
+ );
+
+ gBrowser.removeTab(errorTab);
+ gBrowser.removeTab(portalTab);
+ gBrowser.removeTab(portalTab2);
+}
+
+add_task(async function setup() {
+ 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..2075f86b72
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
@@ -0,0 +1,175 @@
+/* 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/";
+
+async function setupCaptivePortalTab() {
+ let captivePortalStatePropagated = TestUtils.topicObserved(
+ "ipc:network:captive-portal-set-state"
+ );
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ info(
+ "Waiting for captive portal state to be propagated to the content process."
+ );
+ await captivePortalStatePropagated;
+
+ // 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
+ );
+ info("Waiting for cert error page to load");
+ await certErrorLoaded;
+ return errorTab;
+}
+
+add_task(async function setup() {
+ 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"
+ );
+
+ let tab = await setupCaptivePortalTab();
+ let browser = tab.linkedBrowser;
+ let portalTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ CANONICAL_URL
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.getElementById("openPortalLoginPageButton");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.is_visible(loginButton),
+ "Captive portal error page UI is visible"
+ );
+
+ is(
+ loginButton.getAttribute("autofocus"),
+ "true",
+ "openPortalLoginPageButton has autofocus"
+ );
+ 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."
+ );
+
+ // 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.");
+
+ 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);
+});
+
+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."
+ );
+ let tab = await setupCaptivePortalTab();
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [BAD_CERT_PAGE], async expectedURL => {
+ let doc = content.document;
+ let advancedButton = doc.getElementById("advancedButton");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.is_visible(advancedButton),
+ "Captive portal UI is visible"
+ );
+
+ info("Clicking on the advanced button");
+ await EventUtils.synthesizeMouseAtCenter(advancedButton, {}, content);
+ let advPanelContainer = doc.getElementById("advancedPanelContainer");
+ ok(
+ ContentTaskUtils.is_visible(advPanelContainer),
+ "Advanced panel is now visible"
+ );
+
+ let advPanelContent = doc.getElementById("badCertTechnicalInfo");
+ ok(
+ ContentTaskUtils.is_visible(advPanelContent) &&
+ advPanelContent.textContent.includes("expired.example.com"),
+ "Advanced panel text content is visible"
+ );
+
+ let advPanelErrorCode = doc.getElementById("errorCode");
+ ok(
+ advPanelErrorCode.textContent,
+ "Cert error code is visible in the advanced panel"
+ );
+
+ let advPanelExceptionButton = doc.getElementById("exceptionDialogButton");
+ await EventUtils.synthesizeMouseAtCenter(
+ advPanelExceptionButton,
+ {},
+ content
+ );
+ ok(
+ doc.location.href.startsWith(expectedURL),
+ "Accept the risk and continue button works on the captive portal page"
+ );
+ });
+
+ // Clear the certificate exception.
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("expired.example.com", -1);
+ 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..66b2aea904
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const LOGIN_LINK = `<html><body><a href="/unlock">login</a></body></html>`;
+const BAD_CERT_PAGE = "https://expired.example.com/";
+const LOGIN_URL = "http://localhost:8080/login";
+const CANONICAL_SUCCESS_URL = "http://localhost:8080/success";
+const CPS = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
+ Ci.nsICaptivePortalService
+);
+
+let server;
+let loginPageShown = false;
+
+async function openCaptivePortalErrorTab() {
+ await portalDetected();
+
+ // 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) {
+ let portalTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ LOGIN_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;
+}
+
+function redirectHandler(request, response) {
+ if (loginPageShown) {
+ return;
+ }
+ response.setStatusLine(request.httpVersion, 302, "captive");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Location", LOGIN_URL);
+}
+
+function loginHandler(request, response) {
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(LOGIN_LINK, LOGIN_LINK.length);
+ loginPageShown = true;
+}
+
+function unlockHandler(request, response) {
+ response.setStatusLine(request.httpVersion, 302, "login complete");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Location", CANONICAL_SUCCESS_URL);
+}
+
+add_task(async function setup() {
+ // Set up a mock server for handling captive portal redirect.
+ server = new HttpServer();
+ server.registerPathHandler("/success", redirectHandler);
+ server.registerPathHandler("/login", loginHandler);
+ server.registerPathHandler("/unlock", unlockHandler);
+ server.start(8080);
+ info("Mock server is now set up for captive portal redirect");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["captivedetect.canonicalURL", CANONICAL_SUCCESS_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT],
+ ],
+ });
+});
+
+// This test checks if the captive portal tab is removed after the
+// sucess/abort events are fired, assuming the tab has already redirected
+// to the canonical URL before they are fired.
+add_task(async function checkCaptivePortalTabCloseOnCanonicalURL_one() {
+ let errorTab = await openCaptivePortalErrorTab();
+ let tab = await openCaptivePortalLoginTab(errorTab);
+ 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;
+ let errorTab = await openCaptivePortalErrorTab();
+ let tab = await openCaptivePortalLoginTab(errorTab);
+ let browser = tab.linkedBrowser;
+
+ let redirectedToCanonicalURL = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ CANONICAL_SUCCESS_URL
+ );
+ let errorPageReloaded = BrowserTestUtils.waitForErrorPage(
+ errorTab.linkedBrowser
+ );
+
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ await TestUtils.waitForCondition(
+ () => CPS.state == CPS.UNLOCKED_PORTAL,
+ "Captive portal is released"
+ );
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(tab);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ let loginButton = doc.querySelector("a");
+ await ContentTaskUtils.waitForCondition(
+ () => loginButton,
+ "Login button on the captive portal tab is visible"
+ );
+ info("Clicking the login button on the captive portal tab page");
+ await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content);
+ });
+
+ await redirectedToCanonicalURL;
+ info(
+ "Re-direct to canonical URL in the captive portal tab was succcessful after login"
+ );
+ await tabClosed;
+ info(
+ "Captive portal tab was closed on re-direct to canonical URL after login as expected"
+ );
+
+ await errorPageReloaded;
+ info("Captive portal error page was reloaded");
+ gBrowser.removeTab(errorTab);
+
+ // Stop the server.
+ await new Promise(r => server.stop(r));
+});
diff --git a/browser/base/content/test/captivePortal/head.js b/browser/base/content/test/captivePortal/head.js
new file mode 100644
index 0000000000..0255874079
--- /dev/null
+++ b/browser/base/content/test/captivePortal/head.js
@@ -0,0 +1,214 @@
+var { BrowserWindowTracker } = ChromeUtils.import(
+ "resource:///modules/BrowserWindowTracker.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "CaptivePortalWatcher",
+ "resource:///modules/CaptivePortalWatcher.jsm"
+);
+
+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";
+
+async function setupPrefsAndRecentWindowBehavior() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["captivedetect.canonicalURL", CANONICAL_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT],
+ ],
+ });
+ // We need to test behavior when a portal is detected when there is no browser
+ // window, but we can't close the default window opened by the test harness.
+ // Instead, we deactivate CaptivePortalWatcher in the default window and
+ // exclude it using an attribute to mask its presence.
+ window.CaptivePortalWatcher.uninit();
+ window.document.documentElement.setAttribute("ignorecaptiveportal", "true");
+
+ registerCleanupFunction(function cleanUp() {
+ window.CaptivePortalWatcher.init();
+ window.document.documentElement.removeAttribute("ignorecaptiveportal");
+ });
+}
+
+async function portalDetected() {
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ await TestUtils.waitForCondition(() => {
+ return cps.state == cps.LOCKED_PORTAL;
+ }, "Waiting for Captive Portal Service to update state after portal detected.");
+}
+
+async function freePortal(aSuccess) {
+ Services.obs.notifyObservers(
+ null,
+ "captive-portal-login-" + (aSuccess ? "success" : "abort")
+ );
+ await TestUtils.waitForCondition(() => {
+ return cps.state != cps.LOCKED_PORTAL;
+ }, "Waiting for Captive Portal Service to update state after portal freed.");
+}
+
+// If a window is provided, it will be focused. Otherwise, a new window
+// will be opened and focused.
+async function focusWindowAndWaitForPortalUI(aLongRecheck, win) {
+ // CaptivePortalWatcher triggers a recheck when a window gains focus. If
+ // the time taken for the check to complete is under PORTAL_RECHECK_DELAY_MS,
+ // a tab with the login page is opened and selected. If it took longer,
+ // no tab is opened. It's not reliable to time things in an async test,
+ // so use a delay threshold of -1 to simulate a long recheck (so that any
+ // amount of time is considered excessive), and a very large threshold to
+ // simulate a short recheck.
+ Services.prefs.setIntPref(
+ "captivedetect.portalRecheckDelayMS",
+ aLongRecheck ? -1 : 1000000
+ );
+
+ if (!win) {
+ win = await BrowserTestUtils.openNewBrowserWindow();
+ }
+ let windowActivePromise = waitForBrowserWindowActive(win);
+ win.focus();
+ await windowActivePromise;
+
+ // After a new window is opened, CaptivePortalWatcher asks for a recheck, and
+ // waits for it to complete. We need to manually tell it a recheck completed.
+ await TestUtils.waitForCondition(() => {
+ return win.CaptivePortalWatcher._waitingForRecheck;
+ }, "Waiting for CaptivePortalWatcher to trigger a recheck.");
+ Services.obs.notifyObservers(null, "captive-portal-check-complete");
+
+ let notification = ensurePortalNotification(win);
+
+ if (aLongRecheck) {
+ ensureNoPortalTab(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+ return win;
+ }
+
+ let tab = win.gBrowser.tabs[1];
+ if (tab.linkedBrowser.currentURI.spec != CANONICAL_URL) {
+ // The tab should load the canonical URL, wait for it.
+ await BrowserTestUtils.waitForLocationChange(win.gBrowser, CANONICAL_URL);
+ }
+ is(
+ win.gBrowser.selectedTab,
+ tab,
+ "The captive portal tab should be open and selected in the new window."
+ );
+ testShowLoginPageButtonVisibility(notification, "hidden");
+ return win;
+}
+
+function ensurePortalTab(win) {
+ // For the tests that call this function, it's enough to ensure there
+ // are two tabs in the window - the default tab and the portal tab.
+ is(
+ win.gBrowser.tabs.length,
+ 2,
+ "There should be a captive portal tab in the window."
+ );
+}
+
+function ensurePortalNotification(win) {
+ let notification = win.gHighPriorityNotificationBox.getNotificationWithValue(
+ PORTAL_NOTIFICATION_VALUE
+ );
+ isnot(
+ notification,
+ null,
+ "There should be a captive portal notification in the window."
+ );
+ return notification;
+}
+
+// Helper to test whether the "Show Login Page" is visible in the captive portal
+// notification (it should be hidden when the portal tab is selected).
+function testShowLoginPageButtonVisibility(notification, visibility) {
+ let showLoginPageButton = notification.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.gHighPriorityNotificationBox.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;
+}
diff --git a/browser/base/content/test/chrome/chrome.ini b/browser/base/content/test/chrome/chrome.ini
new file mode 100644
index 0000000000..9882f4b647
--- /dev/null
+++ b/browser/base/content/test/chrome/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_aboutCrashed.xhtml]
+[test_aboutRestartRequired.xhtml]
diff --git a/browser/base/content/test/chrome/test_aboutCrashed.xhtml b/browser/base/content/test/chrome/test_aboutCrashed.xhtml
new file mode 100644
index 0000000000..78e0da55c9
--- /dev/null
+++ b/browser/base/content/test/chrome/test_aboutCrashed.xhtml
@@ -0,0 +1,78 @@
+<?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[
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ 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");
+ let uri1 = Services.io.newURI("http://www.example.com/1");
+ 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..2addde4f4c
--- /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[
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ 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");
+ let uri1 = Services.io.newURI("http://www.example.com/1");
+ 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/contextMenu/.eslintrc.js b/browser/base/content/test/contextMenu/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/contextMenu/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/contextMenu/browser.ini b/browser/base/content/test/contextMenu/browser.ini
new file mode 100644
index 0000000000..a8fea52423
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser.ini
@@ -0,0 +1,38 @@
+[DEFAULT]
+prefs =
+ plugin.load_flash_only=false
+support-files =
+ subtst_contextmenu_webext.html
+ test_contextmenu_links.html
+ subtst_contextmenu.html
+ subtst_contextmenu_input.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
+
+[browser_contextmenu_loadblobinnewtab.js]
+support-files = browser_contextmenu_loadblobinnewtab.html
+[browser_contextmenu_save_blocked.js]
+[browser_contextmenu_spellcheck.js]
+skip-if = toolkit == "gtk" || (os == "win" && processor == "aarch64") # disabled on Linux due to bug 513558, aarch64 due to 1533161
+[browser_view_image.js]
+support-files =
+ test_view_image_revoked_cached_blob.html
+[browser_contextmenu_touch.js]
+skip-if = true # Bug 1424433, disable due to very high frequency failure rate also on Windows 10
+[browser_contextmenu_linkopen.js]
+[browser_contextmenu_iframe.js]
+support-files =
+ test_contextmenu_iframe.html
+[browser_utilityOverlay.js]
+[browser_utilityOverlayPrincipal.js]
+[browser_contextmenu_childprocess.js]
+[browser_contextmenu.js]
+tags = fullscreen
+skip-if = toolkit == "gtk" || verify || (os == "win" && processor == "aarch64") # disabled on Linux due to bug 513558, aarch64 due to 1531590
+[browser_contextmenu_input.js]
+skip-if = toolkit == "gtk" || (os == "win" && processor == "aarch64") # disabled on Linux due to bug 513558, aarch64 due to 1533161
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..9ff50c091c
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu.js
@@ -0,0 +1,2058 @@
+"use strict";
+
+let contextMenu;
+let LOGIN_FILL_ITEMS = [
+ "---",
+ null,
+ "fill-login",
+ null,
+ [
+ "fill-login-no-logins",
+ false,
+ "---",
+ null,
+ "fill-login-saved-passwords",
+ true,
+ ],
+ null,
+];
+let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
+let hasContainers =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ ContextualIdentityService.getPublicIdentities().length;
+
+const example_base =
+ "http://example.com/browser/browser/base/content/test/contextMenu/";
+const chrome_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+const head_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/contextMenu/";
+
+/* import-globals-from contextmenu_common.js */
+Services.scriptloader.loadSubScript(
+ chrome_base + "contextmenu_common.js",
+ this
+);
+
+/* import-globals-from ../general/head.js */
+Services.scriptloader.loadSubScript(head_base + "head.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_task(async function init() {
+ // Ensure screenshots is really disabled (bug 1498738).
+ const addon = await AddonManager.getAddonByID("screenshots@mozilla.org");
+ await addon.disable({ allowSystemAddons: true });
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+
+ // 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 SpecialPowers.pushPrefEnv({
+ set: [["dom.menuitem.enabled", true]],
+ });
+
+ 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() {
+ plainTextItems = [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ true,
+ [],
+ null,
+ "---",
+ null,
+ "context-viewbgimage",
+ false,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ true,
+ ];
+ await test_contextmenu("#test-text", plainTextItems, {
+ maybeScreenshotsPresent: true,
+ });
+});
+
+add_task(async function test_link() {
+ await test_contextmenu("#test-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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+});
+
+add_task(async function test_link_in_shadow_dom() {
+ await test_contextmenu(
+ "#shadow-host",
+ [
+ "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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ }
+ );
+});
+
+add_task(async function test_mailto() {
+ await test_contextmenu("#test-mailto", [
+ "context-copyemail",
+ true,
+ "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-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "---",
+ null,
+ "context-saveimage",
+ true,
+ "context-sendimage",
+ true,
+ "context-setDesktopBackground",
+ true,
+ "context-viewimageinfo",
+ 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,
+ "context-selectall",
+ true,
+ ],
+ {
+ maybeScreenshotsPresent: 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-media-hidecontrols",
+ true,
+ "context-video-fullscreen",
+ true,
+ "context-video-pictureinpicture",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "---",
+ null,
+ "context-savevideo",
+ true,
+ "context-video-saveimage",
+ 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-media-hidecontrols",
+ true,
+ "context-video-fullscreen",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "---",
+ null,
+ "context-savevideo",
+ true,
+ "context-video-saveimage",
+ 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-copyaudiourl",
+ true,
+ "---",
+ null,
+ "context-saveaudio",
+ 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-media-hidecontrols",
+ false,
+ "context-video-fullscreen",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "---",
+ null,
+ "context-savevideo",
+ true,
+ "context-video-saveimage",
+ false,
+ "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-media-hidecontrols",
+ false,
+ "context-video-fullscreen",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "---",
+ null,
+ "context-savevideo",
+ true,
+ "context-video-saveimage",
+ false,
+ "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-media-hidecontrols",
+ false,
+ "context-video-fullscreen",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ false,
+ "context-copyvideourl",
+ false,
+ "---",
+ null,
+ "context-savevideo",
+ false,
+ "context-video-saveimage",
+ 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-media-hidecontrols",
+ false,
+ "context-video-fullscreen",
+ false,
+ "---",
+ null,
+ "context-viewvideo",
+ false,
+ "context-copyvideourl",
+ false,
+ "---",
+ null,
+ "context-savevideo",
+ false,
+ "context-video-saveimage",
+ false,
+ "context-sendvideo",
+ false,
+ ]);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_iframe() {
+ await test_contextmenu("#test-iframe", [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ true,
+ [],
+ null,
+ "---",
+ null,
+ "context-viewbgimage",
+ false,
+ "context-selectall",
+ true,
+ "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-viewframesource",
+ true,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ 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-media-hidecontrols",
+ true,
+ "context-video-fullscreen",
+ true,
+ "context-video-pictureinpicture",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "---",
+ null,
+ "context-savevideo",
+ true,
+ "context-video-saveimage",
+ true,
+ "context-sendvideo",
+ true,
+ "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-media-hidecontrols",
+ true,
+ "context-video-fullscreen",
+ true,
+ "---",
+ null,
+ "context-viewvideo",
+ true,
+ "context-copyvideourl",
+ true,
+ "---",
+ null,
+ "context-savevideo",
+ true,
+ "context-video-saveimage",
+ true,
+ "context-sendvideo",
+ true,
+ "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-copyaudiourl",
+ true,
+ "---",
+ null,
+ "context-saveaudio",
+ true,
+ "context-sendaudio",
+ true,
+ "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-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "---",
+ null,
+ "context-saveimage",
+ true,
+ "context-sendimage",
+ true,
+ "context-setDesktopBackground",
+ true,
+ "context-viewimageinfo",
+ true,
+ "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",
+ [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ true,
+ [],
+ null,
+ "context-selectall",
+ true,
+ "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,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ true,
+ ],
+ { maybeScreenshotsPresent: 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,
+ "---", 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,
+ "---", 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, {
+ maybeScreenshotsPresent: true,
+ });
+});
+
+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,
+ "---", 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,
+ 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,
+ "---", 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}
+ );
+ */
+});
+
+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,
+ "http://mozilla.com/",
+ "paste for command cmd_paste"
+ );
+ });
+ },
+ });
+});
+
+add_task(async function test_pagemenu() {
+ await test_contextmenu(
+ "#test-pagemenu",
+ [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "+Plain item",
+ { type: "", icon: "", checked: false, disabled: false },
+ "+Disabled item",
+ { type: "", icon: "", checked: false, disabled: true },
+ "+Item w/ textContent",
+ { type: "", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "+Checkbox",
+ { type: "checkbox", icon: "", checked: true, disabled: false },
+ "---",
+ null,
+ "+Radio1",
+ { type: "checkbox", icon: "", checked: true, disabled: false },
+ "+Radio2",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "+Radio3",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "+Item w/ icon",
+ { type: "", icon: "favicon.ico", checked: false, disabled: false },
+ "+Item w/ bad icon",
+ { type: "", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "generated-submenu-1",
+ true,
+ [
+ "+Radio1",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "+Radio2",
+ { type: "checkbox", icon: "", checked: true, disabled: false },
+ "+Radio3",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "+Checkbox",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ ],
+ null,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ true,
+ [],
+ null,
+ "---",
+ null,
+ "context-viewbgimage",
+ false,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ true,
+ ],
+ {
+ async postCheckContextMenuFn() {
+ let item = contextMenu.getElementsByAttribute(
+ "generateditemid",
+ "1"
+ )[0];
+ ok(item, "Got generated XUL menu item");
+ item.doCommand();
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function() {
+ let pagemenu = content.document.getElementById("test-pagemenu");
+ Assert.ok(
+ !pagemenu.hasAttribute("hopeless"),
+ "attribute got removed"
+ );
+ }
+ );
+ },
+ maybeScreenshotsPresent: true,
+ }
+ );
+});
+
+add_task(async function test_dom_full_screen() {
+ await test_contextmenu(
+ "#test-dom-full-screen",
+ [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-leave-dom-fullscreen",
+ true,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ true,
+ [],
+ null,
+ "---",
+ null,
+ "context-viewbgimage",
+ false,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ true,
+ ],
+ {
+ maybeScreenshotsPresent: true,
+ shiftkey: true,
+ async preCheckContextMenuFn() {
+ await pushPrefs(
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"]
+ );
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function() {
+ let doc = content.document;
+ let win = doc.defaultView;
+ let full_screen_element = doc.getElementById(
+ "test-dom-full-screen"
+ );
+ let awaitFullScreenChange = ContentTaskUtils.waitForEvent(
+ win,
+ "fullscreenchange"
+ );
+ full_screen_element.requestFullscreen();
+ await awaitFullScreenChange;
+ }
+ );
+ },
+ async postCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function() {
+ let win = content.document.defaultView;
+ let awaitFullScreenChange = ContentTaskUtils.waitForEvent(
+ win,
+ "fullscreenchange"
+ );
+ content.document.exitFullscreen();
+ await awaitFullScreenChange;
+ }
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_pagemenu2() {
+ await test_contextmenu(
+ "#test-text",
+ [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ true,
+ [],
+ null,
+ "---",
+ null,
+ "context-viewbgimage",
+ false,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ true,
+ ],
+ { maybeScreenshotsPresent: true, shiftkey: true }
+ );
+});
+
+add_task(async function test_select_text() {
+ await test_contextmenu(
+ "#test-select-text",
+ [
+ "context-copy",
+ true,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "context-print-selection",
+ true,
+ "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._initialized = false;
+ await test_contextmenu(
+ "#test-select-text",
+ [
+ "context-copy",
+ true,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-print-selection",
+ true,
+ "context-viewpartialsource-selection",
+ true,
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ async preCheckContextMenuFn() {
+ await selectText("#test-select-text");
+ },
+ }
+ );
+ // Pretend the search service is not initialised.
+ Services.search.wrappedJSObject._initialized = true;
+});
+
+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,
+ "context-copy",
+ true,
+ "context-selectall",
+ true,
+ "---",
+ null,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ "context-print-selection",
+ true,
+ "context-viewpartialsource-selection",
+ true,
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ async preCheckContextMenuFn() {
+ await selectText("#test-select-text-link");
+ },
+ async postCheckContextMenuFn() {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function() {
+ let win = content.document.defaultView;
+ win.getSelection().removeAllRanges();
+ }
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_imagelink() {
+ await test_contextmenu("#test-image-link", [
+ "context-openlinkintab",
+ true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink",
+ true,
+ "context-openlinkprivate",
+ true,
+ "---",
+ null,
+ "context-bookmarklink",
+ true,
+ "context-savelink",
+ true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink",
+ true,
+ "---",
+ null,
+ "context-viewimage",
+ true,
+ "context-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "---",
+ null,
+ "context-saveimage",
+ true,
+ "context-sendimage",
+ true,
+ "context-setDesktopBackground",
+ true,
+ "context-viewimageinfo",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+});
+
+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,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", true,
+ "---", null,
+ "context-selectall", true,
+ "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,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", true,
+ "---", null,
+ "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-copyimage-contents",
+ true,
+ "context-copyimage",
+ true,
+ "---",
+ null,
+ "context-saveimage",
+ true,
+ "context-sendimage",
+ true,
+ "context-setDesktopBackground",
+ true,
+ "context-viewimageinfo",
+ true,
+ "context-viewimagedesc",
+ true,
+ ]);
+});
+
+add_task(async function test_srcdoc() {
+ await test_contextmenu("#test-srcdoc", [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ true,
+ [],
+ null,
+ "---",
+ null,
+ "context-viewbgimage",
+ false,
+ "context-selectall",
+ true,
+ "frame",
+ null,
+ getThisFrameSubMenu([
+ "context-reloadframe",
+ true,
+ "---",
+ null,
+ "context-saveframe",
+ true,
+ "---",
+ null,
+ "context-printframe",
+ true,
+ "---",
+ null,
+ "context-viewframesource",
+ true,
+ "context-viewframeinfo",
+ true,
+ ]),
+ null,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ 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,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+
+ 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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+
+ 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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+});
+
+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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+
+ 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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+
+ 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,
+ "context-searchselect",
+ true,
+ "context-searchselect-private",
+ true,
+ "---",
+ null,
+ "context-sendlinktodevice",
+ true,
+ [],
+ null,
+ ]);
+});
+
+add_task(async function test_cleanup_html() {
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * Selects the text of the element that matches the provided `selector`
+ *
+ * @param {String} selector
+ * A selector passed to querySelector to find
+ * the element that will be referenced.
+ */
+async function selectText(selector) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [selector],
+ async function(contentSelector) {
+ info(`Selecting text of ${contentSelector}`);
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.getSelection().removeAllRanges();
+ let div = doc.createRange();
+ let element = doc.querySelector(contentSelector);
+ Assert.ok(element, "Found element to select text from");
+ div.setStartBefore(element);
+ div.setEndAfter(element);
+ win.getSelection().addRange(div);
+ }
+ );
+}
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_childprocess.js b/browser/base/content/test/contextMenu/browser_contextmenu_childprocess.js
new file mode 100644
index 0000000000..af85d0a62c
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_childprocess.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const gBaseURL =
+ "https://example.com/browser/browser/base/content/test/contextMenu/";
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.menuitem.enabled", true]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ gBaseURL + "subtst_contextmenu.html"
+ );
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+
+ // Get the point of the element with the page menu (test-pagemenu) and
+ // synthesize a right mouse click there.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#test-pagemenu",
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ tab.linkedBrowser
+ );
+ await popupShownPromise;
+
+ checkMenu(contextMenu);
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+function checkItems(menuitem, arr) {
+ for (let i = 0; i < arr.length; i += 2) {
+ let str = arr[i];
+ let details = arr[i + 1];
+ if (str == "---") {
+ is(menuitem.localName, "menuseparator", "menuseparator");
+ } else if ("children" in details) {
+ is(menuitem.localName, "menu", "submenu");
+ is(menuitem.getAttribute("label"), str, str + " label");
+ checkItems(menuitem.menupopup.firstElementChild, details.children);
+ } else {
+ is(menuitem.localName, "menuitem", str + " menuitem");
+
+ is(menuitem.getAttribute("label"), str, str + " label");
+ is(menuitem.getAttribute("type"), details.type, str + " type");
+ is(
+ menuitem.getAttribute("image"),
+ details.icon ? gBaseURL + details.icon : "",
+ str + " icon"
+ );
+
+ if (details.checked) {
+ is(menuitem.getAttribute("checked"), "true", str + " checked");
+ } else {
+ ok(!menuitem.hasAttribute("checked"), str + " checked");
+ }
+
+ if (details.disabled) {
+ is(menuitem.getAttribute("disabled"), "true", str + " disabled");
+ } else {
+ ok(!menuitem.hasAttribute("disabled"), str + " disabled");
+ }
+ }
+
+ menuitem = menuitem.nextElementSibling;
+ }
+}
+
+function checkMenu(contextMenu) {
+ let items = [
+ "Plain item",
+ { type: "", icon: "", checked: false, disabled: false },
+ "Disabled item",
+ { type: "", icon: "", checked: false, disabled: true },
+ "Item w/ textContent",
+ { type: "", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "Checkbox",
+ { type: "checkbox", icon: "", checked: true, disabled: false },
+ "---",
+ null,
+ "Radio1",
+ { type: "checkbox", icon: "", checked: true, disabled: false },
+ "Radio2",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "Radio3",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "Item w/ icon",
+ { type: "", icon: "favicon.ico", checked: false, disabled: false },
+ "Item w/ bad icon",
+ { type: "", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "Submenu",
+ {
+ children: [
+ "Radio1",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "Radio2",
+ { type: "checkbox", icon: "", checked: true, disabled: false },
+ "Radio3",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ "---",
+ null,
+ "Checkbox",
+ { type: "checkbox", icon: "", checked: false, disabled: false },
+ ],
+ },
+ ];
+ checkItems(contextMenu.children[2], items);
+}
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..0663f0d713
--- /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 menuPopup = contextMenu.querySelector("#frame").menupopup;
+ const menuPopupPromise = BrowserTestUtils.waitForEvent(
+ menuPopup,
+ "popupshown"
+ );
+ menuPopup.openPopup();
+ await menuPopupPromise;
+
+ let domItem = contextMenu.querySelector("#context-openframeintab");
+ info("Going to click item " + domItem.id);
+ ok(
+ BrowserTestUtils.is_visible(domItem),
+ "DOM context menu item tab should be visible"
+ );
+ ok(!domItem.disabled, "DOM context menu item tab shouldn't be disabled");
+ domItem.click();
+
+ let openedTab = await openPromise;
+ contextMenu.hidePopup();
+ 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..94e9465fd5
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_input.js
@@ -0,0 +1,332 @@
+"use strict";
+
+let contextMenu;
+let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
+
+add_task(async function test_setup() {
+ const example_base =
+ "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);
+
+ // Ensure screenshots is really disabled (bug 1498738)
+ const addon = await AddonManager.getAddonByID("screenshots@mozilla.org");
+ await addon.disable({ allowSystemAddons: true });
+});
+
+add_task(async function test_text_input() {
+ await test_contextmenu("#input_text", [
+ "context-undo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ true,
+ "context-copy",
+ true,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "---",
+ null,
+ "context-selectall",
+ false,
+ "---",
+ null,
+ "spell-check-enabled",
+ true,
+ ]);
+});
+
+add_task(async function test_text_input_disabled() {
+ await test_contextmenu(
+ "#input_disabled",
+ [
+ "context-undo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ true,
+ "context-copy",
+ true,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "---",
+ null,
+ "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]],
+ });
+ todo(
+ false,
+ "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled"
+ );
+ await test_contextmenu(
+ "#input_password",
+ [
+ "fill-login",
+ null,
+ [
+ "fill-login-no-logins",
+ false,
+ "---",
+ null,
+ "fill-login-saved-passwords",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-undo",
+ false,
+ "---",
+ null,
+ "context-cut",
+ true,
+ "context-copy",
+ true,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "---",
+ null,
+ "context-selectall",
+ 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 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,
+ "---",
+ null,
+ "context-cut",
+ true,
+ "context-copy",
+ true,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "---",
+ null,
+ "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,
+ [
+ "context-navigation",
+ null,
+ [
+ "context-back",
+ false,
+ "context-forward",
+ false,
+ "context-reload",
+ true,
+ "context-bookmarkpage",
+ true,
+ ],
+ null,
+ "---",
+ null,
+ "context-savepage",
+ true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---",
+ null,
+ "context-sendpagetodevice",
+ null,
+ [],
+ null,
+ "---",
+ null,
+ "context-viewbgimage",
+ false,
+ "context-selectall",
+ null,
+ "---",
+ null,
+ "context-viewsource",
+ true,
+ "context-viewinfo",
+ true,
+ ],
+ {
+ // XXX Bug 1345081. Currently the Screenshots menu option is shown for
+ // various text elements even though it is set to type "page". That bug
+ // should remove the need for next line.
+ maybeScreenshotsPresent: 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,
+ "---",
+ null,
+ "context-cut",
+ true,
+ "context-copy",
+ true,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "---",
+ null,
+ "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,
+ "---",
+ null,
+ "context-cut",
+ true,
+ "context-copy",
+ true,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ false,
+ "---",
+ null,
+ "context-selectall",
+ null,
+ ],
+ {
+ // XXX Bug 1345081. Currently the Screenshots menu option is shown for
+ // various text elements even though it is set to type "page". That bug
+ // should remove the need for next line.
+ maybeScreenshotsPresent: true,
+ skipFocusChange: true,
+ }
+ );
+});
+
+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..1b9c6e2809
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_linkopen.js
@@ -0,0 +1,111 @@
+/* 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);
+ let bounds = domItem.getBoundingClientRect();
+ ok(
+ bounds.height && bounds.width,
+ "DOM context menu item " + where + " should be visible"
+ );
+ ok(
+ !domItem.disabled,
+ "DOM context menu item " + where + " shouldn't be disabled"
+ );
+ domItem.click();
+ contextMenu.hidePopup();
+ 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..d8e7be99bf
--- /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.loadURI(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.loadURI(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 setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.bloburl_per_agent_cluster", false]],
+ });
+});
+
+add_task(async function test_rightclick_open_bloburl_in_new_tab() {
+ let blobDataFromLoadedPage = await rightClickOpenInNewTabAndReturnContent(
+ "blob-url-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
+
+add_task(async function test_rightclick_open_bloburl_referrer_in_new_tab() {
+ let blobDataFromLoadedPage = await rightClickOpenInNewTabAndReturnContent(
+ "blob-url-referrer-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
+
+add_task(async function test_open_bloburl_in_new_tab() {
+ let blobDataFromLoadedPage = await openInNewTabAndReturnContent(
+ "blob-url-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
+
+add_task(async function test_open_bloburl_referrer_in_new_tab() {
+ let blobDataFromLoadedPage = await openInNewTabAndReturnContent(
+ "blob-url-referrer-link"
+ );
+ is(
+ blobDataFromLoadedPage,
+ blobDataAsString,
+ "The blobURL is correctly loaded"
+ );
+});
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js b/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js
new file mode 100644
index 0000000000..e3c8ac6145
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js
@@ -0,0 +1,80 @@
+/* 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: {
+ applications: { gecko: { id: "cancel@test" } },
+ name: "Cancel Test",
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ 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;
+ };
+ EventUtils.synthesizeMouseAtCenter(
+ menu.querySelector("#context-savelink"),
+ {}
+ );
+ });
+ }
+ );
+
+ await ext.unload();
+});
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..3a150fecff
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_spellcheck.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let contextMenu;
+
+const example_base =
+ "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);
+
+ // Ensure screenshots is really disabled (bug 1498738)
+ const addon = await AddonManager.getAddonByID("screenshots@mozilla.org");
+ await addon.disable({ allowSystemAddons: true });
+});
+
+add_task(async function test_text_input_spellcheck() {
+ await test_contextmenu(
+ "#input_spellcheck_no_value",
+ [
+ "context-undo",
+ 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,
+ "---",
+ 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,
+ "---",
+ null,
+ "context-cut",
+ null,
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "---",
+ 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,
+ "---",
+ null,
+ "context-cut",
+ null,
+ "context-copy",
+ null,
+ "context-paste",
+ null, // ignore clipboard state
+ "context-delete",
+ null,
+ "---",
+ 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.loadURI(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.loadURI(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.loadURI(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_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..81e6462a38
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_touch.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This test checks that context menus are in touchmode
+ * when opened through a touch event (long tap). */
+
+async function openAndCheckContextMenu(contextMenu, target) {
+ is(contextMenu.state, "closed", "Context menu is initally closed.");
+
+ let popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeNativeTapAtCenter(target, true);
+ await popupshown;
+
+ is(contextMenu.state, "open", "Context menu is open.");
+ is(
+ contextMenu.getAttribute("touchmode"),
+ "true",
+ "Context menu is in touchmode."
+ );
+
+ contextMenu.hidePopup();
+
+ popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(target, { type: "contextmenu" });
+ await popupshown;
+
+ is(contextMenu.state, "open", "Context menu is open.");
+ ok(
+ !contextMenu.hasAttribute("touchmode"),
+ "Context menu is not in touchmode."
+ );
+
+ contextMenu.hidePopup();
+}
+
+// Ensure that we can run touch events properly for windows [10]
+add_task(async function setup() {
+ let isWindows = AppConstants.isPlatformAndVersionAtLeast("win", "10.0");
+ await SpecialPowers.pushPrefEnv({
+ set: [["apz.test.fails_with_native_injection", isWindows]],
+ });
+});
+
+// Test the content area context menu.
+add_task(async function test_contentarea_contextmenu_touch() {
+ await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ await openAndCheckContextMenu(contextMenu, browser);
+ });
+});
+
+// Test the back and forward buttons.
+add_task(async function test_back_forward_button_contextmenu_touch() {
+ 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")
+ );
+ BrowserTestUtils.loadURI(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_utilityOverlay.js b/browser/base/content/test/contextMenu/browser_utilityOverlay.js
new file mode 100644
index 0000000000..ff470b2a15
--- /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_getTopWin() {
+ is(getTopWin(), window, "got top window");
+});
+
+add_task(async function test_openUILink() {
+ const kURL = "http://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..2fe9b2c9a0
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_utilityOverlayPrincipal.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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,
+ "http://example.com/"
+ )); // remote tab
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(async function() {
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ "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,
+ "http://example.com/",
+ "sanity: correct doc.nodePrincipal URL"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+ runNextTest();
+ });
+
+ // Ensure we get the correct default of "allowInheritPrincipal: false" from openUILink
+ 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..36496a2327
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_view_image.js
@@ -0,0 +1,148 @@
+/* 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 }) {
+ let mainURL = http_base + page;
+ let accel = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
+ let tests = {
+ tab: {
+ event: { [accel]: true },
+ async loadedPromise() {
+ return BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => url.startsWith("blob"),
+ true
+ ).then(t => t.linkedBrowser);
+ },
+ cleanup(browser) {
+ BrowserTestUtils.removeTab(gBrowser.getTabForBrowser(browser));
+ },
+ },
+ window: {
+ event: { 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() && getCx().currentWindowGlobal.documentURI.schemeIs("blob")
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ return ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "complete"
+ );
+ });
+ return browser;
+ },
+ async cleanup(browser) {
+ return BrowserTestUtils.closeWindow(browser.ownerGlobal);
+ },
+ },
+ self: {
+ event: {},
+ async loadedPromise() {
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url => url.startsWith("blob:")
+ );
+ return gBrowser.selectedBrowser;
+ },
+ async cleanup() {},
+ },
+ // NOTE: If we ever add more tests here, add them above and not below `self`, as it replaces
+ // the test document.
+ };
+ 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)) {
+ 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();
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("context-viewimage"),
+ test.event
+ );
+ await promisePopupHidden;
+
+ let newBrowser = await browserPromise;
+ 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);
+ }
+ });
+}
+
+/**
+ * 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",
+ });
+});
+
+/**
+ * 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",
+ });
+});
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..6a340367e6
--- /dev/null
+++ b/browser/base/content/test/contextMenu/contextmenu_common.js
@@ -0,0 +1,474 @@
+// 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.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm",
+ {}
+ );
+ 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();
+ }
+
+ var isPageMenuItem = item.hasAttribute("generateditemid");
+
+ 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 if (isPageMenuItem) {
+ is(
+ item.id,
+ "",
+ "child menuitem #" + i + " is a generated page menu item"
+ );
+ } 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 (isPageMenuItem) {
+ 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-saved-passwords" &&
+ item.id != "fill-login-no-logins" &&
+ // XXX Screenshots doesn't have an access key. This needs
+ // at least bug 1320462 fixing first.
+ item.id != "screenshots_mozilla_org-menuitem-_create-screenshot" &&
+ // Inspect accessibility properties does not have an access key. See
+ // bug 1630717 for more details.
+ item.id != "context-inspect-a11y"
+ ) {
+ 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 && !isPageMenuItem) {
+ items.push(item.id);
+ }
+ if (isPageMenuItem) {
+ var p = {};
+ p.type = item.getAttribute("type");
+ p.icon = item.getAttribute("image");
+ p.checked = item.hasAttribute("checked");
+ p.disabled = item.hasAttribute("disabled");
+ items.push(p);
+ } else {
+ 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") {
+ if (isPageMenuItem) {
+ item.id = "generated-submenu-" + aData.generatedSubmenuId++;
+ }
+ ok(item.id, "child menu #" + i + " has an ID");
+ if (!isPageMenuItem) {
+ 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
+ * maybeScreenshotsPresent: if true, the screenshots menu entry is
+ * expected to be present in the menu if
+ * screenshots is enabled, optional
+ * 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.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ 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 = ["---", null];
+ if (Services.prefs.getBoolPref("devtools.accessibility.enabled", true)) {
+ inspectItems.push("context-inspect-a11y", true);
+ }
+
+ inspectItems.push("context-inspect", true);
+ menuItems = menuItems.concat(inspectItems);
+ }
+
+ if (
+ options.maybeScreenshotsPresent &&
+ !Services.prefs.getBoolPref("extensions.screenshots.disabled", false)
+ ) {
+ let screenshotItems = [
+ "---",
+ null,
+ "screenshots_mozilla_org-menuitem-_create-screenshot",
+ true,
+ ];
+
+ menuItems = menuItems.concat(screenshotItems);
+ }
+
+ 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/subtst_contextmenu.html b/browser/base/content/test/contextMenu/subtst_contextmenu.html
new file mode 100644
index 0000000000..68f2bf4fae
--- /dev/null
+++ b/browser/base/content/test/contextMenu/subtst_contextmenu.html
@@ -0,0 +1,90 @@
+<!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>
+<script>
+// Create the shadow DOM in case shadow DOM is enabled.
+if ("ShadowRoot" in this) {
+ var sr = document.getElementById("shadow-host").attachShadow({mode: "closed"});
+ sr.innerHTML = "<a href='http://mozilla.com'>Click the monkey!</a>";
+}
+</script>
+<a id="test-mailto" href="mailto:codemonkey@mozilla.com">Mail the monkey!</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 contextmenu="myMenu">
+ <p id="test-pagemenu" hopeless="true">I've got a context menu!</p>
+ <menu id="myMenu" type="context">
+ <menuitem label="Plain item" onclick="document.getElementById('test-pagemenu').removeAttribute('hopeless');"></menuitem>
+ <menuitem label="Disabled item" disabled></menuitem>
+ <menuitem> Item w/ textContent</menuitem>
+ <menu>
+ <menuitem type="checkbox" label="Checkbox" checked></menuitem>
+ </menu>
+ <menu>
+ <menuitem type="radio" label="Radio1" checked></menuitem>
+ <menuitem type="radio" label="Radio2"></menuitem>
+ <menuitem type="radio" label="Radio3"></menuitem>
+ </menu>
+ <menu>
+ <menuitem label="Item w/ icon" icon="favicon.ico"></menuitem>
+ <menuitem label="Item w/ bad icon" icon="http://example.com%0a%23.google.com/"></menuitem>
+ </menu>
+ <menu label="Submenu">
+ <menuitem type="radio" label="Radio1" radiogroup="rg"></menuitem>
+ <menuitem type="radio" label="Radio2" checked radiogroup="rg"></menuitem>
+ <menuitem type="radio" label="Radio3" radiogroup="rg"></menuitem>
+ <menu>
+ <menuitem type="checkbox" label="Checkbox"></menuitem>
+ </menu>
+ </menu>
+ <menu hidden>
+ <menuitem label="Bogus item"></menuitem>
+ </menu>
+ <menu>
+ </menu>
+ <menuitem label="Hidden item" hidden></menuitem>
+ <menuitem></menuitem>
+ </menu>
+</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>
+</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..f7921f6833
--- /dev/null
+++ b/browser/base/content/test/contextMenu/subtst_contextmenu_input.html
@@ -0,0 +1,29 @@
+<!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">
+</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_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/.eslintrc.js b/browser/base/content/test/favicons/.eslintrc.js
new file mode 100644
index 0000000000..7612459de1
--- /dev/null
+++ b/browser/base/content/test/favicons/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test", "plugin:mozilla/mochitest-test"],
+};
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..8842572935
--- /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/webp,*/*";
+ if (expected != request.getHeader("Accept")) {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+ response.setHeader("Location", "moz.png");
+}
diff --git a/browser/base/content/test/favicons/auth_test.html b/browser/base/content/test/favicons/auth_test.html
new file mode 100644
index 0000000000..90b78432f8
--- /dev/null
+++ b/browser/base/content/test/favicons/auth_test.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for http auth</title>
+ <link rel="icon" type="image/png" href="auth_test.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/auth_test.png b/browser/base/content/test/favicons/auth_test.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/base/content/test/favicons/auth_test.png
diff --git a/browser/base/content/test/favicons/auth_test.png^headers^ b/browser/base/content/test/favicons/auth_test.png^headers^
new file mode 100644
index 0000000000..5024ae1c4b
--- /dev/null
+++ b/browser/base/content/test/favicons/auth_test.png^headers^
@@ -0,0 +1,2 @@
+HTTP 401 Unauthorized
+WWW-Authenticate: Basic realm="Favicon auth"
diff --git a/browser/base/content/test/favicons/blank.html b/browser/base/content/test/favicons/blank.html
new file mode 100644
index 0000000000..297eb8cd78
--- /dev/null
+++ b/browser/base/content/test/favicons/blank.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+</head>
+</html>
diff --git a/browser/base/content/test/favicons/browser.ini b/browser/base/content/test/favicons/browser.ini
new file mode 100644
index 0000000000..6923b12515
--- /dev/null
+++ b/browser/base/content/test/favicons/browser.ini
@@ -0,0 +1,100 @@
+[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_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_referer.js]
+support-files =
+ file_favicon_no_referrer.html
+[browser_multiple_icons_in_short_timeframe.js]
+[browser_rich_icons.js]
+support-files =
+ file_rich_icon.html
+ file_mask_icon.html
+[browser_icon_discovery.js]
+[browser_invalid_href_fallback.js]
+support-files =
+ file_invalid_href.html
+[browser_preferred_icons.js]
+support-files =
+ icon.svg
+[browser_favicon_load.js]
+support-files =
+ file_favicon.html
+ file_favicon.png
+ file_favicon.png^headers^
+ file_favicon_thirdParty.html
+[browser_subframe_favicons_not_used.js]
+support-files =
+ file_bug970276_popup1.html
+ file_bug970276_popup2.html
+ file_bug970276_favicon2.ico
+[browser_missing_favicon.js]
+support-files =
+ blank.html
+[browser_redirect.js]
+support-files =
+ file_favicon_redirect.html
+ file_favicon_redirect.ico
+ file_favicon_redirect.ico^headers^
+[browser_rooticon.js]
+support-files =
+ blank.html
+[browser_mixed_content.js]
+support-files =
+ file_insecure_favicon.html
+ file_favicon.png
+[browser_title_flicker.js]
+support-files =
+ file_with_slow_favicon.html
+ blank.html
+ file_favicon.png
+[browser_favicon_cache.js]
+support-files =
+ cookie_favicon.sjs
+ cookie_favicon.html
+[browser_oversized.js]
+support-files =
+ large_favicon.html
+ large.png
+[browser_favicon_auth.js]
+support-files =
+ auth_test.html
+ auth_test.png
+ auth_test.png^headers^
+[browser_favicon_accept.js]
+support-files =
+ accept.html
+ accept.sjs
+[browser_favicon_nostore.js]
+support-files =
+ no-store.html
+ no-store.png
+ no-store.png^headers^
+[browser_favicon_crossorigin.js]
+support-files =
+ crossorigin.html
+ crossorigin.png
+ crossorigin.png^headers^
+[browser_favicon_credentials.js]
+support-files =
+ credentials1.html
+ credentials2.html
+ credentials.png
+ credentials.png^headers^
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..f0a9d25a78
--- /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.loadURI(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..64de975564
--- /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.loadURI(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..99bc7aa276
--- /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.loadURI(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..8ea12f2a1f
--- /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.loadURI(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..23e4deee61
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_cache.js
@@ -0,0 +1,48 @@
+add_task(async () => {
+ const testPath =
+ "http://example.com/browser/browser/base/content/test/favicons/cookie_favicon.html";
+ const resetPath =
+ "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.loadURI(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..5a249508d9
--- /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.loadURI(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..96c64f83ae
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js
@@ -0,0 +1,55 @@
+"use strict";
+
+const TEST_ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+const TEST_URL = TEST_ROOT + "file_favicon_change_not_in_document.html";
+
+/*
+ * This test tests a link element won't fire DOMLinkChanged/DOMLinkAdded unless
+ * it is added to the DOM. See more details in bug 1083895.
+ *
+ * Note that there is debounce logic in ContentLinkHandler.jsm, adding a new
+ * icon link after the icon parsing timeout will trigger a new icon extraction
+ * cycle. Hence, there should be two favicons loads in this test as it appends
+ * a new link to the DOM in the timeout callback defined in the test HTML page.
+ * However, the not-yet-added link element with href as "http://example.org/other-icon"
+ * should not fire the DOMLinkAdded event, nor should it fire the DOMLinkChanged
+ * event after its href gets updated later.
+ */
+add_task(async function() {
+ let extraTab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ROOT
+ ));
+ let domLinkAddedFired = 0;
+ let domLinkChangedFired = 0;
+ const linkAddedHandler = event => domLinkAddedFired++;
+ const linkChangedhandler = event => domLinkChangedFired++;
+ BrowserTestUtils.addContentEventListener(
+ gBrowser.selectedBrowser,
+ "DOMLinkAdded",
+ linkAddedHandler
+ );
+ BrowserTestUtils.addContentEventListener(
+ gBrowser.selectedBrowser,
+ "DOMLinkChanged",
+ linkChangedhandler
+ );
+
+ let expectedFavicon = TEST_ROOT + "file_generic_favicon.ico";
+ let faviconPromise = waitForFavicon(extraTab.linkedBrowser, expectedFavicon);
+
+ BrowserTestUtils.loadURI(extraTab.linkedBrowser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(extraTab.linkedBrowser);
+
+ await faviconPromise;
+
+ is(
+ domLinkAddedFired,
+ 2,
+ "Should fire the correct number of DOMLinkAdded event."
+ );
+ is(domLinkChangedFired, 0, "Should not fire any DOMLinkChanged event.");
+
+ gBrowser.removeTab(extraTab);
+});
diff --git a/browser/base/content/test/favicons/browser_favicon_credentials.js b/browser/base/content/test/favicons/browser_favicon_credentials.js
new file mode 100644
index 0000000000..f563c7b9d0
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_credentials.js
@@ -0,0 +1,71 @@
+/* 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_COM_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+);
+
+const FAVICON_URL = EXAMPLE_COM_ROOT + "credentials.png";
+
+function run_test(url, shouldHaveCookies, description) {
+ add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ const faviconPromise = waitForFaviconMessage(true, FAVICON_URL);
+
+ BrowserTestUtils.loadURI(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}).`
+ );
+ }
+ }
+ );
+ });
+}
+
+// crossorigin="" only has credentials in the same-origin case
+run_test(`${MOCHI_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(`${MOCHI_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..2410643126
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_crossorigin.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT_DIR = getRootDirectory(gTestPath);
+
+const MOCHI_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+const EXAMPLE_NET_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "http://example.net/"
+);
+
+const EXAMPLE_COM_ROOT = ROOT_DIR.replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+);
+
+const FAVICON_URL = EXAMPLE_COM_ROOT + "crossorigin.png";
+
+function run_test(root, shouldSucceed, description) {
+ add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ const faviconPromise = waitForFaviconMessage(true, FAVICON_URL);
+
+ BrowserTestUtils.loadURI(browser, `${root}crossorigin.html`);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ if (shouldSucceed) {
+ try {
+ const result = await faviconPromise;
+ Assert.equal(
+ result.iconURL,
+ FAVICON_URL,
+ `Should have seen the icon (${description}).`
+ );
+ } catch (e) {
+ Assert.ok(false, `Favicon load failed (${description}).`);
+ }
+ } else {
+ await Assert.rejects(
+ faviconPromise,
+ result => result.iconURL == FAVICON_URL,
+ `Should have failed to load the icon (${description}).`
+ );
+ }
+ }
+ );
+ });
+}
+
+// crossorigin.png only allows CORS for MOCHI_ROOT.
+run_test(EXAMPLE_NET_ROOT, false, "remote origin not allowed");
+run_test(MOCHI_ROOT, true, "remote origin allowed");
+
+// Same-origin but with the crossorigin attribute.
+run_test(EXAMPLE_COM_ROOT, true, "same-origin");
diff --git a/browser/base/content/test/favicons/browser_favicon_load.js b/browser/base/content/test/favicons/browser_favicon_load.js
new file mode 100644
index 0000000000..14afa0a317
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_load.js
@@ -0,0 +1,175 @@
+/**
+ * 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;
+
+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.defineModuleGetter(
+ this,
+ "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+function clearAllImageCaches() {
+ var tools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
+ var imageCache = tools.getImgCacheForDocument(window.document);
+ imageCache.clearCache(true); // true=chrome
+ imageCache.clearCache(false); // false=content
+}
+
+function clearAllPlacesFavicons() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "places-favicons-expired");
+ resolve();
+ }, "places-favicons-expired");
+
+ PlacesUtils.favicons.expireAllFavicons();
+ });
+}
+
+function FaviconObserver(aPageURI, aFaviconURL, aTailingEnabled) {
+ this.reset(aPageURI, aFaviconURL, aTailingEnabled);
+}
+
+FaviconObserver.prototype = {
+ observe(aSubject, aTopic, aData) {
+ // Make sure that the topic is 'http-on-modify-request'.
+ if (aTopic === "http-on-modify-request") {
+ let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
+ let reqLoadInfo = httpChannel.loadInfo;
+ // Make sure this is a favicon request.
+ if (httpChannel.URI.spec !== this._faviconURL) {
+ return;
+ }
+
+ let cos = aSubject.QueryInterface(Ci.nsIClassOfService);
+ if (!cos) {
+ ok(false, "Http channel should implement nsIClassOfService.");
+ return;
+ }
+
+ if (!reqLoadInfo) {
+ ok(false, "Should have load info.");
+ return;
+ }
+
+ let haveTailFlag = !!(cos.classFlags & Ci.nsIClassOfService.Tail);
+ info("classFlags=" + cos.classFlags);
+ is(haveTailFlag, this._tailingEnabled, "Should have correct cos flag.");
+ } else {
+ ok(false, "Received unexpected topic: ", aTopic);
+ }
+
+ this._faviconLoaded.resolve();
+ },
+
+ reset(aPageURI, aFaviconURL, aTailingEnabled) {
+ this._faviconURL = aFaviconURL;
+ this._faviconLoaded = PromiseUtils.defer();
+ this._tailingEnabled = aTailingEnabled;
+ },
+
+ get promise() {
+ return this._faviconLoaded.promise;
+ },
+};
+
+function waitOnFaviconLoaded(aFaviconURL) {
+ return PlacesTestUtils.waitForNotification(
+ "favicon-changed",
+ events => events.some(e => e.faviconUrl == aFaviconURL),
+ "places"
+ );
+}
+
+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..9702640118
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_nostore.js
@@ -0,0 +1,152 @@
+/* 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();
+ }
+ );
+ });
+});
+
+add_task(async function later_addition() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+ registerCleanupFunction(async () => {
+ await cleanup();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ const LATE_ICON_URL =
+ TEST_SITE + "/browser/browser/base/content/test/favicons/moz.png";
+ let iconPromise = waitForFaviconMessage(true, LATE_ICON_URL);
+ await ContentTask.spawn(gBrowser.selectedBrowser, LATE_ICON_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);
+ });
+ let { iconURL } = await iconPromise;
+ is(iconURL, LATE_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 root_icon_stored() {
+ AddonTestUtils.initMochitest(this);
+ let server = AddonTestUtils.createHttpServer({ hosts: ["www.nostore.com"] });
+ server.registerFile(
+ "/favicon.ico",
+ FileUtils.getFile(
+ "CurWorkD",
+ `/browser/browser/base/content/test/favicons/no-store.png`.split("/")
+ )
+ );
+ 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..243462f419
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_referer.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const FOLDER = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+add_task(async function test_check_referrer_for_discovered_favicon() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let referrerPromise = TestUtils.topicObserved(
+ "http-on-modify-request",
+ (s, t, d) => {
+ let chan = s.QueryInterface(Ci.nsIHttpChannel);
+ return chan.URI.spec == "http://mochi.test:8888/favicon.ico";
+ }
+ ).then(([chan]) => chan.getRequestHeader("Referer"));
+
+ BrowserTestUtils.loadURI(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.loadURI(
+ 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_icon_discovery.js b/browser/base/content/test/favicons/browser_icon_discovery.js
new file mode 100644
index 0000000000..81efe07d50
--- /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..dab858f7ee
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_invalid_href_fallback.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async () => {
+ const testPath =
+ "http://example.com/browser/browser/base/content/test/favicons/";
+ const expectedIcon = "http://example.com/favicon.ico";
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let faviconPromise = waitForLinkAvailable(browser);
+ BrowserTestUtils.loadURI(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..739601c1d7
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_missing_favicon.js
@@ -0,0 +1,33 @@
+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.loadURI(browser, testPath + "file_with_favicon.html");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let iconURI = await faviconPromise;
+ is(iconURI, expectedIcon, "Got correct icon.");
+
+ BrowserTestUtils.loadURI(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..d5f9544132
--- /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..0ffb1add08
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_oversized.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ let faviconPromise = waitForFaviconMessage(true, `${ROOT}large.png`);
+
+ BrowserTestUtils.loadURI(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..8b9893f264
--- /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_task(async function setup() {
+ 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..676e20ec7a
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_rooticon.js
@@ -0,0 +1,22 @@
+add_task(async () => {
+ const testPath =
+ "http://example.com/browser/browser/base/content/test/favicons/blank.html";
+ 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.loadURI(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..9bf28e75fa
--- /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..4d66f54529
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_title_flicker.js
@@ -0,0 +1,183 @@
+const TEST_PATH =
+ "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.loadURI(
+ 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;
+ 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.loadURI(
+ 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..a3b697011a
--- /dev/null
+++ b/browser/base/content/test/favicons/cookie_favicon.sjs
@@ -0,0 +1,22 @@
+/* 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", "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..c9595bb1d3
--- /dev/null
+++ b/browser/base/content/test/favicons/credentials.png^headers^
@@ -0,0 +1,3 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
+Access-Control-Allow-Credentials: true
+Set-Cookie: faviconCookie2=test
diff --git a/browser/base/content/test/favicons/credentials1.html b/browser/base/content/test/favicons/credentials1.html
new file mode 100644
index 0000000000..d4e0a61596
--- /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="http://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..ef7e1e3645
--- /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="http://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/discovery.html b/browser/base/content/test/favicons/discovery.html
new file mode 100644
index 0000000000..2ff2aaa5f2
--- /dev/null
+++ b/browser/base/content/test/favicons/discovery.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Autodiscovery Test</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_bug970276_favicon1.ico b/browser/base/content/test/favicons/file_bug970276_favicon1.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_favicon1.ico
Binary files differ
diff --git a/browser/base/content/test/favicons/file_bug970276_favicon2.ico b/browser/base/content/test/favicons/file_bug970276_favicon2.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_favicon2.ico
Binary files differ
diff --git a/browser/base/content/test/favicons/file_bug970276_popup1.html b/browser/base/content/test/favicons/file_bug970276_popup1.html
new file mode 100644
index 0000000000..5ce7dab879
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_popup1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bug 970276.</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_bug970276_favicon1.ico">
+</head>
+<body>
+ Test file for bug 970276.
+
+ <iframe src="file_bug970276_popup2.html">
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_bug970276_popup2.html b/browser/base/content/test/favicons/file_bug970276_popup2.html
new file mode 100644
index 0000000000..0b9e5294ef
--- /dev/null
+++ b/browser/base/content/test/favicons/file_bug970276_popup2.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bug 970276.</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_bug970276_favicon2.ico">
+</head>
+<body>
+ Test inner file for bug 970276.
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon.html b/browser/base/content/test/favicons/file_favicon.html
new file mode 100644
index 0000000000..f294b47758
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for originAttributes</title>
+ <link rel="icon" type="image/png" href="file_favicon.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon.png b/browser/base/content/test/favicons/file_favicon.png
new file mode 100644
index 0000000000..5535363c94
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon.png
Binary files differ
diff --git a/browser/base/content/test/favicons/file_favicon.png^headers^ b/browser/base/content/test/favicons/file_favicon.png^headers^
new file mode 100644
index 0000000000..9e23c73b7f
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon.png^headers^
@@ -0,0 +1 @@
+Cache-Control: no-cache
diff --git a/browser/base/content/test/favicons/file_favicon_change.html b/browser/base/content/test/favicons/file_favicon_change.html
new file mode 100644
index 0000000000..035549c5aa
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_change.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="file_bug970276_favicon1.ico" type="image/ico" id="i">
+</head>
+<body>
+ <script>
+ window.addEventListener("PleaseChangeFavicon", function() {
+ var ico = document.getElementById("i");
+ ico.setAttribute("href", "moz.png");
+ });
+ </script>
+</body></html>
diff --git a/browser/base/content/test/favicons/file_favicon_change_not_in_document.html b/browser/base/content/test/favicons/file_favicon_change_not_in_document.html
new file mode 100644
index 0000000000..c44a2f8153
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_change_not_in_document.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="file_bug970276_favicon1.ico" type="image/ico" id="i">
+</head>
+<body onload="onload()">
+ <script>
+ function onload() {
+ var ico = document.createElement("link");
+ ico.setAttribute("rel", "icon");
+ ico.setAttribute("type", "image/ico");
+ ico.setAttribute("href", "file_bug970276_favicon1.ico");
+ setTimeout(function() {
+ ico.setAttribute("href", "file_generic_favicon.ico");
+ document.getElementById("i").remove();
+ document.head.appendChild(ico);
+ }, 1000);
+ }
+ </script>
+</body></html>
diff --git a/browser/base/content/test/favicons/file_favicon_no_referrer.html b/browser/base/content/test/favicons/file_favicon_no_referrer.html
new file mode 100644
index 0000000000..4f363ffd04
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_no_referrer.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for referrer</title>
+ <link rel="icon" type="image/png" referrerpolicy="origin" href="file_favicon.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon_redirect.html b/browser/base/content/test/favicons/file_favicon_redirect.html
new file mode 100644
index 0000000000..9da4777591
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_redirect.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file with an icon that redirects</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_favicon_redirect.ico">
+</head>
+<body>
+ Test file for bugs with favicons
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_favicon_redirect.ico b/browser/base/content/test/favicons/file_favicon_redirect.ico
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_redirect.ico
diff --git a/browser/base/content/test/favicons/file_favicon_redirect.ico^headers^ b/browser/base/content/test/favicons/file_favicon_redirect.ico^headers^
new file mode 100644
index 0000000000..380fa3d3a4
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_redirect.ico^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Found
+Location: http://example.com/browser/browser/base/content/test/favicons/file_generic_favicon.ico
diff --git a/browser/base/content/test/favicons/file_favicon_thirdParty.html b/browser/base/content/test/favicons/file_favicon_thirdParty.html
new file mode 100644
index 0000000000..7d690e5981
--- /dev/null
+++ b/browser/base/content/test/favicons/file_favicon_thirdParty.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for originAttributes</title>
+ <link rel="icon" type="image/png" href="http://mochi.test:8888/browser/browser/base/content/test/favicons/file_favicon.png" />
+ </head>
+ <body>
+ Third Party Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_generic_favicon.ico b/browser/base/content/test/favicons/file_generic_favicon.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/browser/base/content/test/favicons/file_generic_favicon.ico
Binary files differ
diff --git a/browser/base/content/test/favicons/file_insecure_favicon.html b/browser/base/content/test/favicons/file_insecure_favicon.html
new file mode 100644
index 0000000000..7b13b47829
--- /dev/null
+++ b/browser/base/content/test/favicons/file_insecure_favicon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Favicon Test for mixed content</title>
+ <link rel="icon" type="image/png" href="http://example.com/browser/browser/base/content/test/favicons/file_favicon.png" />
+ </head>
+ <body>
+ Favicon!!
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_invalid_href.html b/browser/base/content/test/favicons/file_invalid_href.html
new file mode 100644
index 0000000000..087ff01403
--- /dev/null
+++ b/browser/base/content/test/favicons/file_invalid_href.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bugs with invalid hrefs for favicons</title>
+
+ <!--Empty href; that's the whole point of this file.-->
+ <link rel="icon" href="">
+</head>
+<body>
+ Test file for bugs with invalid hrefs for favicons
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_mask_icon.html b/browser/base/content/test/favicons/file_mask_icon.html
new file mode 100644
index 0000000000..5bcd9e694f
--- /dev/null
+++ b/browser/base/content/test/favicons/file_mask_icon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Mask Icon</title>
+ <link rel="icon" mask href="moz.png" type="image/png" />
+ <link rel="mask-icon" href="moz.png" type="image/png" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_rich_icon.html b/browser/base/content/test/favicons/file_rich_icon.html
new file mode 100644
index 0000000000..ce7550b611
--- /dev/null
+++ b/browser/base/content/test/favicons/file_rich_icon.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Rich Icons</title>
+ <link rel="icon" href="moz.png" type="image/png" />
+ <link rel="apple-touch-icon" sizes="96x96" href="rich_moz_1.png" type="image/png" />
+ <link rel="apple-touch-icon" sizes="256x256" href="rich_moz_2.png" type="image/png" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/favicons/file_with_favicon.html b/browser/base/content/test/favicons/file_with_favicon.html
new file mode 100644
index 0000000000..0702b4aaba
--- /dev/null
+++ b/browser/base/content/test/favicons/file_with_favicon.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bugs with favicons</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_generic_favicon.ico">
+</head>
+<body>
+ Test file for bugs with favicons
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/file_with_slow_favicon.html b/browser/base/content/test/favicons/file_with_slow_favicon.html
new file mode 100644
index 0000000000..76fb015587
--- /dev/null
+++ b/browser/base/content/test/favicons/file_with_slow_favicon.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for title flicker</title>
+</head>
+<body>
+ <!-- Putting the icon down here means we won't start loading it until the doc is fully parsed -->
+ <link rel="icon" href="file_generic_favicon.ico">
+</body>
+</html>
diff --git a/browser/base/content/test/favicons/head.js b/browser/base/content/test/favicons/head.js
new file mode 100644
index 0000000000..23cb63b116
--- /dev/null
+++ b/browser/base/content/test/favicons/head.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ LinkHandlerParent: "resource:///actors/LinkHandlerParent.jsm",
+});
+
+// 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/.eslintrc.js b/browser/base/content/test/forms/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/forms/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/forms/browser.ini b/browser/base/content/test/forms/browser.ini
new file mode 100644
index 0000000000..e9a474a10e
--- /dev/null
+++ b/browser/base/content/test/forms/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+prefs =
+ gfx.font_loader.delay=0
+ gfx.font_loader.interval=0
+support-files =
+ head.js
+
+[browser_selectpopup.js]
+skip-if = os == "linux" || (os == "mac" && !debug) || (verify && (os == 'win')) # Bug 1329991 - linux, Bug 1661132 - osx
+[browser_selectpopup_colors.js]
+skip-if = os == "linux" # Bug 1329991 - test fails intermittently on Linux builds
+[browser_selectpopup_searchfocus.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..89cbaea664
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -0,0 +1,1337 @@
+/* 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.
+
+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 openSelectPopup(
+ selectPopup,
+ mode = "key",
+ selector = "select",
+ win = window
+) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+
+ 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
+ );
+ }
+
+ return Promise.all([popupShownPromise, mousePromise]);
+ }
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ return popupShownPromise;
+}
+
+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 menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ await openSelectPopup(selectPopup);
+
+ 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(selectPopup);
+
+ 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(selectPopup, "click");
+ await hideSelectPopup(selectPopup, "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(selectPopup, "click");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup(selectPopup, "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_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.select_popup_in_parent.enabled", true],
+ ["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);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // First, try it when a different <select> element than the one that is open is removed
+ await openSelectPopup(selectPopup, "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(selectPopup);
+
+ // Next, try it when the same <select> element than the one that is open is removed
+ await openSelectPopup(selectPopup, "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(selectPopup, "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;
+ });
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // First, get the position of the select popup when no translations have been applied.
+ await openSelectPopup(selectPopup);
+
+ let rect = selectPopup.getBoundingClientRect();
+ let expectedX = rect.left;
+ let expectedY = rect.top;
+
+ await hideSelectPopup(selectPopup);
+
+ // 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(selectPopup);
+
+ 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(selectPopup);
+ }
+
+ 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) {
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // 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: false,
+ },
+ {
+ 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: false,
+ },
+ {
+ 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(
+ selectPopup,
+ "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(selectPopup, mode);
+ await eventsPromise;
+ }
+ }
+ );
+});
+
+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();
+ });
+
+ let selectPopup = win.document.getElementById("ContentSelectDropdown")
+ .menupopup;
+ let browserRect = browser.getBoundingClientRect();
+
+ // Check if a drag-select works and scrolls the list.
+ await openSelectPopup(selectPopup, "mousedown", "select", win);
+
+ 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" },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position after mousemove over button should not change"
+ );
+
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.top + 10,
+ { type: "mousemove" },
+ 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" },
+ 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" },
+ 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" },
+ 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" },
+ win
+ );
+ await scrolledPromise;
+ ok(true, "scroll position at drag up from option");
+
+ scrollPos = getScrollPos();
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 25,
+ { type: "mouseup" },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position at mouseup from option should not change"
+ );
+
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 20,
+ { type: "mousemove" },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position at mousemove after mouseup should not change"
+ );
+
+ await hideSelectPopup(selectPopup, "escape", win);
+
+ let positions = [
+ "margin-top: 300px;",
+ "position: fixed; bottom: 200px;",
+ "width: 100%; height: 9999px;",
+ ];
+
+ let position;
+ while (positions.length) {
+ await openSelectPopup(selectPopup, "key", "select", win);
+
+ let rect = selectPopup.getBoundingClientRect();
+ ok(
+ rect.top >= browserRect.top,
+ "Popup top position in within browser area"
+ );
+ ok(
+ rect.bottom <= browserRect.bottom,
+ "Popup bottom position in within browser area"
+ );
+
+ // Don't check the scroll position for the last step as the popup will be cut off.
+ if (positions.length) {
+ let cs = win.getComputedStyle(selectPopup);
+ let bpBottom =
+ parseFloat(cs.paddingBottom) + parseFloat(cs.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.
+ SimpleTest.isfuzzy(
+ selectPopup.children[selectedOption].getBoundingClientRect().bottom,
+ selectPopup.getBoundingClientRect().bottom - bpBottom,
+ 1,
+ "Popup scroll at correct position " + bpBottom
+ );
+ }
+
+ await hideSelectPopup(selectPopup, "enter", win);
+
+ position = positions.shift();
+
+ let contentPainted = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "MozAfterPaint"
+ );
+ await SpecialPowers.spawn(browser, [position], async function(
+ contentPosition
+ ) {
+ let select = content.document.getElementById("one");
+ select.setAttribute("style", contentPosition || "");
+ select.getBoundingClientRect();
+ });
+ await contentPainted;
+ }
+
+ if (navigator.platform.indexOf("Mac") == 0) {
+ await SpecialPowers.spawn(browser, [], async function() {
+ let doc = content.document;
+ doc.body.style = "padding-top: 400px;";
+
+ let select = doc.getElementById("one");
+ select.options[41].selected = true;
+ select.focus();
+ });
+
+ await openSelectPopup(selectPopup, "key", "select", win);
+
+ ok(
+ selectPopup.getBoundingClientRect().top >
+ browser.getBoundingClientRect().top,
+ "select popup appears over selected item"
+ );
+
+ await hideSelectPopup(selectPopup, "escape", win);
+ }
+}
+
+// This test checks select elements with a large number of options to ensure that
+// the popup appears within the browser area.
+add_task(async function test_large_popup() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ await performLargePopupTests(window);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test checks the same as the previous test but in a new, vertically smaller window.
+add_task(async function test_large_popup_in_small_window() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ let resizePromise = BrowserTestUtils.waitForEvent(
+ newWin,
+ "resize",
+ false,
+ e => {
+ info(`Got resize event (innerHeight: ${newWin.innerHeight})`);
+ return newWin.innerHeight <= 400;
+ }
+ );
+ newWin.resizeTo(600, 400);
+ await resizePromise;
+
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ newWin.gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, pageUrl);
+ await browserLoadedPromise;
+
+ newWin.gBrowser.selectedBrowser.focus();
+
+ await performLargePopupTests(newWin);
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+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 = win.document.getElementById("ContentSelectDropdown")
+ .menupopup;
+ await openSelectPopup(selectPopup, 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(selectPopup, "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);
+
+ let selectPopup = document.getElementById("ContentSelectDropdown").menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mousedown" },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ 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",
+ });
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mouseup" },
+ gBrowser.selectedBrowser
+ );
+
+ await hideSelectPopup(selectPopup);
+
+ // The popup should be closed when fullscreen mode is entered or exited.
+ for (let steps = 0; steps < 2; steps++) {
+ await openSelectPopup(selectPopup, "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 = document.getElementById("ContentSelectDropdown").menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mousedown" },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ // The exact number is not needed; just ensure the height is larger than 4 items to accomodate any popup borders.
+ ok(
+ selectPopup.getBoundingClientRect().height >=
+ selectPopup.lastElementChild.getBoundingClientRect().height * 4,
+ "Height contains at least 4 items"
+ );
+ ok(
+ selectPopup.getBoundingClientRect().height <
+ selectPopup.lastElementChild.getBoundingClientRect().height * 5,
+ "Height doesn't contain 5 items"
+ );
+
+ // The label contains the substring 'Visible' for items that are visible.
+ // Otherwise, it is expected to be display: none.
+ is(selectPopup.parentNode.itemCount, 9, "Correct number of items");
+ let child = selectPopup.firstElementChild;
+ let idx = 1;
+ while (child) {
+ is(
+ getComputedStyle(child).display,
+ child.label.indexOf("Visible") > 0 ? "-moz-box" : "none",
+ "Item " + idx++ + " is visible"
+ );
+ child = child.nextElementSibling;
+ }
+
+ await hideSelectPopup(selectPopup, "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 menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ await openSelectPopup(selectPopup);
+
+ 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);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ info("Opening the popup");
+ await openSelectPopup(selectPopup, "click");
+
+ info("Opened the popup");
+ let nonZoomedFontSize = parseFloat(
+ getComputedStyle(selectPopup.querySelector("menuitem")).fontSize,
+ 10
+ );
+
+ info("font-size is " + nonZoomedFontSize);
+ await hideSelectPopup(selectPopup);
+
+ info("Hid 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(selectPopup, "click");
+
+ let zoomedFontSize = parseFloat(
+ getComputedStyle(selectPopup.querySelector("menuitem")).fontSize,
+ 10
+ );
+ info("Zoomed font-size is " + zoomedFontSize);
+
+ ok(
+ Math.abs(zoomedFontSize - nonZoomedFontSize * 2.0) < 0.01,
+ `Zoom should affect menu popup size, got ${zoomedFontSize}, ` +
+ `expected ${nonZoomedFontSize * 2.0}`
+ );
+
+ await hideSelectPopup(selectPopup);
+ info("Hid the popup again");
+
+ ZoomManager.toggleZoom();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+function getIsHandlingUserInput(browser, elementId, eventName) {
+ return SpecialPowers.spawn(browser, [[elementId, eventName]], async function([
+ contentElementId,
+ contentEventName,
+ ]) {
+ let element = content.document.getElementById(contentElementId);
+ let isHandlingUserInput = false;
+ await ContentTaskUtils.waitForEvent(element, contentEventName, false, e => {
+ isHandlingUserInput = content.window.windowUtils.isHandlingUserInput;
+ return true;
+ });
+
+ return isHandlingUserInput;
+ });
+}
+
+// This test checks if the change/click event is considered as user input event.
+add_task(async function test_handling_user_input() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // Test onchange event when changing value via keyboard.
+ await openSelectPopup(selectPopup, "click", "#one");
+ let getPromise = getIsHandlingUserInput(tab.linkedBrowser, "one", "change");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup(selectPopup);
+ is(await getPromise, true, "isHandlingUserInput should be true");
+
+ // Test onchange event when changing value via mouse click
+ await openSelectPopup(selectPopup, "click", "#two");
+ getPromise = getIsHandlingUserInput(tab.linkedBrowser, "two", "change");
+ EventUtils.synthesizeMouseAtCenter(selectPopup.lastElementChild, {});
+ is(await getPromise, true, "isHandlingUserInput should be true");
+
+ // Test onclick event fired from clicking select popup.
+ await openSelectPopup(selectPopup, "click", "#three");
+ getPromise = getIsHandlingUserInput(tab.linkedBrowser, "three", "click");
+ EventUtils.synthesizeMouseAtCenter(selectPopup.firstElementChild, {});
+ is(await getPromise, true, "isHandlingUserInput should be true");
+
+ 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);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // Test change and input events get handled consistently
+ await openSelectPopup(selectPopup, "click");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup(selectPopup);
+
+ 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);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ await openSelectPopup(selectPopup, "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..2f60c80f7c
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_colors.js
@@ -0,0 +1,683 @@
+const 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": "rgba(0, 0, 0, 0)", "unstyled": "true"}</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": "rgba(0, 0, 0, 0)", "unstyled": "true"}</option>' +
+ ' <option value="Seven" selected="true">{"unstyled": "true"}</option>' +
+ "</select></body></html>";
+
+const 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>";
+
+const 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>";
+
+const 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>";
+
+const 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>";
+
+const 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>";
+
+const 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>";
+
+const DISABLED_OPTGROUP_AND_OPTIONS =
+ "<html><head>" +
+ "<body><select id='one'>" +
+ ' <optgroup label=\'{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}\'>' +
+ ' <option disabled="">{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option disabled="">{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ " </optgroup>" +
+ ' <optgroup label=\'{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}\' disabled=\'\'>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ " </optgroup>" +
+ ' <option value="Two" selected="true">{"end": "true"}</option>' +
+ "</select></body></html>";
+
+const 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>";
+
+const SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS =
+ "<html><head><style>" +
+ " select { background-color: black; }" +
+ " option { color: white; }" +
+ "</style></head>" +
+ "<body><select id='one'>" +
+ ' <option>{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>";
+
+const 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": "rgb(0, 0, 0)", "backgroundColor": "rgb(255, 255, 255)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>";
+
+const 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>";
+
+const 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": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>";
+
+const SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND =
+ "<html><head><style>" +
+ " select { transition: all .1s; }" +
+ " select:focus { text-shadow: 0 0 0 #303030; }" +
+ "</style></head><body><select id='one'>" +
+ ' <option>{"color": "rgb(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>";
+
+const 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>";
+
+let SELECT_LONG_WITH_TRANSITION =
+ "<html><head><style>" +
+ " select { transition: all .2s linear; }" +
+ " select:focus { color: purple; }" +
+ "</style></head><body><select id='one'>";
+for (let i = 0; i < 75; i++) {
+ SELECT_LONG_WITH_TRANSITION +=
+ ' <option>{"color": "rgb(128, 0, 128)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>';
+}
+SELECT_LONG_WITH_TRANSITION +=
+ ' <option selected="true">{"end": "true"}</option>' +
+ "</select></body></html>";
+
+const 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 class="redColor">{"color": "rgb(255, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</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>
+`;
+
+const 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>
+`;
+
+const 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>
+`;
+
+function getSystemColor(color) {
+ // Need to convert system color to RGB color.
+ let textarea = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "textarea"
+ );
+ textarea.style.display = "none";
+ textarea.style.color = color;
+ document.documentElement.appendChild(textarea);
+ let computed = getComputedStyle(textarea).color;
+ textarea.remove();
+ return computed;
+}
+
+function testOptionColors(index, item, menulist) {
+ // The label contains a JSON string of the expected colors for
+ // `color` and `background-color`.
+ let expected = JSON.parse(item.label);
+
+ for (let color of Object.keys(expected)) {
+ if (
+ color.toLowerCase().includes("color") &&
+ !expected[color].startsWith("rgb")
+ ) {
+ expected[color] = getSystemColor(expected[color]);
+ }
+ }
+
+ // 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"),
+ `Item ${index} should not have any custom option styling: ${item.outerHTML}`
+ );
+ } else {
+ is(
+ getComputedStyle(item).color,
+ expected.color,
+ "Item " + index + " has correct foreground color"
+ );
+ is(
+ getComputedStyle(item).backgroundColor,
+ expected.backgroundColor,
+ "Item " + index + " has correct background color"
+ );
+ if (expected.textShadow) {
+ is(
+ getComputedStyle(item).textShadow,
+ expected.textShadow,
+ "Item " + index + " has correct text-shadow color"
+ );
+ }
+ }
+}
+
+async function openSelectPopup(select) {
+ const pageUrl = "data:text/html," + escape(select);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mousedown" },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+ return { tab, menulist, selectPopup };
+}
+
+async function testSelectColors(select, itemCount, options) {
+ let { tab, menulist, selectPopup } = await openSelectPopup(select);
+ if (options.waitForComputedStyle) {
+ let property = options.waitForComputedStyle.property;
+ let value = options.waitForComputedStyle.value;
+ await TestUtils.waitForCondition(() => {
+ info(
+ `<select> has ${property}: ${getComputedStyle(selectPopup)[property]}`
+ );
+ return getComputedStyle(selectPopup)[property] == value;
+ }, `Waiting for <select> to have ${property}: ${value}`);
+ }
+
+ is(selectPopup.parentNode.itemCount, itemCount, "Correct number of items");
+ let child = selectPopup.firstElementChild;
+ let idx = 1;
+
+ if (!options.skipSelectColorTest) {
+ is(
+ getComputedStyle(selectPopup).color,
+ options.selectColor,
+ "popup has expected foreground color"
+ );
+
+ if (options.selectTextShadow) {
+ is(
+ getComputedStyle(selectPopup).textShadow,
+ options.selectTextShadow,
+ "popup has expected text-shadow color"
+ );
+ }
+
+ // Combine the select popup's backgroundColor and the
+ // backgroundImage color to get the color that is seen by
+ // the user.
+ let base = getComputedStyle(selectPopup).backgroundColor;
+ 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(selectPopup).backgroundImage;
+ if (topCoat == "none") {
+ is(
+ `rgb(${bR}, ${bG}, ${bB})`,
+ options.selectBgColor,
+ "popup has expected background color"
+ );
+ } 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,
+ "popup has expected background color"
+ );
+ }
+ }
+
+ ok(!child.selected, "The first child should not be selected");
+ while (child) {
+ testOptionColors(idx, child, menulist);
+ idx++;
+ child = child.nextElementSibling;
+ }
+
+ if (!options.leaveOpen) {
+ await hideSelectPopup(selectPopup, "escape");
+ BrowserTestUtils.removeTab(tab);
+ }
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.select_popup_in_parent.enabled", true],
+ ["dom.forms.select.customstyling", true],
+ ],
+ });
+});
+
+// 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 = {
+ selectColor: getSystemColor("-moz-ComboboxText"),
+ selectBgColor: getSystemColor("-moz-Combobox"),
+ };
+ 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: "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: "rgb(255, 255, 255)",
+ };
+
+ 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: "rgb(255, 255, 255)",
+ waitForComputedStyle: {
+ property: "color",
+ value: "rgb(128, 0, 128)",
+ },
+ leaveOpen: true,
+ };
+
+ await testSelectColors(SELECT_LONG_WITH_TRANSITION, 76, options);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.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(selectPopup, "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)",
+ selectBgColor: "rgb(255, 255, 255)",
+ selectTextShadow: "rgb(0, 0, 255) 1px 1px 2px",
+ leaveOpen: true,
+ };
+
+ await testSelectColors(
+ SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT,
+ 4,
+ options
+ );
+
+ let stylesheetEl = document.getElementById(
+ "ContentSelectDropdownStylesheet"
+ );
+ let sheet = stylesheetEl.sheet;
+ /* Check that there are no rulesets for the first option, but that
+ one exists for the second option and sets the color of that
+ option to "rgb(255, 0, 0)" */
+
+ function hasMatchingRuleForOption(cssRules, index, styles = {}) {
+ for (let rule of cssRules) {
+ if (rule.selectorText.includes(`:nth-child(${index})`)) {
+ if (
+ Object.keys(styles).some(key => rule.style[key] !== styles[key])
+ ) {
+ continue;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ is(
+ hasMatchingRuleForOption(sheet.cssRules, 1),
+ false,
+ "There should be no rules specific to option1"
+ );
+ is(
+ hasMatchingRuleForOption(sheet.cssRules, 2, {
+ color: "rgb(255, 0, 0)",
+ }),
+ true,
+ "There should be a rule specific to option2 and it should have color: red"
+ );
+ is(
+ hasMatchingRuleForOption(sheet.cssRules, 3, {
+ "text-shadow": "rgb(0, 0, 0) 1px 1px 2px",
+ }),
+ true,
+ "There should be a rule specific to option3 and it should have text-shadow: rgb(0, 0, 0) 1px 1px 2px"
+ );
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ await hideSelectPopup(selectPopup, "escape");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+);
+
+add_task(async function test_select_font_inherits_to_option() {
+ let { tab, menulist, selectPopup } = await openSelectPopup(
+ 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(selectPopup, "escape");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_scrollbar_props() {
+ let { tab, selectPopup } = await openSelectPopup(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(selectPopup, "escape");
+ BrowserTestUtils.removeTab(tab);
+});
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..bdfcf98005
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
@@ -0,0 +1,52 @@
+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_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.select_popup_in_parent.enabled", true],
+ ["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 menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mousedown" },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ 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(selectPopup, "escape");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/forms/head.js b/browser/base/content/test/forms/head.js
new file mode 100644
index 0000000000..4776639054
--- /dev/null
+++ b/browser/base/content/test/forms/head.js
@@ -0,0 +1,20 @@
+function hideSelectPopup(selectPopup, mode = "enter", win = window) {
+ let browser = win.gBrowser.selectedBrowser;
+ let selectClosedPromise = SpecialPowers.spawn(browser, [], async function() {
+ let { SelectContentHelper } = ChromeUtils.import(
+ "resource://gre/actors/SelectChild.jsm",
+ null
+ );
+ 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") {
+ EventUtils.synthesizeMouseAtCenter(selectPopup.lastElementChild, {}, win);
+ }
+
+ return selectClosedPromise;
+}
diff --git a/browser/base/content/test/fullscreen/.eslintrc.js b/browser/base/content/test/fullscreen/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/fullscreen/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/fullscreen/FullscreenFrame.jsm b/browser/base/content/test/fullscreen/FullscreenFrame.jsm
new file mode 100644
index 0000000000..28fabcd7f2
--- /dev/null
+++ b/browser/base/content/test/fullscreen/FullscreenFrame.jsm
@@ -0,0 +1,105 @@
+/* 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.
+ */
+
+var EXPORTED_SYMBOLS = ["FullscreenFrameChild"];
+
+class FullscreenFrameChild extends JSWindowActorChild {
+ actorCreated() {
+ this.fullscreen_events = [];
+ }
+
+ changed() {
+ return new Promise(resolve => {
+ this.contentWindow.document.addEventListener(
+ "fullscreenchange",
+ () => resolve(),
+ {
+ once: true,
+ }
+ );
+ });
+ }
+
+ requestFullscreen() {
+ let doc = this.contentWindow.document;
+ let button = doc.createElement("button");
+ doc.body.appendChild(button);
+
+ return new Promise(resolve => {
+ button.onclick = () => {
+ doc.body.requestFullscreen().then(resolve);
+ doc.body.removeChild(button);
+ };
+ button.click();
+ });
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "WaitForChange":
+ return this.changed();
+ case "ExitFullscreen":
+ return this.contentWindow.document.exitFullscreen();
+ case "RequestFullscreen":
+ this.browsingContext.isActive = true;
+ return Promise.all([this.changed(), this.requestFullscreen()]);
+ case "CreateChild":
+ let child = msg.data;
+ let iframe = this.contentWindow.document.createElement("iframe");
+ iframe.allow = child.allow_fullscreen ? "fullscreen" : "";
+ iframe.name = child.name;
+
+ let loaded = new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ () => resolve(iframe.browsingContext),
+ { once: true }
+ );
+ });
+ iframe.src = child.url;
+ this.contentWindow.document.body.appendChild(iframe);
+ return loaded;
+ case "GetEvents":
+ return Promise.resolve(this.fullscreen_events);
+ case "ClearEvents":
+ this.fullscreen_events = [];
+ return Promise.resolve();
+ case "GetFullscreenElement":
+ let document = this.contentWindow.document;
+ let child_iframe = this.contentWindow.document.getElementsByTagName(
+ "iframe"
+ )
+ ? this.contentWindow.document.getElementsByTagName("iframe")[0]
+ : null;
+ switch (document.fullscreenElement) {
+ case null:
+ return Promise.resolve("null");
+ case document:
+ return Promise.resolve("document");
+ case document.body:
+ return Promise.resolve("body");
+ case child_iframe:
+ return Promise.resolve("child_iframe");
+ default:
+ return Promise.resolve("other");
+ }
+ }
+
+ return Promise.reject("Unexpected Message");
+ }
+
+ async handleEvent(event) {
+ switch (event.type) {
+ case "fullscreenchange":
+ this.fullscreen_events.push(true);
+ break;
+ case "fullscreenerror":
+ this.fullscreen_events.push(false);
+ break;
+ }
+ }
+}
diff --git a/browser/base/content/test/fullscreen/browser.ini b/browser/base/content/test/fullscreen/browser.ini
new file mode 100644
index 0000000000..b5b52db326
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+ head.js
+ open_and_focus_helper.html
+[browser_bug1557041.js]
+skip-if = os == 'linux' # Bug 1561973
+[browser_fullscreen_permissions_prompt.js]
+skip-if = debug && os == 'mac' # Bug 1568570
+[browser_fullscreen_cross_origin.js]
+support-files = fullscreen.html fullscreen_frame.html
+[browser_bug1620341.js]
+support-files = fullscreen.html fullscreen_frame.html
+[browser_fullscreen_enterInUrlbar.js]
+skip-if = (os == 'mac') || (os == 'linux') #Bug 1648649
+[browser_fullscreen_window_open.js]
+skip-if = debug && os == 'mac' # Bug 1568570
+[browser_fullscreen_window_focus.js]
+skip-if =
+ os == 'linux' && fission # Bug 1677899
+ os == 'mac' # Bug 1568570
+ os == 'win' && fission # Bug 1677899
+[browser_fullscreen_api_fission.js]
+support-files = fullscreen.html FullscreenFrame.jsm
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..6dfa090676
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_bug1557041.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BrowserTestUtils } = ChromeUtils.import(
+ "resource://testing-common/BrowserTestUtils.jsm"
+);
+
+// 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.loadURI(browser, url);
+ await loaded;
+
+ let identityBox = document.getElementById("identity-box");
+
+ info("Entering DOM fullscreen");
+ await changeFullscreen(browser, true);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == document.getElementById("identity-popup")
+ );
+ let fsExit = waitForFullScreenState(browser, false);
+
+ identityBox.click();
+
+ info("Waiting for fullscreen exit and identity popup to show");
+ await Promise.all([fsExit, popupShown]);
+
+ let identityPopup = document.getElementById("identity-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..dc6a56a7d2
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_bug1620341.js
@@ -0,0 +1,92 @@
+/* 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")
+ );
+
+ // 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")
+ );
+
+ // Remove the remaining tab and leave the test.
+ let tabClosed = BrowserTestUtils.waitForTabClosing(tab1);
+ BrowserTestUtils.removeTab(tab1);
+ await tabClosed;
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js b/browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js
new file mode 100644
index 0000000000..4207b2b521
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_api_fission.js
@@ -0,0 +1,246 @@
+/* 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.jsm";
+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",
+ url: `http://example.com${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "A",
+ url: `http://example.org${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "C",
+ url: `http://example.com${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "D",
+ url: `http://example.com${fullscreenPath}?different-uri=1`,
+ allow_fullscreen: true,
+ children: [
+ {
+ name: "E",
+ url: `http://example.org${fullscreenPath}`,
+ allow_fullscreen: true,
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: "B",
+ 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: {
+ moduleURI: 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_cross_origin.js b/browser/base/content/test/fullscreen/browser_fullscreen_cross_origin.js
new file mode 100644
index 0000000000..b1199557b3
--- /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..0f549e7ecf
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_enterInUrlbar.js
@@ -0,0 +1,50 @@
+/* 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";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+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,
+ 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");
+ });
+});
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..377cf1d655
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_permissions_prompt.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test tends to trigger a race in the fullscreen time telemetry,
+// where the fullscreen enter and fullscreen exit events (which use the
+// same histogram ID) overlap. That causes TelemetryStopwatch to log an
+// error.
+SimpleTest.ignoreAllUncaughtExceptions(true);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/Not in fullscreen mode/);
+
+SimpleTest.requestCompleteLog();
+
+async function requestNotificationPermission(browser) {
+ return SpecialPowers.spawn(browser, [], () => {
+ return content.Notification.requestPermission();
+ });
+}
+
+async function requestCameraPermission(browser) {
+ return SpecialPowers.spawn(browser, [], () =>
+ content.navigator.mediaDevices
+ .getUserMedia({ video: true, fake: true })
+ .then(
+ () => true,
+ () => false
+ )
+ );
+}
+
+add_task(async function test_fullscreen_closes_permissionui_prompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.webnotifications.requireuserinteraction", false],
+ ["permissions.fullscreen.allowed", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ let browser = tab.linkedBrowser;
+
+ let popupShown, requestResult, popupHidden;
+
+ popupShown = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popupshown"
+ );
+
+ info("Requesting notification permission");
+ requestResult = requestNotificationPermission(browser);
+ await popupShown;
+
+ info("Entering DOM full-screen");
+ popupHidden = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ await changeFullscreen(browser, true);
+
+ await popupHidden;
+
+ is(
+ await requestResult,
+ "default",
+ "Expect permission request to be cancelled"
+ );
+
+ await changeFullscreen(browser, false);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_fullscreen_closes_webrtc_permission_prompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.navigator.permission.fake", true],
+ ["media.navigator.permission.force", true],
+ ["permissions.fullscreen.allowed", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ let browser = tab.linkedBrowser;
+ let popupShown, requestResult, popupHidden;
+
+ popupShown = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popupshown"
+ );
+
+ info("Requesting camera permission");
+ requestResult = requestCameraPermission(browser);
+
+ await popupShown;
+
+ info("Entering DOM full-screen");
+ popupHidden = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popuphidden"
+ );
+ await changeFullscreen(browser, true);
+
+ await popupHidden;
+
+ is(
+ await requestResult,
+ false,
+ "Expect webrtc permission request to be cancelled"
+ );
+
+ await changeFullscreen(browser, false);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_permission_prompt_closes_fullscreen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.webnotifications.requireuserinteraction", false],
+ ["permissions.fullscreen.allowed", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ let browser = tab.linkedBrowser;
+ info("Entering DOM full-screen");
+ await changeFullscreen(browser, true);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ window.PopupNotifications.panel,
+ "popupshown"
+ );
+ let fullScreenExit = waitForFullScreenState(browser, false);
+
+ info("Requesting notification permission");
+ requestNotificationPermission(browser).catch(() => {});
+ await popupShown;
+
+ info("Waiting for full-screen exit");
+ await fullScreenExit;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js b/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js
new file mode 100644
index 0000000000..c8a2ca745f
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js
@@ -0,0 +1,52 @@
+/* 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 TEST_URL =
+ "http://example.com/browser/browser/base/content/test/fullscreen/open_and_focus_helper.html";
+const IFRAME_ID = "testIframe";
+
+async function testWindowFocus(iframeID) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Calling window.open()");
+ let popup = await jsWindowOpen(tab.linkedBrowser, iframeID);
+ info("re-focusing main window");
+ await waitForFocus();
+
+ info("Entering full-screen");
+ await changeFullscreen(tab.linkedBrowser, true);
+
+ await testExpectFullScreenExit(tab.linkedBrowser, true, async () => {
+ info("Calling window.focus()");
+ await jsWindowFocus(tab.linkedBrowser, iframeID);
+ });
+
+ // Cleanup
+ popup.close();
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function setup() {
+ 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_parentWindowFocus() {
+ return testWindowFocus();
+});
+
+add_task(function test_iframeWindowFocus() {
+ return testWindowFocus(IFRAME_ID);
+});
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..d140b1389b
--- /dev/null
+++ b/browser/base/content/test/fullscreen/browser_fullscreen_window_open.js
@@ -0,0 +1,48 @@
+/* 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 TEST_URL =
+ "http://example.com/browser/browser/base/content/test/fullscreen/open_and_focus_helper.html";
+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, iframeID);
+ });
+
+ // Cleanup
+ await BrowserTestUtils.closeWindow(popup);
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function setup() {
+ 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);
+});
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..b6aade2e03
--- /dev/null
+++ b/browser/base/content/test/fullscreen/head.js
@@ -0,0 +1,125 @@
+const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+);
+function waitForFullScreenState(browser, state) {
+ return new Promise(resolve => {
+ let eventReceived = false;
+
+ let observe = (subject, topic, data) => {
+ if (!eventReceived) {
+ return;
+ }
+ Services.obs.removeObserver(observe, "fullscreen-painted");
+ resolve();
+ };
+ Services.obs.addObserver(observe, "fullscreen-painted");
+
+ browser.ownerGlobal.addEventListener(
+ `MozDOMFullscreen:${state ? "Entered" : "Exited"}`,
+ () => {
+ eventReceived = true;
+ },
+ { once: true }
+ );
+ });
+}
+
+/**
+ * Spawns content task in browser to enter / leave fullscreen
+ * @param browser - Browser to use for JS fullscreen requests
+ * @param {Boolean} fullscreenState - true to enter fullscreen, false to leave
+ * @returns {Promise} - Resolves once fullscreen change is applied
+ */
+async function changeFullscreen(browser, fullScreenState) {
+ await new Promise(resolve =>
+ SimpleTest.waitForFocus(resolve, browser.ownerGlobal)
+ );
+ let fullScreenChange = waitForFullScreenState(browser, fullScreenState);
+ SpecialPowers.spawn(browser, [fullScreenState], async state => {
+ // Wait for document focus before requesting full-screen
+ await ContentTaskUtils.waitForCondition(
+ () => content.browsingContext.isActive && content.document.hasFocus(),
+ "Waiting for document focus"
+ );
+ if (state) {
+ content.document.body.requestFullscreen();
+ } else {
+ content.document.exitFullscreen();
+ }
+ });
+ return fullScreenChange;
+}
+
+async function testExpectFullScreenExit(browser, leaveFS, action) {
+ let fsPromise = waitForFullScreenState(browser, !leaveFS);
+ 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");
+ });
+}
+
+async function jsWindowOpen(browser, iframeId) {
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+ ContentTask.spawn(browser, { 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, "open");
+ });
+ 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..088e278965
--- /dev/null
+++ b/browser/base/content/test/fullscreen/open_and_focus_helper.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset='utf-8'>
+</head>
+<body>
+ <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.popup = window.open('https://example.com', '', 'top=0,height=1, width=300');
+ if (canReply) event.ports[0].postMessage('opened');
+ } else if(event.data === "focus") {
+ window.popup.focus();
+ if (canReply) event.ports[0].postMessage('focused');
+ }
+ }
+ window.addEventListener('message', window.onMessage);
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/.eslintrc.js b/browser/base/content/test/general/.eslintrc.js
new file mode 100644
index 0000000000..7612459de1
--- /dev/null
+++ b/browser/base/content/test/general/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test", "plugin:mozilla/mochitest-test"],
+};
diff --git a/browser/base/content/test/general/alltabslistener.html b/browser/base/content/test/general/alltabslistener.html
new file mode 100644
index 0000000000..166c31037a
--- /dev/null
+++ b/browser/base/content/test/general/alltabslistener.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Test page for bug 463387</title>
+</head>
+<body>
+<p>Test page for bug 463387</p>
+</body>
+</html>
diff --git a/browser/base/content/test/general/app_bug575561.html b/browser/base/content/test/general/app_bug575561.html
new file mode 100644
index 0000000000..13c525487e
--- /dev/null
+++ b/browser/base/content/test/general/app_bug575561.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=575561
+-->
+ <head>
+ <title>Test for links in app tabs</title>
+ </head>
+ <body>
+ <a href="http://example.com/browser/browser/base/content/test/general/dummy_page.html">same domain</a>
+ <a href="http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html">same domain (different subdomain)</a>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html">different domain</a>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html" target="foo">different domain (with target)</a>
+ <a href="http://www.example.com/browser/browser/base/content/test/general/dummy_page.html">same domain (www prefix)</a>
+ <a href="data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>">data: URI</a>
+ <iframe src="app_subframe_bug575561.html"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/app_subframe_bug575561.html b/browser/base/content/test/general/app_subframe_bug575561.html
new file mode 100644
index 0000000000..8690497ffb
--- /dev/null
+++ b/browser/base/content/test/general/app_subframe_bug575561.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=575561
+-->
+ <head>
+ <title>Test for links in app tab subframes</title>
+ </head>
+ <body>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html">different domain</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/audio.ogg b/browser/base/content/test/general/audio.ogg
new file mode 100644
index 0000000000..477544875d
--- /dev/null
+++ b/browser/base/content/test/general/audio.ogg
Binary files differ
diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini
new file mode 100644
index 0000000000..7baa632780
--- /dev/null
+++ b/browser/base/content/test/general/browser.ini
@@ -0,0 +1,377 @@
+###############################################################################
+# 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]
+prefs =
+ plugin.load_flash_only=false # for plugin usage in browser_tab_dragdrop.js
+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
+ bug592338.html
+ bug792517-2.html
+ bug792517.html
+ bug792517.sjs
+ clipboard_pastefile.html
+ discovery.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
+ page_style_sample.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_addKeywordSearch.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]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_beforeunload_duplicate_dialogs.js]
+# 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_bug423833.js]
+skip-if = true # bug 428712
+# 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 = toolkit == "cocoa"
+# 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
+# skip-if = e10s # Bug 1134458 - Find bar doesn't work correctly in a detached tab
+# 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_bug1261299.js]
+skip-if = toolkit != "cocoa" # 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 = toolkit != "cocoa" # 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_bug575561.js]
+# 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]
+# 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]
+# 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_bug647886.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^
+# 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 && bits == 64 # 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_accesskeys.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_clipboard.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_clipboard_pastefile.js]
+skip-if = true # Disabled due to the clipboard not supporting real file types yet (bug 1288773)
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_contentAreaClick.js]
+skip-if = e10s # Clicks in content don't go through contentAreaClick with e10s.
+# 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_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_decoderDoctor.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_search_discovery.js]
+# 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_documentnavigation.js]
+skip-if = (verify && !debug && (os == 'linux'))
+# 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_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_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]
+skip-if = e10s # Bug 863514 - no gesture support.
+# 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]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_lastAccessedTab.js]
+skip-if = toolkit == "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]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_minimize.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+fail-if = (os == 'linux' && os_version == '18.04') # Bug 1600177
+[browser_modifiedclick_inherit_principal.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_new_http_window_opened_from_file_tab.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_page_style_menu.js]
+support-files =
+ page_style_only_alternates.html
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_page_style_menu_update.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_plainTextLinks.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_printpreview.js]
+skip-if = (os == 'win') || (os == 'linux' && bits == 64 && os_version == '18.04') # Bug 1384127
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_private_browsing_window.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_private_no_prompt.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_refreshBlocker.js]
+skip-if = os == "mac" || (os == "linux" && !debug )|| (os == "win" && bits == 32) # Bug 1559410 for all instances
+support-files =
+ refresh_header.sjs
+ refresh_meta.sjs
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_relatedTabs.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_remoteTroubleshoot.js]
+skip-if = !updater
+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_removeTabsToTheEnd.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 = (e10s && debug && os == "win") || verify # Bug 1280505
+# 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_link_when_window_navigates.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_tab_close_dependent_window.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tabDrop.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_tab_detach_restore.js]
+# 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]
+# 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]
+skip-if = (os == "mac" && !e10s) # Bug 1237713 - OSX eats keypresses for some reason
+# 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]
+run-if = e10s
+# 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_zbug569342.js]
+skip-if = e10s || debug # Bug 1094240 - has findbar-related failures
+# 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_windowactivation.js]
+skip-if = verify
+support-files =
+ file_window_activation.html
+ file_window_activation2.html
+# 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_domFullscreen_fullscreenMode.js]
+tags = fullscreen
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_newTabDrop.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_newWindowDrop.js]
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_newwindow_focus.js]
+skip-if = os == "linux" && !e10s # Bug 1263254 - Perma fails on Linux without e10s for some reason.
+# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+[browser_bug1299667.js]
+# 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..a04cb2da78
--- /dev/null
+++ b/browser/base/content/test/general/browser_accesskeys.js
@@ -0,0 +1,204 @@
+/* eslint-env mozilla/frame-script */
+
+add_task(async function() {
+ await pushPrefs(["ui.key.contentAccess", 5], ["ui.key.chromeAccess", 5]);
+
+ const gPageURL1 =
+ "data:text/html,<body><p>" +
+ "<button id='button' accesskey='y'>Button</button>" +
+ "<input id='checkbox' type='checkbox' accesskey='z'>Checkbox" +
+ "</p></body>";
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL1);
+
+ Services.focus.clearFocus(window);
+
+ // Press an accesskey in the child document while the chrome is focused.
+ let focusedId = await performAccessKey(tab1.linkedBrowser, "y");
+ is(focusedId, "button", "button accesskey");
+
+ // Press an accesskey in the child document while the content document is focused.
+ focusedId = await performAccessKey(tab1.linkedBrowser, "z");
+ is(focusedId, "checkbox", "checkbox accesskey");
+
+ // Add an element with an accesskey to the chrome and press its accesskey while the chrome is focused.
+ let newButton = document.createXULElement("button");
+ newButton.id = "chromebutton";
+ newButton.setAttribute("accesskey", "z");
+ document.documentElement.appendChild(newButton);
+
+ Services.focus.clearFocus(window);
+
+ newButton.getBoundingClientRect(); // Accesskey registration happens during frame construction.
+
+ focusedId = await performAccessKeyForChrome("z");
+ is(focusedId, "chromebutton", "chromebutton accesskey");
+
+ // Add a second tab and ensure that accesskey from the first tab is not used.
+ const gPageURL2 =
+ "data:text/html,<body>" +
+ "<button id='tab2button' accesskey='y'>Button in Tab 2</button>" +
+ "</body>";
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL2);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = await performAccessKey(tab2.linkedBrowser, "y");
+ is(focusedId, "tab2button", "button accesskey in tab2");
+
+ // Press the accesskey for the chrome element while the content document is focused.
+ focusedId = await performAccessKeyForChrome("z");
+ is(focusedId, "chromebutton", "chromebutton accesskey");
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+
+ // Test whether access key for the newButton isn't available when content
+ // consumes the key event.
+
+ // When content in the tab3 consumes all keydown events.
+ const gPageURL3 =
+ "data:text/html,<body id='tab3body'>" +
+ "<button id='tab3button' accesskey='y'>Button in Tab 3</button>" +
+ "<script>" +
+ "document.body.addEventListener('keydown', (event)=>{ event.preventDefault(); });" +
+ "</script></body>";
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL3);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = await performAccessKey(tab3.linkedBrowser, "y");
+ is(focusedId, "tab3button", "button accesskey in tab3 should be focused");
+
+ newButton.onfocus = () => {
+ ok(false, "chromebutton shouldn't get focus during testing with tab3");
+ };
+
+ // Press the accesskey for the chrome element while the content document is focused.
+ focusedId = await performAccessKey(tab3.linkedBrowser, "z");
+ is(
+ focusedId,
+ "tab3body",
+ "button accesskey in tab3 should keep having focus"
+ );
+
+ newButton.onfocus = null;
+
+ gBrowser.removeTab(tab3);
+
+ // When content in the tab4 consumes all keypress events.
+ const gPageURL4 =
+ "data:text/html,<body id='tab4body'>" +
+ "<button id='tab4button' accesskey='y'>Button in Tab 4</button>" +
+ "<script>" +
+ "document.body.addEventListener('keypress', (event)=>{ event.preventDefault(); });" +
+ "</script></body>";
+ let tab4 = await BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL4);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = await performAccessKey(tab4.linkedBrowser, "y");
+ is(focusedId, "tab4button", "button accesskey in tab4 should be focused");
+
+ newButton.onfocus = () => {
+ // EventStateManager handles accesskey before dispatching keypress event
+ // into the DOM tree, therefore, chrome accesskey always wins focus from
+ // content. However, this is different from shortcut keys.
+ todo(false, "chromebutton shouldn't get focus during testing with tab4");
+ };
+
+ // Press the accesskey for the chrome element while the content document is focused.
+ focusedId = await performAccessKey(tab4.linkedBrowser, "z");
+ is(
+ focusedId,
+ "tab4body",
+ "button accesskey in tab4 should keep having focus"
+ );
+
+ newButton.onfocus = null;
+
+ gBrowser.removeTab(tab4);
+
+ newButton.remove();
+});
+
+function performAccessKey(browser, key) {
+ return new Promise(resolve => {
+ let removeFocus, removeKeyDown, removeKeyUp;
+ function callback(eventName, result) {
+ removeFocus();
+ removeKeyUp();
+ removeKeyDown();
+
+ SpecialPowers.spawn(browser, [], () => {
+ let oldFocusedElement = content._oldFocusedElement;
+ delete content._oldFocusedElement;
+ return oldFocusedElement.id;
+ }).then(oldFocus => resolve(oldFocus));
+ }
+
+ removeFocus = BrowserTestUtils.addContentEventListener(
+ browser,
+ "focus",
+ callback,
+ { capture: true },
+ event => {
+ if (!(event.target instanceof HTMLElement)) {
+ 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..f871dc4a16
--- /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._identityBox.click();
+ await promisePanelOpen;
+
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "ViewShown"
+ );
+ document.getElementById("identity-popup-security-expander").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("background-image");
+ let securityContentBG = gBrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-mainView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("background-image");
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/connection-mixed-passive-loaded.svg")',
+ "Using expected icon image in the identity block"
+ );
+ is(
+ securityViewBG,
+ 'url("chrome://global/skin/icons/connection-mixed-passive-loaded.svg")',
+ "Using expected icon image in the Control Center main view"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/connection-mixed-passive-loaded.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_addKeywordSearch.js b/browser/base/content/test/general/browser_addKeywordSearch.js
new file mode 100644
index 0000000000..3e0b7b2035
--- /dev/null
+++ b/browser/base/content/test/general/browser_addKeywordSearch.js
@@ -0,0 +1,89 @@
+var testData = [
+ { desc: "No path", action: "http://example.com/", param: "q" },
+ {
+ desc: "With path",
+ action: "http://example.com/new-path-here/",
+ param: "q",
+ },
+ { desc: "No action", action: "", param: "q" },
+ {
+ desc: "With Query String",
+ action: "http://example.com/search?oe=utf-8",
+ param: "q",
+ },
+];
+
+add_task(async function() {
+ const TEST_URL =
+ "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let count = 0;
+ for (let method of ["GET", "POST"]) {
+ for (let { desc, action, param } of testData) {
+ info(`Running ${method} keyword test '${desc}'`);
+ let id = `keyword-form-${count++}`;
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let contextMenuPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ action, param, method, id }],
+ async function(args) {
+ let doc = content.document;
+ let form = doc.createElement("form");
+ form.id = args.id;
+ form.method = args.method;
+ form.action = args.action;
+ let element = doc.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("name", args.param);
+ form.appendChild(element);
+ doc.body.appendChild(form);
+ }
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ `#${id} > input`,
+ { type: "contextmenu", button: 2 },
+ tab.linkedBrowser
+ );
+ await contextMenuPromise;
+ let url = action || tab.linkedBrowser.currentURI.spec;
+ let actor = gContextMenu.actor;
+
+ let data = await actor.getSearchFieldBookmarkData(
+ gContextMenu.targetIdentifier
+ );
+ if (method == "GET") {
+ ok(
+ data.spec.endsWith(`${param}=%s`),
+ `Check expected url for field named ${param} and action ${action}`
+ );
+ } else {
+ is(
+ data.spec,
+ url,
+ `Check expected url for field named ${param} and action ${action}`
+ );
+ is(
+ data.postData,
+ `${param}%3D%25s`,
+ `Check expected POST data for field named ${param} and action ${action}`
+ );
+ }
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+ }
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
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..bf2a1c5e60
--- /dev/null
+++ b/browser/base/content/test/general/browser_alltabslistener.js
@@ -0,0 +1,326 @@
+const gCompleteState =
+ Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+
+function getOriginalURL(request) {
+ return request && request.QueryInterface(Ci.nsIChannel).originalURI.spec;
+}
+
+var gFrontProgressListener = {
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {},
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ return;
+ }
+ var state = "onStateChange";
+ info(
+ "FrontProgress (" + url + "): " + state + " 0x" + aStateFlags.toString(16)
+ );
+ assertCorrectBrowserAndEventOrderForFront(state);
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ return;
+ }
+ var state = "onLocationChange";
+ info("FrontProgress: " + state + " " + aLocationURI.spec);
+ assertCorrectBrowserAndEventOrderForFront(state);
+ },
+
+ onSecurityChange(aWebProgress, aRequest, aState) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ return;
+ }
+ var state = "onSecurityChange";
+ info("FrontProgress (" + url + "): " + state + " 0x" + aState.toString(16));
+ assertCorrectBrowserAndEventOrderForFront(state);
+ },
+};
+
+function assertCorrectBrowserAndEventOrderForFront(aEventName) {
+ Assert.less(
+ gFrontNotificationsPos,
+ gFrontNotifications.length,
+ "Got an expected notification for the front notifications listener"
+ );
+ is(
+ aEventName,
+ gFrontNotifications[gFrontNotificationsPos],
+ "Got a notification for the front notifications listener"
+ );
+ gFrontNotificationsPos++;
+}
+
+var gAllProgressListener = {
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ // ignore initial about blank
+ return;
+ }
+ var state = "onStateChange";
+ info(
+ "AllProgress (" + url + "): " + state + " 0x" + aStateFlags.toString(16)
+ );
+ assertCorrectBrowserAndEventOrderForAll(state, aBrowser);
+ assertReceivedFlags(
+ state,
+ gAllNotifications[gAllNotificationsPos],
+ aStateFlags
+ );
+ gAllNotificationsPos++;
+
+ if ((aStateFlags & gCompleteState) == gCompleteState) {
+ is(
+ gAllNotificationsPos,
+ gAllNotifications.length,
+ "Saw the expected number of notifications"
+ );
+ is(
+ gFrontNotificationsPos,
+ gFrontNotifications.length,
+ "Saw the expected number of frontnotifications"
+ );
+ executeSoon(gNextTest);
+ }
+ },
+
+ onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ // ignore initial about blank
+ return;
+ }
+ var state = "onLocationChange";
+ info("AllProgress: " + state + " " + aLocationURI.spec);
+ assertCorrectBrowserAndEventOrderForAll(state, aBrowser);
+ assertReceivedFlags(
+ "onLocationChange",
+ gAllNotifications[gAllNotificationsPos],
+ aFlags
+ );
+ gAllNotificationsPos++;
+ },
+
+ onSecurityChange(aBrowser, aWebProgress, aRequest, aState) {
+ var url = getOriginalURL(aRequest);
+ if (url == "about:blank") {
+ // ignore initial about blank
+ return;
+ }
+ var state = "onSecurityChange";
+ info("AllProgress (" + url + "): " + state + " 0x" + aState.toString(16));
+ assertCorrectBrowserAndEventOrderForAll(state, aBrowser);
+ is(
+ state,
+ gAllNotifications[gAllNotificationsPos],
+ "Got a notification for the all notifications listener"
+ );
+ gAllNotificationsPos++;
+ },
+};
+
+function assertCorrectBrowserAndEventOrderForAll(aState, aBrowser) {
+ ok(
+ aBrowser == gTestBrowser,
+ aState + " notification came from the correct browser"
+ );
+ Assert.less(
+ gAllNotificationsPos,
+ gAllNotifications.length,
+ "Got an expected notification for the all notifications listener"
+ );
+}
+
+function assertReceivedFlags(aState, aObjOrEvent, aFlags) {
+ if (aObjOrEvent !== null && typeof aObjOrEvent === "object") {
+ is(
+ aState,
+ aObjOrEvent.state,
+ "Got a notification for the all notifications listener"
+ );
+ is(aFlags, aFlags & aObjOrEvent.flags, `Got correct flags for ${aState}`);
+ } else {
+ is(
+ aState,
+ aObjOrEvent,
+ "Got a notification for the all notifications listener"
+ );
+ }
+}
+
+var gFrontNotifications,
+ gAllNotifications,
+ gFrontNotificationsPos,
+ gAllNotificationsPos;
+var gBackgroundTab,
+ gForegroundTab,
+ gBackgroundBrowser,
+ gForegroundBrowser,
+ gTestBrowser;
+var gTestPage =
+ "/browser/browser/base/content/test/general/alltabslistener.html";
+const kBasePage =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/dummy_page.html";
+var gNextTest;
+
+async function test() {
+ waitForExplicitFinish();
+
+ gBackgroundTab = BrowserTestUtils.addTab(gBrowser);
+ gForegroundTab = BrowserTestUtils.addTab(gBrowser);
+ gBackgroundBrowser = gBrowser.getBrowserForTab(gBackgroundTab);
+ gForegroundBrowser = gBrowser.getBrowserForTab(gForegroundTab);
+ gBrowser.selectedTab = gForegroundTab;
+
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange",
+ ];
+
+ // We must wait until a page has completed loading before
+ // starting tests or we get notifications from that
+ let promises = [
+ BrowserTestUtils.browserStopped(gBackgroundBrowser, kBasePage),
+ BrowserTestUtils.browserStopped(gForegroundBrowser, kBasePage),
+ ];
+ BrowserTestUtils.loadURI(gBackgroundBrowser, kBasePage);
+ BrowserTestUtils.loadURI(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.loadURI(browser, url);
+}
+
+function startTest1() {
+ info("\nTest 1");
+ gBrowser.addProgressListener(gFrontProgressListener);
+ gBrowser.addTabsProgressListener(gAllProgressListener);
+
+ gFrontNotifications = gAllNotifications;
+ 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 = [];
+ 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;
+ runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest6);
+}
+
+function startTest6() {
+ info("\nTest 6");
+ gFrontNotifications = [];
+ 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",
+ ];
+ 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..996a155a79
--- /dev/null
+++ b/browser/base/content/test/general/browser_backButtonFitts.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function() {
+ let firstLocation =
+ "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..c96baab4b5
--- /dev/null
+++ b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
@@ -0,0 +1,88 @@
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
+
+var expectingDialog = false;
+var wantToClose = true;
+var resolveDialogPromise;
+function onTabModalDialogLoaded(node) {
+ 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();
+ }
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+});
+
+SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+});
+
+// Listen for the dialog being created
+Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.tabs.warnOnClose");
+ Services.obs.removeObserver(onTabModalDialogLoaded, "tabmodal-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),
+ "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..b81bd60602
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1297539.js
@@ -0,0 +1,122 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for Bug 1297539
+ * Test that the content event "pasteTransferable"
+ * (mozilla::EventMessage::eContentCommandPasteTransferable)
+ * is handled correctly for plain text and html in the remote case.
+ *
+ * Original test test_bug525389.html for command content event
+ * "pasteTransferable" runs only in the content process.
+ * This doesn't test the remote case.
+ *
+ */
+
+"use strict";
+
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function getTransferableFromClipboard(asHTML) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ if (asHTML) {
+ trans.addDataFlavor("text/html");
+ } else {
+ trans.addDataFlavor("text/unicode");
+ }
+ Services.clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
+ return trans;
+}
+
+async function cutCurrentSelection(elementQueryString, property, browser) {
+ // Cut the current selection.
+ await BrowserTestUtils.synthesizeKey("x", { accelKey: true }, browser);
+
+ // The editor should be empty after cut.
+ await SpecialPowers.spawn(
+ browser,
+ [[elementQueryString, property]],
+ async function([contentElementQueryString, contentProperty]) {
+ let element = content.document.querySelector(contentElementQueryString);
+ is(
+ element[contentProperty],
+ "",
+ `${contentElementQueryString} should be empty after cut (superkey + x)`
+ );
+ }
+ );
+}
+
+// Test that you are able to pasteTransferable for plain text
+// which is handled by TextEditor::PasteTransferable to paste into the editor.
+add_task(async function test_paste_transferable_plain_text() {
+ let testPage =
+ "data:text/html," +
+ '<textarea id="textarea">Write something here</textarea>';
+
+ await BrowserTestUtils.withNewTab(testPage, async function(browser) {
+ // Select all the content in your editor element.
+ await BrowserTestUtils.synthesizeMouse("#textarea", 0, 0, {}, browser);
+ await BrowserTestUtils.synthesizeKey("a", { accelKey: true }, browser);
+
+ await cutCurrentSelection("#textarea", "value", browser);
+
+ let trans = getTransferableFromClipboard(false);
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ DOMWindowUtils.sendContentCommandEvent("pasteTransferable", trans);
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ let textArea = content.document.querySelector("#textarea");
+ is(
+ textArea.value,
+ "Write something here",
+ "Send content command pasteTransferable successful"
+ );
+ });
+ });
+});
+
+// Test that you are able to pasteTransferable for html
+// which is handled by HTMLEditor::PasteTransferable to paste into the editor.
+//
+// On Linux,
+// BrowserTestUtils.synthesizeKey("a", {accelKey: true}, browser);
+// doesn't seem to trigger for contenteditable which is why we use
+// Selection to select the contenteditable contents.
+add_task(async function test_paste_transferable_html() {
+ let testPage =
+ "data:text/html," +
+ '<div contenteditable="true"><b>Bold Text</b><i>italics</i></div>';
+
+ await BrowserTestUtils.withNewTab(testPage, async function(browser) {
+ // Select all the content in your editor element.
+ await BrowserTestUtils.synthesizeMouse("div", 0, 0, {}, browser);
+ await SpecialPowers.spawn(browser, [], async function() {
+ let element = content.document.querySelector("div");
+ let selection = content.window.getSelection();
+ selection.selectAllChildren(element);
+ });
+
+ await cutCurrentSelection("div", "textContent", browser);
+
+ let trans = getTransferableFromClipboard(true);
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ DOMWindowUtils.sendContentCommandEvent("pasteTransferable", trans);
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ let textArea = content.document.querySelector("div");
+ is(
+ textArea.innerHTML,
+ "<b>Bold Text</b><i>italics</i>",
+ "Send content command pasteTransferable successful"
+ );
+ });
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug1299667.js b/browser/base/content/test/general/browser_bug1299667.js
new file mode 100644
index 0000000000..f33ba7a33c
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1299667.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+ 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;
+ 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;
+ 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..5fa19e9840
--- /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..d02352e6e7
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug356571.js
@@ -0,0 +1,102 @@
+// 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/embedcomp/prompt-service;1";
+
+// Save original prompt service factory
+const kPromptServiceFactory = Cm.getClassObject(
+ Cc[kPromptServiceContractID],
+ Ci.nsIFactory
+);
+
+var fakePromptServiceFactory = {
+ createInstance(aOuter, aIid) {
+ if (aOuter != null) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ 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 =
+ "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+const kURIs = ["bad://www.mozilla.org/", kDummyPage, kDummyPage];
+
+var gProgressListener = {
+ _runCount: 0,
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if ((aStateFlags & kCompleteState) == kCompleteState) {
+ if (++this._runCount != kURIs.length) {
+ return;
+ }
+ // Check we failed on unknown protocol (received an alert from docShell)
+ ok(didFail, "Correctly failed on unknown protocol");
+ // Check we opened all tabs
+ ok(
+ gBrowser.tabs.length == kURIs.length,
+ "Correctly opened all expected tabs"
+ );
+ finishTest();
+ }
+ },
+};
+
+function test() {
+ todo(false, "temp. disabled");
+ /* FIXME */
+ /*
+ waitForExplicitFinish();
+ // Wait for all tabs to finish loading
+ gBrowser.addTabsProgressListener(gProgressListener);
+ loadOneOrMoreURIs(kURIs.join("|"));
+ */
+}
+
+function finishTest() {
+ // Unregister the factory so we do not leak
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory(
+ Components.ID(kPromptServiceUUID),
+ fakePromptServiceFactory
+ );
+
+ // Restore the original factory
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory(
+ Components.ID(kPromptServiceUUID),
+ "Prompt Service",
+ kPromptServiceContractID,
+ kPromptServiceFactory
+ );
+
+ // Remove the listener
+ gBrowser.removeTabsProgressListener(gProgressListener);
+
+ // Close opened tabs
+ for (var i = gBrowser.tabs.length - 1; i > 0; i--) {
+ gBrowser.removeTab(gBrowser.tabs[i]);
+ }
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug380960.js b/browser/base/content/test/general/browser_bug380960.js
new file mode 100644
index 0000000000..5571d8f08e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug380960.js
@@ -0,0 +1,18 @@
+function test() {
+ var tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ gBrowser.removeTab(tab);
+ is(tab.parentNode, null, "tab removed immediately");
+
+ tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ gBrowser.removeTab(tab, { animate: true });
+ gBrowser.removeTab(tab);
+ is(
+ tab.parentNode,
+ null,
+ "tab removed immediately when calling removeTab again after the animation was kicked off"
+ );
+}
diff --git a/browser/base/content/test/general/browser_bug406216.js b/browser/base/content/test/general/browser_bug406216.js
new file mode 100644
index 0000000000..1fc013f131
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug406216.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * "TabClose" event is possibly used for closing related tabs of the current.
+ * "removeTab" method should work correctly even if the number of tabs are
+ * changed while "TabClose" event.
+ */
+
+var count = 0;
+const URIS = [
+ "about:config",
+ "about:plugins",
+ "about:buildconfig",
+ "data:text/html,<title>OK</title>",
+];
+
+function test() {
+ waitForExplicitFinish();
+ URIS.forEach(addTab);
+}
+
+function addTab(aURI, aIndex) {
+ var tab = BrowserTestUtils.addTab(gBrowser, aURI);
+ if (aIndex == 0) {
+ gBrowser.removeTab(gBrowser.tabs[0], { skipPermitUnload: true });
+ }
+
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ if (++count == URIS.length) {
+ executeSoon(doTabsTest);
+ }
+ });
+}
+
+function doTabsTest() {
+ is(gBrowser.tabs.length, URIS.length, "Correctly opened all expected tabs");
+
+ // sample of "close related tabs" feature
+ gBrowser.tabContainer.addEventListener(
+ "TabClose",
+ function(event) {
+ var closedTab = event.originalTarget;
+ var scheme = closedTab.linkedBrowser.currentURI.scheme;
+ Array.from(gBrowser.tabs).forEach(function(aTab) {
+ if (
+ aTab != closedTab &&
+ aTab.linkedBrowser.currentURI.scheme == scheme
+ ) {
+ gBrowser.removeTab(aTab, { skipPermitUnload: true });
+ }
+ });
+ },
+ { capture: true, once: true }
+ );
+
+ gBrowser.removeTab(gBrowser.tabs[0], { skipPermitUnload: true });
+ is(gBrowser.tabs.length, 1, "Related tabs are not closed unexpectedly");
+
+ BrowserTestUtils.addTab(gBrowser, "about:blank");
+ gBrowser.removeTab(gBrowser.tabs[0], { skipPermitUnload: true });
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug417483.js b/browser/base/content/test/general/browser_bug417483.js
new file mode 100644
index 0000000000..0fe32d556c
--- /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.loadURI(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_bug423833.js b/browser/base/content/test/general/browser_bug423833.js
new file mode 100644
index 0000000000..397473ed13
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug423833.js
@@ -0,0 +1,168 @@
+/* Tests for proper behaviour of "Show this frame" context menu options */
+
+// Two frames, one with text content, the other an error page
+var invalidPage = "http://127.0.0.1:55555/";
+var validPage = "http://example.com/";
+var testPage =
+ 'data:text/html,<frameset cols="400,400"><frame src="' +
+ validPage +
+ '"><frame src="' +
+ invalidPage +
+ '"></frameset>';
+
+// Store the tab and window created in tests 2 and 3 respectively
+var test2tab;
+var test3window;
+
+// We use setInterval instead of setTimeout to avoid race conditions on error doc loads
+var intervalID;
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedBrowser.addEventListener("load", test1Setup, true);
+ content.location = testPage;
+}
+
+function test1Setup() {
+ if (content.frames.length < 2 || content.frames[1].location != invalidPage) {
+ // The error frame hasn't loaded yet
+ return;
+ }
+
+ gBrowser.selectedBrowser.removeEventListener("load", test1Setup, true);
+
+ var badFrame = content.frames[1];
+ document.popupNode = badFrame.document.firstElementChild;
+
+ var contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ var contextMenu = new nsContextMenu(contentAreaContextMenu);
+
+ // We'd like to use another load listener here, but error pages don't fire load events
+ contextMenu.showOnlyThisFrame();
+ intervalID = setInterval(testShowOnlyThisFrame, 3000);
+}
+
+function testShowOnlyThisFrame() {
+ if (content.location.href == testPage) {
+ // This is a stale event from the original page loading
+ return;
+ }
+
+ // We should now have loaded the error page frame content directly
+ // in the tab, make sure the URL is right.
+ clearInterval(intervalID);
+
+ is(
+ content.location.href,
+ invalidPage,
+ "Should navigate to page url, not about:neterror"
+ );
+
+ // Go back to the frames page
+ gBrowser.addEventListener("load", test2Setup, true);
+ content.location = testPage;
+}
+
+function test2Setup() {
+ if (content.frames.length < 2 || content.frames[1].location != invalidPage) {
+ // The error frame hasn't loaded yet
+ return;
+ }
+
+ gBrowser.removeEventListener("load", test2Setup, true);
+
+ // Now let's do the whole thing again, but this time for "Open frame in new tab"
+ var badFrame = content.frames[1];
+
+ document.popupNode = badFrame.document.firstElementChild;
+
+ var contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ var contextMenu = new nsContextMenu(contentAreaContextMenu);
+
+ gBrowser.tabContainer.addEventListener("TabOpen", function listener(event) {
+ test2tab = event.target;
+ gBrowser.tabContainer.removeEventListener("TabOpen", listener);
+ });
+ contextMenu.openFrameInTab();
+ ok(test2tab, "openFrameInTab() opened a tab");
+
+ gBrowser.selectedTab = test2tab;
+
+ intervalID = setInterval(testOpenFrameInTab, 3000);
+}
+
+function testOpenFrameInTab() {
+ if (gBrowser.contentDocument.location.href == "about:blank") {
+ // Wait another cycle
+ return;
+ }
+
+ clearInterval(intervalID);
+
+ // We should now have the error page in a new, active tab.
+ is(
+ gBrowser.contentDocument.location.href,
+ invalidPage,
+ "New tab should have page url, not about:neterror"
+ );
+
+ // Clear up the new tab, and punt to test 3
+ gBrowser.removeCurrentTab();
+
+ test3Setup();
+}
+
+function test3Setup() {
+ // One more time, for "Open frame in new window"
+ var badFrame = content.frames[1];
+ document.popupNode = badFrame.document.firstElementChild;
+
+ var contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ var contextMenu = new nsContextMenu(contentAreaContextMenu);
+
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ if (aTopic == "domwindowopened") {
+ test3window = aSubject;
+ }
+ Services.ww.unregisterNotification(notification);
+ });
+
+ contextMenu.openFrame();
+
+ intervalID = setInterval(testOpenFrame, 3000);
+}
+
+function testOpenFrame() {
+ if (!test3window || test3window.content.location.href == "about:blank") {
+ info("testOpenFrame: Wait another cycle");
+ return;
+ }
+
+ clearInterval(intervalID);
+
+ is(
+ test3window.content.location.href,
+ invalidPage,
+ "New window should have page url, not about:neterror"
+ );
+
+ test3window.close();
+ cleanup();
+}
+
+function cleanup() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
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..df76720382
--- /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..6be32351c2
--- /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..b64e511aec
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug431826.js
@@ -0,0 +1,56 @@
+function remote(task) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], task);
+}
+
+add_task(async function() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(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..512b184a67
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug432599.js
@@ -0,0 +1,104 @@
+/* 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;
+function checkBookmarksPanel(phase) {
+ StarUI._createPanelIfNeeded();
+ let popupElement = document.getElementById("editBookmarkPanel");
+ let titleElement = document.getElementById("editBookmarkPanelTitle");
+ let removeElement = document.getElementById("editBookmarkPanelRemoveButton");
+ switch (phase) {
+ case 1:
+ case 3:
+ return promisePopupShown(popupElement);
+ case 2:
+ initialValue = titleElement.value;
+ initialRemoveHidden = removeElement.hidden;
+ return promisePopupHidden(popupElement);
+ case 4:
+ Assert.equal(
+ titleElement.value,
+ initialValue,
+ "The bookmark panel's title should be the same"
+ );
+ Assert.equal(
+ removeElement.hidden,
+ initialRemoveHidden,
+ "The bookmark panel's visibility should not change"
+ );
+ return promisePopupHidden(popupElement);
+ }
+ return Promise.reject(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..050225d89f
--- /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..8c2c70e1ae
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug462289.js
@@ -0,0 +1,118 @@
+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() {
+ 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);
+}
+
+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"
+ );
+
+ ok(true, "focusing URLBar then sending 2 Shift+Tab.");
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ info(`Focus is now on Home button (#${document.activeElement.id})`);
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+
+ 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..4e97608064
--- /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..d89903579c
--- /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..be9f1439a6
--- /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..adea10877a
--- /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..34c8c4b803
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug495058.js
@@ -0,0 +1,48 @@
+/**
+ * Tests that the right elements of a tab are focused when it is
+ * torn out into its own window.
+ */
+
+const URIS = ["about:blank", "about:sessionrestore", "about:privatebrowsing"];
+
+add_task(async function() {
+ for (let uri of URIS) {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.loadURI(tab.linkedBrowser, uri);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ let win = gBrowser.replaceTabWithWindow(tab);
+
+ let contentPainted = Promise.resolve();
+ // 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 (tab.linkedBrowser.isRemoteBrowser) {
+ contentPainted = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "MozAfterPaint"
+ );
+ }
+
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == win
+ );
+ await contentPainted;
+ tab = win.gBrowser.selectedTab;
+
+ Assert.equal(
+ win.gBrowser.currentURI.spec,
+ uri,
+ uri + ": uri loaded in detached tab"
+ );
+ Assert.equal(
+ win.document.activeElement,
+ win.gBrowser.selectedBrowser,
+ uri + ": browser is focused"
+ );
+ 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..547ee3b563
--- /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.loadURI(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..7cec308763
--- /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..e3e1019bf6
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug537013.js
@@ -0,0 +1,166 @@
+/* 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.supportsFindClipboard();
+
+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..e2456ff600
--- /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..ded4382376
--- /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..dc5588bc3e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug567306.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var HasFindClipboard = Services.clipboard.supportsFindClipboard();
+
+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("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..ec4b7a109e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug575561.js
@@ -0,0 +1,117 @@
+requestLongerTimeout(2);
+
+const TEST_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..7fd0e4c8b8
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug578534.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/. */
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+add_task(async function test() {
+ 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..3fa0563fa4
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug579872.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 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");
+
+ openTrustedLinkIn("http://example.com/1", "current");
+ is(gBrowser.tabs.length, 2, "Should open in current tab");
+
+ 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..677c322e15
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug581253.js
@@ -0,0 +1,75 @@
+/* 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.loadURI(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),
+ "places"
+ );
+
+ 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..094f132086
--- /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..391234aedc
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug594131.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 newTab = BrowserTestUtils.addTab(gBrowser, "http://example.com");
+ waitForExplicitFinish();
+ BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(mainPart);
+
+ function mainPart() {
+ gBrowser.pinTab(newTab);
+ gBrowser.selectedTab = newTab;
+
+ 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..493cb222cf
--- /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.loadOneTab("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..923e816f81
--- /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..1a45c5839d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug623893.js
@@ -0,0 +1,44 @@
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ "data:text/plain;charset=utf-8,1",
+ async function(browser) {
+ BrowserTestUtils.loadURI(browser, "data:text/plain;charset=utf-8,2");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.loadURI(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..a50bc6ac84
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug624734.js
@@ -0,0 +1,47 @@
+/* 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-add",
+ "main-context-menu-bookmark-add-with-shortcut",
+ ].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.loadURI(
+ tab.linkedBrowser,
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html"
+ );
+}
diff --git a/browser/base/content/test/general/browser_bug647886.js b/browser/base/content/test/general/browser_bug647886.js
new file mode 100644
index 0000000000..550bb8f638
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug647886.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
+ content.history.pushState({}, "2", "2.html");
+ });
+
+ await new Promise(resolve =>
+ SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
+ );
+
+ let backButton = document.getElementById("back-button");
+ let rect = backButton.getBoundingClientRect();
+
+ info("waiting for the history menu to open");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ backButton,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(backButton, { type: "mousedown" });
+ EventUtils.synthesizeMouse(backButton, rect.width / 2, rect.height, {
+ type: "mouseup",
+ });
+ 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;
+ 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;
+ 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");
+
+ event.target.hidePopup();
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
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..32dd6c242a
--- /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..ce39b660f4
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug676619.js
@@ -0,0 +1,122 @@
+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);
+ });
+}
+
+async function testLink(link, name) {
+ info("Checking " + link + " with name: " + name);
+
+ 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);
+}
+
+// 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");
+ 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);
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function() {
+ requestLongerTimeout(3);
+ waitForExplicitFinish();
+
+ 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"
+ );
+});
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..1ce98fc3cf
--- /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..3fd397f831
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug724239.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabStateFlusher } = ChromeUtils.import(
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+
+add_task(async function test_blank() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function(browser) {
+ BrowserTestUtils.loadURI(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.loadURI(browser, "about:newtab");
+ await stopped;
+
+ stopped = BrowserTestUtils.browserStopped(browser, "http://example.com/");
+ BrowserTestUtils.loadURI(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..184aebab45
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug734076.js
@@ -0,0 +1,186 @@
+/* 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);
+
+ let browser = tab.linkedBrowser;
+ browser.stop(); // stop the about:blank load
+
+ let writeDomainURL = encodeURI(
+ "data:text/html,<script>document.write(document.domain);</script>"
+ );
+
+ let tests = [
+ {
+ name: "view background image",
+ url: "http://mochi.test:8888/",
+ element: "body",
+ go() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ writeDomainURL }],
+ async function(arg) {
+ let contentBody = content.document.body;
+ contentBody.style.backgroundImage =
+ "url('" + arg.writeDomainURL + "')";
+
+ return "context-viewbgimage";
+ }
+ );
+ },
+ verify() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function(
+ arg
+ ) {
+ Assert.ok(
+ !content.document.body.textContent,
+ "no domain was inherited for view background image"
+ );
+ });
+ },
+ },
+ {
+ name: "view image",
+ url: "http://mochi.test:8888/",
+ element: "img",
+ 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() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function(
+ arg
+ ) {
+ Assert.ok(
+ !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() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function(
+ arg
+ ) {
+ Assert.ok(
+ !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.loadURI(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 = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ document.getElementById(commandToRun).click();
+ await loadedAfterCommandPromise;
+
+ await test.verify();
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ contentAreaContextMenu.hidePopup();
+ await popupHiddenPromise;
+ }
+
+ 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..39d63a6c1b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug749738.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/. */
+
+"use strict";
+
+const DUMMY_PAGE =
+ "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..2aaec9a7b6
--- /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..80e4fec933
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { AboutNewTab } = ChromeUtils.import(
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+async function doTest(isPrivate) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: isPrivate });
+ let defaultURL = AboutNewTab.newTabURL;
+ let newTabURL;
+ let mode;
+ let testURL = "http://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..c022553b41
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug817947.js
@@ -0,0 +1,59 @@
+/* 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";
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(PREF);
+ });
+
+ preparePendingTab(function(aTab) {
+ let win = gBrowser.replaceTabWithWindow(aTab);
+
+ whenDelayedStartupFinished(win, function() {
+ let [tab] = win.gBrowser.tabs;
+
+ whenLoaded(tab.linkedBrowser, function() {
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ URL,
+ "correct url should be loaded"
+ );
+ ok(!tab.hasAttribute("pending"), "tab should not be pending");
+
+ win.close();
+ finish();
+ });
+ });
+ });
+}
+
+function preparePendingTab(aCallback) {
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+
+ whenLoaded(tab.linkedBrowser, function() {
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ sessionUpdatePromise.then(() => {
+ let [{ state }] = JSON.parse(SessionStore.getClosedTabData(window));
+
+ tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ whenLoaded(tab.linkedBrowser, function() {
+ SessionStore.setTabState(tab, JSON.stringify(state));
+ ok(tab.hasAttribute("pending"), "tab should be pending");
+ aCallback(tab);
+ });
+ });
+ });
+}
+
+function whenLoaded(aBrowser, aCallback) {
+ BrowserTestUtils.browserLoaded(aBrowser).then(() => {
+ executeSoon(aCallback);
+ });
+}
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..7ab3ab926d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug832435.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+ ok(true, "Starting up");
+
+ gBrowser.selectedBrowser.focus();
+ gURLBar.addEventListener(
+ "focus",
+ function() {
+ ok(true, "Invoked onfocus handler");
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
+
+ // javscript: URIs are evaluated async.
+ SimpleTest.executeSoon(function() {
+ ok(true, "Evaluated without crashing");
+ finish();
+ });
+ },
+ { once: true }
+ );
+ gURLBar.inputField.value = "javascript: var foo = '11111111'; ";
+ gURLBar.focus();
+}
diff --git a/browser/base/content/test/general/browser_bug882977.js b/browser/base/content/test/general/browser_bug882977.js
new file mode 100644
index 0000000000..abfbdc826a
--- /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..488551a2c3
--- /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..73a62938b2
--- /dev/null
+++ b/browser/base/content/test/general/browser_clipboard_pastefile.js
@@ -0,0 +1,80 @@
+// This test is used to check that pasting files removes all non-file data from
+// event.clipboardData.
+
+add_task(async function() {
+ var input = document.createElement("input");
+ document.documentElement.appendChild(input);
+
+ input.focus();
+ input.value = "Text";
+ input.select();
+
+ await new Promise((resolve, reject) => {
+ input.addEventListener(
+ "copy",
+ function(event) {
+ event.clipboardData.setData("text/plain", "Alternate");
+ // For this test, it doesn't matter that the file isn't actually a file.
+ event.clipboardData.setData("application/x-moz-file", "Sample");
+ event.preventDefault();
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+
+ EventUtils.synthesizeKey("c", { accelKey: true });
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/browser/browser/base/content/test/general/clipboard_pastefile.html"
+ );
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async function(arg) {
+ content.document.getElementById("input").focus();
+ });
+
+ await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
+
+ let output = await SpecialPowers.spawn(browser, [], async function(arg) {
+ return content.document.getElementById("output").textContent;
+ });
+ is(output, "Passed", "Paste file");
+
+ 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.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"
+ );
+
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+
+ EventUtils.synthesizeKey("v", { accelKey: true });
+ });
+
+ input.remove();
+
+ BrowserTestUtils.removeTab(tab);
+});
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..f3e6a4aefa
--- /dev/null
+++ b/browser/base/content/test/general/browser_contentAltClick.js
@@ -0,0 +1,206 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.defineModuleGetter(
+ this,
+ "Downloads",
+ "resource://gre/modules/Downloads.jsm"
+);
+
+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,
+ "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..89e6c05403
--- /dev/null
+++ b/browser/base/content/test/general/browser_contentAreaClick.js
@@ -0,0 +1,327 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.import("resource://testing-common/Sinon.jsm");
+
+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..b31b971315
--- /dev/null
+++ b/browser/base/content/test/general/browser_ctrlTab.js
@@ -0,0 +1,282 @@
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.ctrlTab.recentlyUsedOrder", 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"
+ );
+ }
+
+ {
+ // 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();
+ }
+
+ {
+ // 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);
+
+ {
+ // 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);
+ }
+
+ {
+ // 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);
+
+ {
+ // 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"
+ );
+ }
+
+ /* private utility functions */
+
+ function pressCtrlTab(aShiftKey) {
+ let promise;
+ if (!isOpen() && canOpen()) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_TAB", {
+ ctrlKey: true,
+ shiftKey: !!aShiftKey,
+ });
+ return promise;
+ }
+
+ function releaseCtrl() {
+ let promise;
+ if (isOpen()) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popuphidden");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+ return promise;
+ }
+
+ function synthesizeCtrlW() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose"
+ );
+ EventUtils.synthesizeKey("w", { ctrlKey: true });
+ return promise;
+ }
+
+ function isOpen() {
+ return ctrlTab.isOpen;
+ }
+
+ function canOpen() {
+ return (
+ Services.prefs.getBoolPref("browser.ctrlTab.recentlyUsedOrder") &&
+ 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.tabs.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, [], () => {
+ content.window.addEventListener("keyup", () => {
+ content.window._ctrlTabTestKeyupHappend = true;
+ });
+ })
+ )
+ );
+
+ for (let i = 0; i < tabTimes; i++) {
+ await pressCtrlTab();
+
+ if (tabCount > 2) {
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ indexStart,
+ "Selected tab doesn't change while tabbing"
+ );
+ }
+ }
+
+ if (tabCount > 2) {
+ ok(
+ isOpen(),
+ "With " + tabCount + " tabs open, Ctrl+Tab opens the preview panel"
+ );
+
+ await releaseCtrl();
+
+ ok(!isOpen(), "Releasing Ctrl closes the preview panel");
+ } else {
+ ok(
+ !isOpen(),
+ "With " +
+ tabCount +
+ " tabs open, Ctrl+Tab doesn't open the preview panel"
+ );
+ }
+
+ is(
+ gBrowser.tabContainer.selectedIndex,
+ expectedIndex,
+ "With " +
+ tabCount +
+ " tabs open and tab " +
+ indexStart +
+ " selected, Ctrl+Tab*" +
+ tabTimes +
+ " 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..8a765ad88c
--- /dev/null
+++ b/browser/base/content/test/general/browser_datachoices_notification.js
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Pass an empty scope object to the import to prevent "leaked window property"
+// errors in tests.
+var Preferences = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm",
+ {}
+).Preferences;
+var TelemetryReportingPolicy = ChromeUtils.import(
+ "resource://gre/modules/TelemetryReportingPolicy.jsm",
+ {}
+).TelemetryReportingPolicy;
+
+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.import(
+ "resource://gre/modules/TelemetryReportingPolicy.jsm",
+ null
+ ).Policy;
+ reportingPolicy.setShowInfobarTimeout = set;
+ reportingPolicy.clearShowInfobarTimeout = clear;
+}
+
+function sendSessionRestoredNotification() {
+ let reportingPolicyImpl = ChromeUtils.import(
+ "resource://gre/modules/TelemetryReportingPolicy.jsm",
+ null
+ ).TelemetryReportingPolicyImpl;
+ reportingPolicyImpl.observe(null, "sessionstore-windows-restored", null);
+}
+
+/**
+ * Wait for a tick.
+ */
+function promiseNextTick() {
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+/**
+ * Wait for a notification to be shown in a notification box.
+ * @param {Object} aNotificationBox The notification box.
+ * @return {Promise} Resolved when the notification is displayed.
+ */
+function promiseWaitForAlertActive(aNotificationBox) {
+ let deferred = PromiseUtils.defer();
+ aNotificationBox.stack.addEventListener(
+ "AlertActive",
+ function() {
+ deferred.resolve();
+ },
+ { once: true }
+ );
+ return deferred.promise;
+}
+
+/**
+ * Wait for a notification to be closed.
+ * @param {Object} aNotification The notification.
+ * @return {Promise} Resolved when the notification is closed.
+ */
+function promiseWaitForNotificationClose(aNotification) {
+ let deferred = PromiseUtils.defer();
+ waitForNotificationClose(aNotification, deferred.resolve);
+ return deferred.promise;
+}
+
+function triggerInfoBar(expectedTimeoutMs) {
+ let showInfobarCallback = null;
+ let timeoutMs = null;
+ fakeShowPolicyTimeout(
+ (callback, timeout) => {
+ showInfobarCallback = callback;
+ timeoutMs = timeout;
+ },
+ () => {}
+ );
+ sendSessionRestoredNotification();
+ Assert.ok(!!showInfobarCallback, "Must have a timer callback.");
+ if (expectedTimeoutMs !== undefined) {
+ Assert.equal(timeoutMs, expectedTimeoutMs, "Timeout should match");
+ }
+ showInfobarCallback();
+}
+
+var checkInfobarButton = async function(aNotification) {
+ // Check that the button on the data choices infobar does the right thing.
+ let buttons = aNotification.getElementsByTagName("button");
+ Assert.equal(
+ buttons.length,
+ 1,
+ "There is 1 button in the data reporting notification."
+ );
+ let button = buttons[0];
+
+ // Click on the button.
+ button.click();
+
+ // Wait for the preferences panel to open.
+ await promiseNextTick();
+};
+
+add_task(async function setup() {
+ const isFirstRun = Preferences.get(PREF_FIRST_RUN, true);
+ const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, true);
+ const currentPolicyVersion = Preferences.get(PREF_CURRENT_POLICY_VERSION, 1);
+
+ // Register a cleanup function to reset our preferences.
+ registerCleanupFunction(() => {
+ Preferences.set(PREF_FIRST_RUN, isFirstRun);
+ Preferences.set(PREF_BYPASS_NOTIFICATION, bypassNotification);
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, currentPolicyVersion);
+ Preferences.reset(PREF_TELEMETRY_LOG_LEVEL);
+
+ return closeAllNotifications();
+ });
+
+ // Don't skip the infobar visualisation.
+ Preferences.set(PREF_BYPASS_NOTIFICATION, false);
+ // Set the current policy version.
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, TEST_POLICY_VERSION);
+ // Ensure this isn't the first run, because then we open the first run page.
+ Preferences.set(PREF_FIRST_RUN, false);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+});
+
+function clearAcceptedPolicy() {
+ // Reset the accepted policy.
+ Preferences.reset(PREF_ACCEPTED_POLICY_VERSION);
+ Preferences.reset(PREF_ACCEPTED_POLICY_DATE);
+}
+
+function assertCoherentInitialState() {
+ // Make sure that we have a coherent initial state.
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0),
+ 0,
+ "No version should be set on init."
+ );
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_DATE, 0),
+ 0,
+ "No date should be set on init."
+ );
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "User not notified about datareporting policy."
+ );
+}
+
+add_task(async function test_single_window() {
+ clearAcceptedPolicy();
+
+ // Close all the notifications, then try to trigger the data choices infobar.
+ await closeAllNotifications();
+
+ assertCoherentInitialState();
+
+ let alertShownPromise = promiseWaitForAlertActive(gNotificationBox);
+ Assert.ok(
+ !TelemetryReportingPolicy.canUpload(),
+ "User should not be allowed to upload."
+ );
+
+ // Wait for the infobar to be displayed.
+ triggerInfoBar(10 * 1000);
+ await alertShownPromise;
+
+ Assert.equal(
+ gNotificationBox.allNotifications.length,
+ 1,
+ "Notification Displayed."
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "User should be allowed to upload now."
+ );
+
+ await promiseNextTick();
+ let promiseClosed = promiseWaitForNotificationClose(
+ gNotificationBox.currentNotification
+ );
+ await checkInfobarButton(gNotificationBox.currentNotification);
+ await promiseClosed;
+
+ Assert.equal(
+ gNotificationBox.allNotifications.length,
+ 0,
+ "No notifications remain."
+ );
+
+ // Check that we are still clear to upload and that the policy data is saved.
+ Assert.ok(TelemetryReportingPolicy.canUpload());
+ Assert.equal(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ true,
+ "User notified about datareporting policy."
+ );
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0),
+ TEST_POLICY_VERSION,
+ "Version pref set."
+ );
+ Assert.greater(
+ parseInt(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 10),
+ -1,
+ "Date pref set."
+ );
+});
+
+/* See bug 1571932
+add_task(async function test_multiple_windows() {
+ clearAcceptedPolicy();
+
+ // Close all the notifications, then try to trigger the data choices infobar.
+ await closeAllNotifications();
+
+ // Ensure we see the notification on all windows and that action on one window
+ // results in dismiss on every window.
+ let otherWindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ Assert.ok(
+ otherWindow.gNotificationBox,
+ "2nd window has a global notification box."
+ );
+
+ assertCoherentInitialState();
+
+ let showAlertPromises = [
+ promiseWaitForAlertActive(gNotificationBox),
+ promiseWaitForAlertActive(otherWindow.gNotificationBox),
+ ];
+
+ Assert.ok(
+ !TelemetryReportingPolicy.canUpload(),
+ "User should not be allowed to upload."
+ );
+
+ // Wait for the infobars.
+ triggerInfoBar(10 * 1000);
+ await Promise.all(showAlertPromises);
+
+ // Both notification were displayed. Close one and check that both gets closed.
+ let closeAlertPromises = [
+ promiseWaitForNotificationClose(gNotificationBox.currentNotification),
+ promiseWaitForNotificationClose(
+ otherWindow.gNotificationBox.currentNotification
+ ),
+ ];
+ gNotificationBox.currentNotification.close();
+ await Promise.all(closeAlertPromises);
+
+ // Close the second window we opened.
+ await BrowserTestUtils.closeWindow(otherWindow);
+
+ // Check that we are clear to upload and that the policy data us saved.
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "User should be allowed to upload now."
+ );
+ Assert.equal(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ true,
+ "User notified about datareporting policy."
+ );
+ Assert.equal(
+ Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0),
+ TEST_POLICY_VERSION,
+ "Version pref set."
+ );
+ Assert.greater(
+ parseInt(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 10),
+ -1,
+ "Date pref set."
+ );
+});*/
diff --git a/browser/base/content/test/general/browser_decoderDoctor.js b/browser/base/content/test/general/browser_decoderDoctor.js
new file mode 100644
index 0000000000..29f761b6ca
--- /dev/null
+++ b/browser/base/content/test/general/browser_decoderDoctor.js
@@ -0,0 +1,297 @@
+"use strict";
+
+// 'data' contains the notification data object:
+// - data.type must be provided.
+// - data.isSolved and data.decoderDoctorReportId will be added if not provided
+// (false and "testReportId" resp.)
+// - Other fields (e.g.: data.formats) may be provided as needed.
+// 'notificationMessage': Expected message in the notification bar.
+// Falsy if nothing is expected after the notification is sent, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'label': Expected button label. Falsy if no button is expected, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'accessKey': Expected access key for the button.
+// 'tabChecker': function(openedTab) called with the opened tab that resulted
+// from clicking the button.
+async function test_decoder_doctor_notification(
+ data,
+ notificationMessage,
+ label,
+ accessKey,
+ tabChecker
+) {
+ const TEST_URL = "https://example.org";
+ // A helper closure to test notifications in same or different origins.
+ // 'test_cross_origin' is used to determine if the observers used in the test
+ // are notified in the same frame (when false) or in a cross origin iframe
+ // (when true).
+ async function create_tab_and_test(test_cross_origin) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TEST_URL },
+ async function(browser) {
+ let awaitNotificationBar = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "decoder-doctor-notification"
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [data, test_cross_origin],
+ /* eslint-disable-next-line no-shadow */
+ async function(data, test_cross_origin) {
+ if (!test_cross_origin) {
+ // Notify in the same origin.
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ return;
+ // Done notifying in the same origin.
+ }
+
+ // Notify in a different origin.
+ const CROSS_ORIGIN_URL = "https://example.com";
+ let frame = content.document.createElement("iframe");
+ frame.src = CROSS_ORIGIN_URL;
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ await content.SpecialPowers.spawn(frame, [data], async function(
+ /* eslint-disable-next-line no-shadow */
+ data
+ ) {
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ });
+ // Done notifying in a different origin.
+ }
+ );
+
+ if (!notificationMessage) {
+ ok(
+ true,
+ "Tested notifying observers with a nonsensical message, no effects expected"
+ );
+ return;
+ }
+
+ let notification;
+ try {
+ notification = await awaitNotificationBar;
+ } catch (ex) {
+ ok(false, ex);
+ return;
+ }
+ ok(notification, "Got decoder-doctor-notification notification");
+
+ is(
+ notification.messageText.textContent,
+ notificationMessage,
+ "notification message should match expectation"
+ );
+
+ let button = notification.querySelector("button");
+ if (!label) {
+ ok(!button, "There should not be button");
+ return;
+ }
+
+ is(
+ button.getAttribute("label"),
+ label,
+ `notification button should be '${label}'`
+ );
+ is(
+ button.getAttribute("accesskey"),
+ accessKey,
+ "notification button should have accesskey"
+ );
+
+ if (!tabChecker) {
+ ok(false, "Test implementation error: Missing tabChecker");
+ return;
+ }
+ let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ button.click();
+ let openedTab = await awaitNewTab;
+ tabChecker(openedTab);
+ BrowserTestUtils.removeTab(openedTab);
+ }
+ );
+ }
+
+ if (typeof data.type === "undefined") {
+ ok(false, "Test implementation error: data.type must be provided");
+ return;
+ }
+ data.isSolved = data.isSolved || false;
+ if (typeof data.decoderDoctorReportId === "undefined") {
+ data.decoderDoctorReportId = "testReportId";
+ }
+
+ // Test same origin.
+ await create_tab_and_test(false);
+ // Test cross origin.
+ await create_tab_and_test(true);
+}
+
+function tab_checker_for_sumo(expectedPath) {
+ return function(openedTab) {
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let url = baseURL + expectedPath;
+ is(
+ openedTab.linkedBrowser.currentURI.spec,
+ url,
+ `Expected '${url}' in new tab`
+ );
+ };
+}
+
+function tab_checker_for_webcompat(expectedParams) {
+ return function(openedTab) {
+ let urlString = openedTab.linkedBrowser.currentURI.spec;
+ let endpoint = Services.prefs.getStringPref(
+ "media.decoder-doctor.new-issue-endpoint",
+ ""
+ );
+ ok(
+ urlString.startsWith(endpoint),
+ `Expected URL starting with '${endpoint}', got '${urlString}'`
+ );
+ let params = new URL(urlString).searchParams;
+ for (let k in expectedParams) {
+ if (!params.has(k)) {
+ ok(false, `Expected ${k} in webcompat URL`);
+ } else {
+ is(
+ params.get(k),
+ expectedParams[k],
+ `Expected ${k}='${expectedParams[k]}' in webcompat URL`
+ );
+ }
+ }
+ };
+}
+
+add_task(async function test_platform_decoder_not_found() {
+ let message = "";
+ let isLinux = AppConstants.platform == "linux";
+ if (isLinux) {
+ message = gNavigatorBundle.getString("decoder.noCodecsLinux.message");
+ } else if (AppConstants.platform == "win") {
+ message = gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "platform-decoder-not-found", formats: "testFormat" },
+ message,
+ isLinux ? "" : gNavigatorBundle.getString("decoder.noCodecs.button"),
+ isLinux ? "" : gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ tab_checker_for_sumo("fix-video-audio-problems-firefox-windows")
+ );
+});
+
+add_task(async function test_cannot_initialize_pulseaudio() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString("decoder.noPulseAudio.message");
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "cannot-initialize-pulseaudio", formats: "testFormat" },
+ message,
+ gNavigatorBundle.getString("decoder.noCodecs.button"),
+ gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ tab_checker_for_sumo("fix-common-audio-and-video-issues")
+ );
+});
+
+add_task(async function test_unsupported_libavcodec() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString(
+ "decoder.unsupportedLibavcodec.message"
+ );
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "unsupported-libavcodec", formats: "testFormat" },
+ message
+ );
+});
+
+add_task(async function test_decode_error() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ["browser.fixup.fallback-to-https", false],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeError.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-error",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
+
+add_task(async function test_decode_warning() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeWarning.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-warning",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
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..099f716072
--- /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.loadURI(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]
+ );
+ 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..08a59ec92a
--- /dev/null
+++ b/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js
@@ -0,0 +1,237 @@
+/* eslint-env mozilla/frame-script */
+/* 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 =
+ "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..64773986d1
--- /dev/null
+++ b/browser/base/content/test/general/browser_double_close_tab.js
@@ -0,0 +1,93 @@
+/* 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;
+
+function waitForDialog(callback) {
+ function onTabModalDialogLoaded(node) {
+ Services.obs.removeObserver(
+ onTabModalDialogLoaded,
+ "tabmodal-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(onTabModalDialogLoaded, "tabmodal-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 });
+ let failureTimeout = setTimeout(function() {
+ ok(false, "Dialog should have been destroyed");
+ done();
+ }, 10000);
+
+ function done() {
+ clearTimeout(failureTimeout);
+ observer.disconnect();
+ observer = null;
+ callback();
+ }
+}
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", 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");
+ 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..5786712a0d
--- /dev/null
+++ b/browser/base/content/test/general/browser_drag.js
@@ -0,0 +1,64 @@
+async function test() {
+ waitForExplicitFinish();
+
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // ---- Test dragging the proxy icon ---
+ var value = content.location.href;
+ var urlString = value + "\n" + content.document.title;
+ var htmlString = '<a href="' + value + '">' + value + "</a>";
+ var expected = [
+ [
+ { type: "text/x-moz-url", data: urlString },
+ { type: "text/uri-list", data: value },
+ { type: "text/plain", data: value },
+ { type: "text/html", data: htmlString },
+ ],
+ ];
+ // set the valid attribute so dropping is allowed
+ var oldstate = gURLBar.getAttribute("pageproxystate");
+ gURLBar.setPageProxyState("valid");
+ let result = await EventUtils.synthesizePlainDragAndCancel(
+ {
+ srcElement: document.getElementById("identity-box"),
+ },
+ expected
+ );
+ ok(result === true, "dragging dataTransfer should be expected");
+ gURLBar.setPageProxyState(oldstate);
+ // Now, the identity information panel is opened by the proxy icon click.
+ // We need to close it for next tests.
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
+
+ // now test dragging onto a tab
+ var tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ var browser = gBrowser.getBrowserForTab(tab);
+
+ browser.addEventListener(
+ "load",
+ function() {
+ is(
+ browser.contentWindow.location,
+ "http://mochi.test:8888/",
+ "drop on tab"
+ );
+ gBrowser.removeTab(tab);
+ finish();
+ },
+ true
+ );
+
+ EventUtils.synthesizeDrop(
+ tab,
+ tab,
+ [[{ type: "text/uri-list", data: "http://mochi.test:8888/" }]],
+ "copy",
+ window
+ );
+}
diff --git a/browser/base/content/test/general/browser_duplicateIDs.js b/browser/base/content/test/general/browser_duplicateIDs.js
new file mode 100644
index 0000000000..ceaa2721d7
--- /dev/null
+++ b/browser/base/content/test/general/browser_duplicateIDs.js
@@ -0,0 +1,10 @@
+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..17980f25af
--- /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.loadURI(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..4ba16f1490
--- /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..50050bb3fe
--- /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..5a2878040e
--- /dev/null
+++ b/browser/base/content/test/general/browser_gestureSupport.js
@@ -0,0 +1,927 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Simple gestures tests
+//
+// 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.";
+
+function test() {
+ waitForExplicitFinish();
+
+ // Disable the default gestures support during 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);
+
+ // Test Firefox's gestures support.
+ test_commandset = document.getElementById("mainCommandSet");
+ test_swipeGestures();
+ test_latchedGesture("pinch", "out", "in", "MozMagnifyGesture");
+ 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(20, 20, false, false),
+ "evt.target (" + evt.target + ") does not match expected value"
+ );
+ is(
+ evt.clientX,
+ 20,
+ "evt.clientX (" + evt.clientX + ") does not match expected value"
+ );
+ is(
+ evt.clientY,
+ 20,
+ "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, 20, 20, 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, 20, 20, 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.
+function test_emitLatchedEvents(eventPrefix, initialDelta, cmd) {
+ let cumulativeDelta = 0;
+ let isIncreasing = initialDelta > 0;
+
+ let expect = {};
+ // Reset the call counters and initialize expected values
+ for (let dir in cmd) {
+ cmd[dir].callCount = expect[dir] = 0;
+ }
+
+ let check = (aDir, aMsg) => ok(cmd[aDir].callCount == expect[aDir], aMsg);
+ let checkBoth = function(aNum, aInc, aDec) {
+ let prefix = "Step " + aNum + ": ";
+ check("inc", prefix + aInc);
+ check("dec", prefix + aDec);
+ };
+
+ // Send the "Start" event.
+ test_utils.sendSimpleGestureEvent(
+ eventPrefix + "Start",
+ 0,
+ 0,
+ 0,
+ initialDelta,
+ 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);
+ test_utils.sendSimpleGestureEvent(
+ eventPrefix + "Update",
+ 0,
+ 0,
+ 0,
+ delta,
+ 0
+ );
+ cumulativeDelta += delta;
+ checkBoth(
+ 2,
+ "Increasing command was triggered",
+ "Decreasing command was triggered"
+ );
+ }
+
+ // Now go back in the opposite direction.
+ test_utils.sendSimpleGestureEvent(
+ eventPrefix + "Update",
+ 0,
+ 0,
+ 0,
+ -initialDelta,
+ 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);
+ test_utils.sendSimpleGestureEvent(
+ eventPrefix + "Update",
+ 0,
+ 0,
+ 0,
+ delta,
+ 0
+ );
+ cumulativeDelta += delta;
+ checkBoth(
+ 4,
+ "Increasing command was triggered",
+ "Decreasing command was triggered"
+ );
+ }
+
+ // Go back to the original direction. The original command should trigger.
+ test_utils.sendSimpleGestureEvent(
+ eventPrefix + "Update",
+ 0,
+ 0,
+ 0,
+ initialDelta,
+ 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.
+ test_utils.sendSimpleGestureEvent(eventPrefix, 0, 0, 0, cumulativeDelta, 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.
+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.
+ test_emitLatchedEvents(eventPrefix, 500, cmd);
+ 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.
+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;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Start", 0, 0, 0, 49.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Now trigger the threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, 1, 0);
+ ok(cmdInc.callCount == 1, "Increasing command was not triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // The tracking counter should go to zero. Go back the other way and
+ // stop short of triggering the threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, -49.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Now cross the threshold and trigger the decreasing command.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, -1.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 1, "Decreasing command was not triggered");
+
+ // Send the wrap-up event. No commands should trigger.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix, 0, 0, 0, -0.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Restore the gesture to its original configuration.
+ Services.prefs.setBoolPref(branch + "latched", oldLatchedValue);
+ Services.prefs.setIntPref(branch + "threshold", oldThresholdValue);
+ test_removeCommand(cmdInc);
+ test_removeCommand(cmdDec);
+}
+
+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();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, up, 0, 0);
+ ok(cmdUp.callCount == 1, "Step 1: Up command was not triggered");
+ ok(cmdDown.callCount == 0, "Step 1: Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 1: Left command was triggered");
+ ok(cmdRight.callCount == 0, "Step 1: Right command was triggered");
+
+ // DOWN
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, down, 0, 0);
+ ok(cmdUp.callCount == 0, "Step 2: Up command was triggered");
+ ok(cmdDown.callCount == 1, "Step 2: Down command was not triggered");
+ ok(cmdLeft.callCount == 0, "Step 2: Left command was triggered");
+ ok(cmdRight.callCount == 0, "Step 2: Right command was triggered");
+
+ // LEFT
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, left, 0, 0);
+ ok(cmdUp.callCount == 0, "Step 3: Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 3: Down command was triggered");
+ ok(cmdLeft.callCount == 1, "Step 3: Left command was not triggered");
+ ok(cmdRight.callCount == 0, "Step 3: Right command was triggered");
+
+ // RIGHT
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, right, 0, 0);
+ ok(cmdUp.callCount == 0, "Step 4: Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 4: Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 4: Left command was triggered");
+ ok(cmdRight.callCount == 1, "Step 4: Right command was not triggered");
+
+ // Make sure combinations do not trigger events.
+ let combos = [up | left, up | right, down | left, down | right];
+ for (let i = 0; i < combos.length; i++) {
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, combos[i], 0, 0);
+ ok(cmdUp.callCount == 0, "Step 5-" + i + ": Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 5-" + i + ": Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 5-" + i + ": Left command was triggered");
+ ok(
+ cmdRight.callCount == 0,
+ "Step 5-" + i + ": Right command was triggered"
+ );
+ }
+
+ // Remove the test commands.
+ test_removeCommand(cmdUp);
+ test_removeCommand(cmdDown);
+ test_removeCommand(cmdLeft);
+ test_removeCommand(cmdRight);
+}
+
+function test_rotateHelperGetImageRotation(aImageElement) {
+ // Get the true image rotation from the transform matrix, bounded
+ // to 0 <= result < 360
+ let transformValue = content.window.getComputedStyle(aImageElement).transform;
+ if (transformValue == "none") {
+ return 0;
+ }
+
+ transformValue = transformValue
+ .split("(")[1]
+ .split(")")[0]
+ .split(",");
+ var rotation = Math.round(
+ Math.atan2(transformValue[1], transformValue[0]) * (180 / Math.PI)
+ );
+ return rotation < 0 ? rotation + 360 : rotation;
+}
+
+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
+ test_utils.sendSimpleGestureEvent(
+ "MozRotateGestureStart",
+ 0,
+ 0,
+ aDirection,
+ 0.001,
+ 0
+ );
+ test_utils.sendSimpleGestureEvent(
+ "MozRotateGestureUpdate",
+ 0,
+ 0,
+ aDirection,
+ delta,
+ 0
+ );
+ aImageElement.clientTop;
+
+ // If stop, check intermediate
+ if (aStop) {
+ // Send near-zero-delta to stop, and force flush
+ test_utils.sendSimpleGestureEvent(
+ "MozRotateGestureUpdate",
+ 0,
+ 0,
+ aDirection,
+ 0.001,
+ 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
+ test_utils.sendSimpleGestureEvent("MozRotateGesture", 0, 0, aDirection, 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")
+ );
+}
+
+function test_rotateGesturesOnTab() {
+ gBrowser.selectedBrowser.removeEventListener(
+ "load",
+ test_rotateGesturesOnTab,
+ true
+ );
+
+ if (!(content.document instanceof ImageDocument)) {
+ ok(false, "Image document failed to open for rotation testing");
+ gBrowser.removeTab(test_imageTab);
+ 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);
+ 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
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 0), cl, 35, true);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 0), cl, 35, false);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 90), cl, 55, true);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 180), cl, 55, false);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 270), ccl, 35, true);
+ test_rotateHelperOneGesture(
+ imgElem,
+ normRot(initRot + 270),
+ ccl,
+ 35,
+ false
+ );
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 180), ccl, 55, true);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 90), ccl, 55, false);
+
+ // Manually rotate it 90 degrees clockwise to prepare for next iteration,
+ // and force flush
+ test_utils.sendSimpleGestureEvent(
+ "MozRotateGestureStart",
+ 0,
+ 0,
+ cl,
+ 0.001,
+ 0
+ );
+ test_utils.sendSimpleGestureEvent(
+ "MozRotateGestureUpdate",
+ 0,
+ 0,
+ cl,
+ 90,
+ 0
+ );
+ test_utils.sendSimpleGestureEvent(
+ "MozRotateGestureUpdate",
+ 0,
+ 0,
+ cl,
+ 0.001,
+ 0
+ );
+ test_utils.sendSimpleGestureEvent("MozRotateGesture", 0, 0, cl, 0, 0);
+ imgElem.clientTop;
+ }
+
+ gBrowser.removeTab(test_imageTab);
+ test_imageTab = 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..af9405a7b4
--- /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..415aa06145
--- /dev/null
+++ b/browser/base/content/test/general/browser_homeDrop.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+ let HOMEPAGE_PREF = "browser.startup.homepage";
+
+ await pushPrefs([HOMEPAGE_PREF, "about:mozilla"]);
+
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button
+ // that should be visible.
+ let dragSrcElement = document.getElementById("sidebar-button");
+ ok(dragSrcElement, "Sidebar button exists");
+ let homeButton = document.getElementById("home-button");
+ ok(homeButton, "home button present");
+
+ async function drop(dragData, homepage) {
+ let setHomepageDialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+
+ EventUtils.synthesizeDrop(
+ dragSrcElement,
+ homeButton,
+ dragData,
+ "copy",
+ window
+ );
+ // Ensure dnd suppression is cleared.
+ EventUtils.synthesizeMouseAtCenter(homeButton, { type: "mouseup" }, window);
+
+ let setHomepageDialog = await setHomepageDialogPromise;
+ ok(true, "dialog appeared in response to home button drop");
+
+ let setHomepagePromise = new Promise(function(resolve) {
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(subject, topic, data) {
+ is(topic, "nsPref:changed", "observed correct topic");
+ is(data, HOMEPAGE_PREF, "observed correct data");
+ let modified = Services.prefs.getStringPref(HOMEPAGE_PREF);
+ is(modified, homepage, "homepage is set correctly");
+ Services.prefs.removeObserver(HOMEPAGE_PREF, observer);
+
+ Services.prefs.setStringPref(HOMEPAGE_PREF, "about:mozilla;");
+
+ resolve();
+ },
+ };
+ Services.prefs.addObserver(HOMEPAGE_PREF, observer);
+ });
+
+ setHomepageDialog.document.getElementById("commonDialog").acceptDialog();
+
+ await setHomepagePromise;
+ }
+
+ 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..d63da23aee
--- /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..15383671bd
--- /dev/null
+++ b/browser/base/content/test/general/browser_lastAccessedTab.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// gBrowser.selectedTab.lastAccessed and Date.now() called from this test can't
+// run concurrently, and therefore don't always match exactly.
+const CURRENT_TIME_TOLERANCE_MS = 15;
+
+function isCurrent(tab, msg) {
+ const DIFF = Math.abs(Date.now() - tab.lastAccessed);
+ ok(DIFF <= CURRENT_TIME_TOLERANCE_MS, msg + " (difference: " + DIFF + ")");
+}
+
+function nextStep(fn) {
+ setTimeout(fn, CURRENT_TIME_TOLERANCE_MS + 10);
+}
+
+var originalTab;
+var newTab;
+
+function test() {
+ waitForExplicitFinish();
+ // This test assumes that time passes between operations. But if the precision
+ // is low enough, and the test fast enough, an operation, and a successive call
+ // to Date.now() will have the same time value.
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.reduceTimerPrecision", false]] },
+ function() {
+ originalTab = gBrowser.selectedTab;
+ nextStep(step2);
+ }
+ );
+}
+
+function step2() {
+ isCurrent(originalTab, "selected tab has the current timestamp");
+ newTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ });
+ nextStep(step3);
+}
+
+function step3() {
+ ok(newTab.lastAccessed < Date.now(), "new tab hasn't been selected so far");
+ gBrowser.selectedTab = newTab;
+ isCurrent(newTab, "new tab has the current timestamp after being selected");
+ nextStep(step4);
+}
+
+function step4() {
+ ok(
+ originalTab.lastAccessed < Date.now(),
+ "original tab has old timestamp after being deselected"
+ );
+ isCurrent(
+ newTab,
+ "new tab has the current timestamp since it's still selected"
+ );
+
+ gBrowser.removeTab(newTab);
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_menuButtonFitts.js b/browser/base/content/test/general/browser_menuButtonFitts.js
new file mode 100644
index 0000000000..3905cdb652
--- /dev/null
+++ b/browser/base/content/test/general/browser_menuButtonFitts.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * Clicking the right end of a maximized window should open the hamburger menu.
+ */
+add_task(async function test_clicking_hamburger_edge_fitts() {
+ let oldWidth = window.outerWidth;
+ let maximizeDone = BrowserTestUtils.waitForEvent(
+ window,
+ "resize",
+ false,
+ () => window.outerWidth >= screen.width - 1
+ );
+ window.maximize();
+ // Ensure we actually finish the resize before continuing.
+ await maximizeDone;
+
+ // 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 = boundingRect.width - 1; // Use the last pixel of the screen since it is maximized.
+
+ let popupHiddenResolve;
+ let popupHiddenPromise = new Promise(resolve => {
+ popupHiddenResolve = resolve;
+ });
+ async function onPopupHidden() {
+ PanelUI.panel.removeEventListener("popuphidden", onPopupHidden);
+ let restoreDone = BrowserTestUtils.waitForEvent(
+ window,
+ "resize",
+ false,
+ () => {
+ let w = window.outerWidth;
+ return w > oldWidth - 5 && w < oldWidth + 5;
+ }
+ );
+ 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(xPixel, yPixel, {}, 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..c44ff19340
--- /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..694808f516
--- /dev/null
+++ b/browser/base/content/test/general/browser_minimize.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+ registerCleanupFunction(function() {
+ window.restore();
+ });
+ function waitForActive() {
+ return gBrowser.selectedTab.linkedBrowser.docShellIsActive;
+ }
+ function waitForInactive() {
+ return !gBrowser.selectedTab.linkedBrowser.docShellIsActive;
+ }
+ await TestUtils.waitForCondition(waitForActive);
+ is(
+ gBrowser.selectedTab.linkedBrowser.docShellIsActive,
+ true,
+ "Docshell should be active"
+ );
+ window.minimize();
+ await TestUtils.waitForCondition(waitForInactive);
+ is(
+ gBrowser.selectedTab.linkedBrowser.docShellIsActive,
+ false,
+ "Docshell should be Inactive"
+ );
+ window.restore();
+ await TestUtils.waitForCondition(waitForActive);
+ is(
+ gBrowser.selectedTab.linkedBrowser.docShellIsActive,
+ true,
+ "Docshell should be active again"
+ );
+});
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..21ec542e1e
--- /dev/null
+++ b/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
@@ -0,0 +1,40 @@
+"use strict";
+
+const kURL =
+ "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,
+ "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..3231f07c3f
--- /dev/null
+++ b/browser/base/content/test/general/browser_newTabDrop.js
@@ -0,0 +1,221 @@
+const ANY_URL = undefined;
+
+registerCleanupFunction(async function cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ await Services.search.setDefault(originalEngine);
+ let engine = Services.search.getEngineByName("MozSearch");
+ await Services.search.removeEngine(engine);
+});
+
+let originalEngine;
+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 Services.search.addEngineWithDetails("MozSearch", {
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("MozSearch");
+ originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+});
+
+// New Tab Button opens any link.
+add_task(async function single_url() {
+ await dropText("mochi.test/first", ["https://www.mochi.test/first"]);
+});
+add_task(async function single_url2() {
+ await dropText("mochi.test/second", ["https://www.mochi.test/second"]);
+});
+add_task(async function single_url3() {
+ await dropText("mochi.test/third", ["https://www.mochi.test/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("mochi.test/1\nmochi.test/2", [
+ "https://www.mochi.test/1",
+ "https://www.mochi.test/2",
+ ]);
+});
+
+// Multiple text/plain items, with single and multiple links.
+add_task(async function multiple_items_single_and_multiple_links() {
+ await drop(
+ [
+ [{ type: "text/plain", data: "mochi.test/5" }],
+ [{ type: "text/plain", data: "mochi.test/6\nmochi.test/7" }],
+ ],
+ [
+ "https://www.mochi.test/5",
+ "https://www.mochi.test/6",
+ "https://www.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",
+ },
+ ],
+ ],
+ ["https://www.mochi.test/8", "https://www.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" },
+ ],
+ ],
+ ["https://www.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"), [
+ "https://www.mochi.test/multi0",
+ "https://www.mochi.test/multi1",
+ "https://www.mochi.test/multi2",
+ "https://www.mochi.test/multi3",
+ "https://www.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"), [
+ "https://www.mochi.test/accept0",
+ "https://www.mochi.test/accept1",
+ "https://www.mochi.test/accept2",
+ "https://www.mochi.test/accept3",
+ "https://www.mochi.test/accept4",
+ ]);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+add_task(async function multiple_tabs_over_max_cancel() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/cancel" + i);
+ }
+ await dropText(urls.join("\n"), []);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+
+// Open URLs ignoring non-URL.
+add_task(async function multiple_urls() {
+ await dropText(
+ `
+ mochi.test/urls0
+ mochi.test/urls1
+ mochi.test/urls2
+ non url0
+ mochi.test/urls3
+ non url1
+ non url2
+`,
+ [
+ "https://www.mochi.test/urls0",
+ "https://www.mochi.test/urls1",
+ "https://www.mochi.test/urls2",
+ "https://www.mochi.test/urls3",
+ ]
+ );
+});
+
+// Open single search if there's no URL.
+add_task(async function multiple_text() {
+ await dropText(
+ `
+ non url0
+ non url1
+ non url2
+`,
+ [ANY_URL]
+ );
+});
+
+function dropText(text, expectedURLs) {
+ return drop([[{ type: "text/plain", data: text }]], expectedURLs);
+}
+
+async function drop(dragData, expectedURLs) {
+ let dragDataString = JSON.stringify(dragData);
+ info(
+ `Starting test for dragData:${dragDataString}; expectedURLs.length:${expectedURLs.length}`
+ );
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button
+ // that should be visible.
+ let dragSrcElement = document.getElementById("sidebar-button");
+ ok(dragSrcElement, "Sidebar button exists");
+ let newTabButton = document.getElementById(
+ gBrowser.tabContainer.hasAttribute("overflow")
+ ? "new-tab-button"
+ : "tabs-newtab-button"
+ );
+ ok(newTabButton, "New Tab button exists");
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(newTabButton, "drop");
+
+ let loadedPromises = expectedURLs.map(url =>
+ BrowserTestUtils.waitForNewTab(gBrowser, url, false, true)
+ );
+
+ EventUtils.synthesizeDrop(
+ dragSrcElement,
+ newTabButton,
+ dragData,
+ "link",
+ window
+ );
+
+ let tabs = await Promise.all(loadedPromises);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ await awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_newWindowDrop.js b/browser/base/content/test/general/browser_newWindowDrop.js
new file mode 100644
index 0000000000..129e0f40d9
--- /dev/null
+++ b/browser/base/content/test/general/browser_newWindowDrop.js
@@ -0,0 +1,232 @@
+registerCleanupFunction(async function cleanup() {
+ await Services.search.setDefault(originalEngine);
+ let engine = Services.search.getEngineByName("MozSearch");
+ await Services.search.removeEngine(engine);
+});
+
+let originalEngine;
+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 Services.search.addEngineWithDetails("MozSearch", {
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("MozSearch");
+ originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ // Move New Window button to nav bar, to make it possible to drag and drop.
+ let { CustomizableUI } = ChromeUtils.import(
+ "resource:///modules/CustomizableUI.jsm"
+ );
+ 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");
+ });
+ }
+});
+
+// New Window Button opens any link.
+add_task(async function single_url() {
+ await dropText("mochi.test/first", ["https://www.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", ["https://www.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", ["https://www.mochi.test/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("mochi.test/1\nmochi.test/2", [
+ "https://www.mochi.test/1",
+ "https://www.mochi.test/2",
+ ]);
+});
+add_task(async function multiple_urls_javascript() {
+ await dropText("javascript:'bad1'\nmochi.test/3", [
+ "about:blank",
+ "https://www.mochi.test/3",
+ ]);
+});
+add_task(async function multiple_urls_data() {
+ await dropText("mochi.test/4\ndata:text/html,bad1", [
+ "https://www.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" }],
+ ],
+ [
+ "https://www.mochi.test/5",
+ "https://www.mochi.test/6",
+ "https://www.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",
+ },
+ ],
+ ],
+ ["https://www.mochi.test/8", "https://www.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" },
+ ],
+ ],
+ ["https://www.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"), [
+ "https://www.mochi.test/multi0",
+ "https://www.mochi.test/multi1",
+ "https://www.mochi.test/multi2",
+ "https://www.mochi.test/multi3",
+ "https://www.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"),
+ [
+ "https://www.mochi.test/accept0",
+ "https://www.mochi.test/accept1",
+ "https://www.mochi.test/accept2",
+ "https://www.mochi.test/accept3",
+ "https://www.mochi.test/accept4",
+ ],
+ true
+ );
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+add_task(async function multiple_tabs_over_max_cancel() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/cancel" + i);
+ }
+ await dropText(urls.join("\n"), [], true);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+
+function dropText(text, expectedURLs, ignoreFirstWindow = false) {
+ return drop(
+ [[{ type: "text/plain", data: text }]],
+ expectedURLs,
+ ignoreFirstWindow
+ );
+}
+
+async function drop(dragData, expectedURLs, ignoreFirstWindow = false) {
+ let dragDataString = JSON.stringify(dragData);
+ info(
+ `Starting test for dragData:${dragDataString}; expectedURLs.length:${expectedURLs.length}`
+ );
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button
+ // that should be visible.
+ let dragSrcElement = document.getElementById("sidebar-button");
+ ok(dragSrcElement, "Sidebar button exists");
+ let newWindowButton = document.getElementById("new-window-button");
+ ok(newWindowButton, "New Window button exists");
+
+ let tmp = {};
+ ChromeUtils.import("resource://testing-common/TestUtils.jsm", tmp);
+
+ 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..18c7c308a6
--- /dev/null
+++ b/browser/base/content/test/general/browser_new_http_window_opened_from_file_tab.js
@@ -0,0 +1,62 @@
+/* -*- 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";
+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..fa0f5c3d79
--- /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_page_style_menu.js b/browser/base/content/test/general/browser_page_style_menu.js
new file mode 100644
index 0000000000..9f180f81e5
--- /dev/null
+++ b/browser/base/content/test/general/browser_page_style_menu.js
@@ -0,0 +1,176 @@
+"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 WEB_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+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.loadURI(browser, WEB_ROOT + "page_style_sample.html");
+ await promiseStylesheetsLoaded(tab, 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/general/browser_page_style_menu_update.js b/browser/base/content/test/general/browser_page_style_menu_update.js
new file mode 100644
index 0000000000..0aef6a1d87
--- /dev/null
+++ b/browser/base/content/test/general/browser_page_style_menu_update.js
@@ -0,0 +1,47 @@
+"use strict";
+
+const PAGE =
+ "http://example.com/browser/browser/base/content/test/general/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.loadURI(browser, PAGE);
+ await promiseStylesheetsLoaded(tab, 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.click();
+
+ 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/general/browser_plainTextLinks.js b/browser/base/content/test/general/browser_plainTextLinks.js
new file mode 100644
index 0000000000..f27e803da0
--- /dev/null
+++ b/browser/base/content/test/general/browser_plainTextLinks.js
@@ -0,0 +1,232 @@
+/* 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");
+ 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(
+ "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(
+ "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(
+ "http://www.example.com/",
+ "Linkified text should open the correct link"
+ ),
+ () => {
+ testExpected(false, "Link options should show for open-suse.ru");
+ testLinkExpected(
+ "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..9a95389900
--- /dev/null
+++ b/browser/base/content/test/general/browser_printpreview.js
@@ -0,0 +1,86 @@
+let ourTab;
+
+async function test() {
+ waitForExplicitFinish();
+ await pushPrefs(["print.tab_modal.enabled", false]);
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true).then(
+ function(tab) {
+ ourTab = tab;
+ ok(
+ !gInPrintPreviewMode,
+ "Should NOT be in print preview mode at starting this tests"
+ );
+ // Skip access key test on platforms which don't support access key.
+ if (!/Win|Linux/.test(navigator.platform)) {
+ openPrintPreview(testClosePrintPreviewWithEscKey);
+ } else {
+ openPrintPreview(testClosePrintPreviewWithAccessKey);
+ }
+ }
+ );
+}
+
+function tidyUp() {
+ BrowserTestUtils.removeTab(ourTab);
+ finish();
+}
+
+async function testClosePrintPreviewWithAccessKey() {
+ let closeButton = document.getElementById(
+ "print-preview-toolbar-close-button"
+ );
+ await TestUtils.waitForCondition(() => closeButton.hasAttribute("accesskey"));
+ EventUtils.synthesizeKey("c", { altKey: true });
+ checkPrintPreviewClosed(function(aSucceeded) {
+ ok(aSucceeded, "print preview mode should be finished by access key");
+ openPrintPreview(testClosePrintPreviewWithEscKey);
+ });
+}
+
+function testClosePrintPreviewWithEscKey() {
+ EventUtils.synthesizeKey("KEY_Escape");
+ checkPrintPreviewClosed(function(aSucceeded) {
+ ok(aSucceeded, "print preview mode should be finished by Esc key press");
+ openPrintPreview(testClosePrintPreviewWithClosingWindowShortcutKey);
+ });
+}
+
+function testClosePrintPreviewWithClosingWindowShortcutKey() {
+ EventUtils.synthesizeKey("w", { accelKey: true });
+ checkPrintPreviewClosed(function(aSucceeded) {
+ ok(
+ aSucceeded,
+ "print preview mode should be finished by closing window shortcut key"
+ );
+ tidyUp();
+ });
+}
+
+function openPrintPreview(aCallback) {
+ document.getElementById("cmd_printPreview").doCommand();
+ executeSoon(function waitForPrintPreview() {
+ if (gInPrintPreviewMode) {
+ executeSoon(aCallback);
+ return;
+ }
+ executeSoon(waitForPrintPreview);
+ });
+}
+
+function checkPrintPreviewClosed(aCallback) {
+ let count = 0;
+ executeSoon(function waitForPrintPreviewClosed() {
+ if (!gInPrintPreviewMode) {
+ executeSoon(function() {
+ aCallback(count < 1000);
+ });
+ return;
+ }
+ if (++count == 1000) {
+ // The test might fail.
+ PrintUtils.exitPrintPreview();
+ }
+ executeSoon(waitForPrintPreviewClosed);
+ });
+}
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..4c069d8c6e
--- /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,
+ "Private Browsing",
+ "New tabs in the private browsing windows should have 'Private Browsing' 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..bf27fc18e1
--- /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..b0eab800ad
--- /dev/null
+++ b/browser/base/content/test/general/browser_refreshBlocker.js
@@ -0,0 +1,157 @@
+"use strict";
+
+const META_PAGE =
+ "http://example.org/browser/browser/base/content/test/general/refresh_meta.sjs";
+const HEADER_PAGE =
+ "http://example.org/browser/browser/base/content/test/general/refresh_header.sjs";
+const TARGET_PAGE =
+ "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, false, true);
+
+ 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.loadURI(
+ 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.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);
+});
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..216e61369c
--- /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..64b67b1cdf
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.import(
+ "resource://gre/modules/WebChannel.jsm"
+);
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+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.loadOneTab(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.import(
+ "resource://gre/modules/UpdateUtils.jsm",
+ {}
+ ).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..3c0f9217ff
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteWebNavigation_postdata.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/. */
+
+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 obj = {};
+ ChromeUtils.import("resource://testing-common/httpd.js", obj);
+ ChromeUtils.import("resource://services-common/utils.js", obj);
+
+ let server = new obj.HttpServer();
+ server.start(-1);
+
+ await new Promise(resolve => {
+ server.registerPathHandler("/test", (request, response) => {
+ let body = obj.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_removeTabsToTheEnd.js b/browser/base/content/test/general/browser_removeTabsToTheEnd.js
new file mode 100644
index 0000000000..668452e178
--- /dev/null
+++ b/browser/base/content/test/general/browser_removeTabsToTheEnd.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() {
+ // Add two new tabs after the original tab. Pin the first one.
+ let originalTab = gBrowser.selectedTab;
+ let newTab1 = BrowserTestUtils.addTab(gBrowser);
+ BrowserTestUtils.addTab(gBrowser);
+ gBrowser.pinTab(newTab1);
+
+ // Check that there is only one closable tab from originalTab to the end
+ is(
+ gBrowser.getTabsToTheEndFrom(originalTab).length,
+ 1,
+ "One unpinned tab to the right"
+ );
+
+ // Remove tabs to the end
+ gBrowser.removeTabsToTheEndFrom(originalTab);
+ is(gBrowser.tabs.length, 2, "Length is 2");
+ is(gBrowser.tabs[1], originalTab, "Starting tab is not removed");
+ is(gBrowser.tabs[0], newTab1, "Pinned tab is not removed");
+
+ // Remove pinned tab
+ gBrowser.removeTab(newTab1);
+}
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..5e54602173
--- /dev/null
+++ b/browser/base/content/test/general/browser_restore_isAppTab.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/frame-script */
+
+const { TabStateFlusher } = ChromeUtils.import(
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+
+const DUMMY =
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+function isBrowserAppTab(browser) {
+ return SpecialPowers.spawn(browser, [], async () => {
+ return content.docShell.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 = await isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = await isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ BrowserTestUtils.loadURI(gBrowser, DUMMY);
+ await BrowserTestUtils.browserStopped(gBrowser);
+ isAppTab = await isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.unpinTab(tab);
+ isAppTab = await isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = await isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ BrowserTestUtils.loadURI(gBrowser, "about:robots");
+ await BrowserTestUtils.browserStopped(gBrowser);
+ isAppTab = await 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 = await isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = await isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ await restart(browser);
+ isAppTab = await 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..6b6920a263
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_link-perwindowpb.js
@@ -0,0 +1,215 @@
+/* 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.loadURI(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();
+ });
+ });
+ });
+ });
+}
+
+/* import-globals-from ../../../../../toolkit/content/tests/browser/common/mockTransfer.js */
+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..126dd2fc34
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_link_when_window_navigates.js
@@ -0,0 +1,181 @@
+/* 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 UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xhtml";
+
+/* import-globals-from ../../../../../toolkit/content/tests/browser/common/mockTransfer.js */
+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;
+}
+
+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";
+ windowObserver.setCallback(onUCTDialog);
+ BrowserTestUtils.loadURI(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();
+
+ 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);
+ info("Finished running the cleanup code");
+ });
+
+ Services.prefs.setBoolPref(ALWAYS_DOWNLOAD_DIR_PREF, false);
+ testOnWindow(undefined, function(win) {
+ let windowGonePromise = BrowserTestUtils.domWindowClosed(win);
+ Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, true);
+ triggerSave(win, function() {
+ windowGonePromise.then(function() {
+ 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..d988d063e5
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_private_link_perwindowpb.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/. */
+
+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,
+ false
+ );
+ 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);
+});
+
+/* import-globals-from ../../../../../toolkit/content/tests/browser/common/mockTransfer.js */
+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..fe50a18c75
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_video.js
@@ -0,0 +1,100 @@
+/* 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.loadURI(
+ 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;
+});
+
+/* import-globals-from ../../../../../toolkit/content/tests/browser/common/mockTransfer.js */
+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..2a421ec666
--- /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.
+ */
+/* import-globals-from ../../../../../toolkit/content/tests/browser/common/mockTransfer.js */
+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");
+ 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_search_discovery.js b/browser/base/content/test/general/browser_search_discovery.js
new file mode 100644
index 0000000000..edbeb95c31
--- /dev/null
+++ b/browser/base/content/test/general/browser_search_discovery.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// Bug 1588193 - BrowserTestUtils.waitForContentEvent now resolves slightly
+// earlier than before, so it no longer suffices to only wait for a single event
+// tick before checking if browser.engines has been updated. Instead we use a 1s
+// timeout, which may cause the test to take more time.
+requestLongerTimeout(2);
+
+add_task(async function() {
+ let url =
+ "http://mochi.test:8888/browser/browser/base/content/test/general/discovery.html";
+ info("Test search discovery");
+ await BrowserTestUtils.withNewTab(url, searchDiscovery);
+});
+
+let searchDiscoveryTests = [
+ { text: "rel search discovered" },
+ { rel: "SEARCH", text: "rel is case insensitive" },
+ { rel: "-search-", pass: false, text: "rel -search- not discovered" },
+ {
+ rel: "foo bar baz search quux",
+ text: "rel may contain additional rels separated by spaces",
+ },
+ { href: "https://not.mozilla.com", text: "HTTPS ok" },
+ { href: "ftp://not.mozilla.com", text: "FTP ok" },
+ { href: "data:text/foo,foo", pass: false, text: "data URI not permitted" },
+ { href: "javascript:alert(0)", pass: false, text: "JS URI not permitted" },
+ {
+ type: "APPLICATION/OPENSEARCHDESCRIPTION+XML",
+ text: "type is case insensitve",
+ },
+ {
+ type: " application/opensearchdescription+xml ",
+ text: "type may contain extra whitespace",
+ },
+ {
+ type: "application/opensearchdescription+xml; charset=utf-8",
+ text: "type may have optional parameters (RFC2046)",
+ },
+ {
+ type: "aapplication/opensearchdescription+xml",
+ pass: false,
+ text: "type should not be loosely matched",
+ },
+ {
+ rel: "search search search",
+ count: 1,
+ text: "only one engine should be added",
+ },
+];
+
+async function searchDiscovery() {
+ let browser = gBrowser.selectedBrowser;
+
+ for (let testCase of searchDiscoveryTests) {
+ if (testCase.pass == undefined) {
+ testCase.pass = true;
+ }
+ testCase.title = testCase.title || searchDiscoveryTests.indexOf(testCase);
+
+ let promiseLinkAdded = BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "DOMLinkAdded",
+ false,
+ null,
+ true
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [testCase], test => {
+ let doc = content.document;
+ let head = doc.getElementById("linkparent");
+ let link = doc.createElement("link");
+ link.rel = test.rel || "search";
+ link.href = test.href || "http://so.not.here.mozilla.com/search.xml";
+ link.type = test.type || "application/opensearchdescription+xml";
+ link.title = test.title;
+ head.appendChild(link);
+ });
+
+ await promiseLinkAdded;
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ if (browser.engines) {
+ info(`Found ${browser.engines.length} engines`);
+ info(`First engine title: ${browser.engines[0].title}`);
+ let hasEngine = testCase.count
+ ? browser.engines[0].title == testCase.title &&
+ browser.engines.length == testCase.count
+ : browser.engines[0].title == testCase.title;
+ ok(hasEngine, testCase.text);
+ browser.engines = null;
+ } else {
+ ok(!testCase.pass, testCase.text);
+ }
+ }
+
+ info("Test multiple engines with the same title");
+ let promiseLinkAdded = BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "DOMLinkAdded",
+ false,
+ e => e.target.href == "http://second.mozilla.com/search.xml",
+ true
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let doc = content.document;
+ let head = doc.getElementById("linkparent");
+ let link = doc.createElement("link");
+ link.rel = "search";
+ link.href = "http://first.mozilla.com/search.xml";
+ link.type = "application/opensearchdescription+xml";
+ link.title = "Test Engine";
+ let link2 = link.cloneNode(false);
+ link2.href = "http://second.mozilla.com/search.xml";
+ head.appendChild(link);
+ head.appendChild(link2);
+ });
+
+ await promiseLinkAdded;
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ ok(browser.engines, "has engines");
+ is(browser.engines.length, 1, "only one engine");
+ is(
+ browser.engines[0].uri,
+ "http://first.mozilla.com/search.xml",
+ "first engine wins"
+ );
+ browser.engines = null;
+}
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..dbad35fb82
--- /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 =
+ "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(
+ Ci.nsISiteSecurityService.HEADER_HSTS,
+ NetUtil.newURI("http://example.com/"),
+ 0,
+ 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.loadURI(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..10c7aae128
--- /dev/null
+++ b/browser/base/content/test/general/browser_star_hsts.sjs
@@ -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/. */
+
+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..2f88f9bbc4
--- /dev/null
+++ b/browser/base/content/test/general/browser_storagePressure_notification.js
@@ -0,0 +1,160 @@
+/* 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() {
+ let promises = [
+ BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ "about:preferences#privacy"
+ ),
+ TestUtils.topicObserved("privacy-pane-loaded", () => true),
+ ];
+ return Promise.all(promises);
+}
+
+// Test only displaying notification once within the given interval
+add_task(async function() {
+ 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(() =>
+ gHighPriorityNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ )
+ );
+ let notification = gHighPriorityNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ );
+ ok(
+ notification instanceof XULElement,
+ "Should display storage pressure notification"
+ );
+ notification.close();
+
+ await notifyStoragePressure();
+ notification = gHighPriorityNotificationBox.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(() =>
+ gHighPriorityNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ )
+ );
+ notification = gHighPriorityNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ );
+ ok(
+ notification instanceof XULElement,
+ "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() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.storageManager.pressureNotification.minIntervalMS", 0]],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ 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(() =>
+ gHighPriorityNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ )
+ );
+ let notification = gHighPriorityNotificationBox.getNotificationWithValue(
+ "storage-pressure-notification"
+ );
+ ok(
+ notification instanceof XULElement,
+ "Should display storage pressure notification"
+ );
+
+ let prefBtn = notification.getElementsByTagName("button")[1];
+ let aboutPrefPromise = openAboutPrefPromise();
+ prefBtn.doCommand();
+ await aboutPrefPromise;
+ let aboutPrefTab = gBrowser.selectedTab;
+ let prefDoc = 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 TEST_NOTIFICATION_INTERVAL_MS = 0;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.storageManager.pressureNotification.minIntervalMS",
+ TEST_NOTIFICATION_INTERVAL_MS,
+ ],
+ ],
+ });
+
+ await notifyStoragePressure();
+ await notifyStoragePressure();
+ let allNotifications = gHighPriorityNotificationBox.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"
+ );
+ gHighPriorityNotificationBox.removeAllNotifications();
+});
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..8746f01d51
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabDrop.js
@@ -0,0 +1,208 @@
+// TODO (Bug 1680996): Investigate why this test takes a long time.
+requestLongerTimeout(2);
+
+const ANY_URL = undefined;
+
+registerCleanupFunction(async function cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ await Services.search.setDefault(originalEngine);
+ let engine = Services.search.getEngineByName("MozSearch");
+ await Services.search.removeEngine(engine);
+});
+
+let originalEngine;
+add_task(async function test_setup() {
+ // Stop search-engine loads from hitting the network
+ await Services.search.addEngineWithDetails("MozSearch", {
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("MozSearch");
+ originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+});
+
+add_task(async function single_url() {
+ await dropText("mochi.test/first", ["https://www.mochi.test/first"]);
+});
+add_task(async function single_javascript() {
+ await dropText("javascript:'bad'", []);
+});
+add_task(async function single_javascript_capital() {
+ await dropText("jAvascript:'bad'", []);
+});
+add_task(async function single_search() {
+ await dropText("search this", [ANY_URL]);
+});
+add_task(async function single_url2() {
+ await dropText("mochi.test/second", ["https://www.mochi.test/second"]);
+});
+add_task(async function single_data_url() {
+ await dropText("data:text/html,bad", []);
+});
+add_task(async function single_url3() {
+ await dropText("mochi.test/third", ["https://www.mochi.test/third"]);
+});
+
+// Single text/plain item, with multiple links.
+add_task(async function multiple_urls() {
+ await dropText("mochi.test/1\nmochi.test/2", [
+ "https://www.mochi.test/1",
+ "https://www.mochi.test/2",
+ ]);
+});
+add_task(async function multiple_urls_javascript() {
+ await dropText("javascript:'bad1'\nmochi.test/3", []);
+});
+add_task(async function multiple_urls_data() {
+ await dropText("mochi.test/4\ndata:text/html,bad1", []);
+});
+
+// Multiple text/plain items, with single and multiple links.
+add_task(async function multiple_items_single_and_multiple_links() {
+ await drop(
+ [
+ [{ type: "text/plain", data: "mochi.test/5" }],
+ [{ type: "text/plain", data: "mochi.test/6\nmochi.test/7" }],
+ ],
+ [
+ "https://www.mochi.test/5",
+ "https://www.mochi.test/6",
+ "https://www.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",
+ },
+ ],
+ ],
+ ["https://www.mochi.test/8", "https://www.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" },
+ ],
+ ],
+ ["https://www.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"), [
+ "https://www.mochi.test/multi0",
+ "https://www.mochi.test/multi1",
+ "https://www.mochi.test/multi2",
+ "https://www.mochi.test/multi3",
+ "https://www.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"), [
+ "https://www.mochi.test/accept0",
+ "https://www.mochi.test/accept1",
+ "https://www.mochi.test/accept2",
+ "https://www.mochi.test/accept3",
+ "https://www.mochi.test/accept4",
+ ]);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+add_task(async function multiple_tabs_over_max_cancel() {
+ await pushPrefs(["browser.tabs.maxOpenBeforeWarn", 4]);
+
+ let confirmPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+
+ let urls = [];
+ for (let i = 0; i < 5; i++) {
+ urls.push("mochi.test/cancel" + i);
+ }
+ await dropText(urls.join("\n"), []);
+
+ await confirmPromise;
+
+ await popPrefs();
+});
+
+function dropText(text, expectedURLs) {
+ return drop([[{ type: "text/plain", data: text }]], expectedURLs);
+}
+
+async function drop(dragData, expectedURLs) {
+ let dragDataString = JSON.stringify(dragData);
+ info(
+ `Starting test for dragData:${dragDataString}; expectedURLs.length:${expectedURLs.length}`
+ );
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "drop");
+
+ let loadedPromises = expectedURLs.map(url =>
+ BrowserTestUtils.waitForNewTab(gBrowser, url, false, true)
+ );
+
+ // A drop type of "link" onto an existing tab would normally trigger a
+ // load in that same tab, but tabbrowser code in _getDragTargetTab treats
+ // drops on the outer edges of a tab differently (loading a new tab
+ // instead). Make events created by synthesizeDrop have all of their
+ // coordinates set to 0 (screenX/screenY), so they're treated as drops
+ // on the outer edge of the tab, thus they open new tabs.
+ var event = {
+ clientX: 0,
+ clientY: 0,
+ screenX: 0,
+ screenY: 0,
+ };
+ EventUtils.synthesizeDrop(
+ gBrowser.selectedTab,
+ gBrowser.selectedTab,
+ dragData,
+ "link",
+ window,
+ undefined,
+ event
+ );
+
+ let tabs = await Promise.all(loadedPromises);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ await awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_tab_close_dependent_window.js b/browser/base/content/test/general/browser_tab_close_dependent_window.js
new file mode 100644
index 0000000000..a9b9c1d999
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_close_dependent_window.js
@@ -0,0 +1,35 @@
+"use strict";
+
+add_task(async function closing_tab_with_dependents_should_close_window() {
+ info("Opening window");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Opening tab with data URI");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ `data:text/html,<html%20onclick="W=window.open()"><body%20onbeforeunload="W.close()">`
+ );
+ info("Closing original tab in this window.");
+ BrowserTestUtils.removeTab(win.gBrowser.tabs[0]);
+ info("Clicking into the window");
+ let depTabOpened = BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "TabOpen"
+ );
+ await BrowserTestUtils.synthesizeMouse("html", 0, 0, {}, tab.linkedBrowser);
+
+ let openedTab = (await depTabOpened).target;
+ info("Got opened tab");
+
+ let windowClosedPromise = BrowserTestUtils.windowClosed(win);
+ BrowserTestUtils.removeTab(tab);
+ is(
+ Cu.isDeadWrapper(openedTab) || openedTab.linkedBrowser == null,
+ true,
+ "Opened tab should also have closed"
+ );
+ info(
+ "If we timeout now, the window failed to close - that shouldn't happen!"
+ );
+ await windowClosedPromise;
+});
diff --git a/browser/base/content/test/general/browser_tab_detach_restore.js b/browser/base/content/test/general/browser_tab_detach_restore.js
new file mode 100644
index 0000000000..5ea47f4453
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_detach_restore.js
@@ -0,0 +1,53 @@
+"use strict";
+
+const { TabStateFlusher } = ChromeUtils.import(
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+
+add_task(async function() {
+ let uri =
+ "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.loadURI(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..0b8a1caef2
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
@@ -0,0 +1,422 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+const EVENTUTILS_URL =
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js";
+var EventUtils = {};
+
+Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils);
+
+/**
+ * Tests that tabs from Private Browsing windows cannot be dragged
+ * into non-private windows, and vice-versa.
+ */
+add_task(async function test_dragging_private_windows() {
+ let normalWin = await BrowserTestUtils.openNewBrowserWindow();
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let normalTab = await BrowserTestUtils.openNewForegroundTab(
+ normalWin.gBrowser
+ );
+ let privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ normalTab,
+ privateTab,
+ [[{ type: TAB_DROP_TYPE, data: normalTab }]],
+ null,
+ normalWin,
+ privateWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a normal tab to a private window"
+ );
+
+ effect = EventUtils.synthesizeDrop(
+ privateTab,
+ normalTab,
+ [[{ type: TAB_DROP_TYPE, data: privateTab }]],
+ null,
+ privateWin,
+ normalWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a private tab to a normal window"
+ );
+
+ normalWin.gBrowser.swapBrowsersAndCloseOther(normalTab, privateTab);
+ is(
+ normalWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a normal tab to a private tabbrowser"
+ );
+ is(
+ privateWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a normal tab in a private tabbrowser"
+ );
+
+ privateWin.gBrowser.swapBrowsersAndCloseOther(privateTab, normalTab);
+ is(
+ privateWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a private tab to a normal tabbrowser"
+ );
+ is(
+ normalWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a private tab in a normal tabbrowser"
+ );
+
+ await BrowserTestUtils.closeWindow(normalWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+/**
+ * Tests that tabs from e10s windows cannot be dragged into non-e10s
+ * windows, and vice-versa.
+ */
+add_task(async function test_dragging_e10s_windows() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ let remoteWin = await BrowserTestUtils.openNewBrowserWindow({ remote: true });
+ let nonRemoteWin = await BrowserTestUtils.openNewBrowserWindow({
+ remote: false,
+ fission: false,
+ });
+
+ let remoteTab = await BrowserTestUtils.openNewForegroundTab(
+ remoteWin.gBrowser
+ );
+ let nonRemoteTab = await BrowserTestUtils.openNewForegroundTab(
+ nonRemoteWin.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ remoteTab,
+ nonRemoteTab,
+ [[{ type: TAB_DROP_TYPE, data: remoteTab }]],
+ null,
+ remoteWin,
+ nonRemoteWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a remote tab to a non-e10s window"
+ );
+
+ effect = EventUtils.synthesizeDrop(
+ nonRemoteTab,
+ remoteTab,
+ [[{ type: TAB_DROP_TYPE, data: nonRemoteTab }]],
+ null,
+ nonRemoteWin,
+ remoteWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a non-remote tab to an e10s window"
+ );
+
+ remoteWin.gBrowser.swapBrowsersAndCloseOther(remoteTab, nonRemoteTab);
+ is(
+ remoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a normal tab to a private tabbrowser"
+ );
+ is(
+ nonRemoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a normal tab in a private tabbrowser"
+ );
+
+ nonRemoteWin.gBrowser.swapBrowsersAndCloseOther(nonRemoteTab, remoteTab);
+ is(
+ nonRemoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a private tab to a normal tabbrowser"
+ );
+ is(
+ remoteWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a private tab in a normal tabbrowser"
+ );
+
+ await BrowserTestUtils.closeWindow(remoteWin);
+ await BrowserTestUtils.closeWindow(nonRemoteWin);
+});
+
+/**
+ * Tests that tabs from fission windows cannot be dragged into non-fission
+ * windows, and vice-versa.
+ */
+add_task(async function test_dragging_fission_windows() {
+ let fissionWin = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ fission: true,
+ });
+ let nonFissionWin = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ fission: false,
+ });
+
+ let fissionTab = await BrowserTestUtils.openNewForegroundTab(
+ fissionWin.gBrowser
+ );
+ let nonFissionTab = await BrowserTestUtils.openNewForegroundTab(
+ nonFissionWin.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ fissionTab,
+ nonFissionTab,
+ [[{ type: TAB_DROP_TYPE, data: fissionTab }]],
+ null,
+ fissionWin,
+ nonFissionWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a fission tab to a non-fission window"
+ );
+
+ effect = EventUtils.synthesizeDrop(
+ nonFissionTab,
+ fissionTab,
+ [[{ type: TAB_DROP_TYPE, data: nonFissionTab }]],
+ null,
+ nonFissionWin,
+ fissionWin
+ );
+ is(
+ effect,
+ "none",
+ "Should not be able to drag a non-fission tab to an fission window"
+ );
+
+ let swapOk = fissionWin.gBrowser.swapBrowsersAndCloseOther(
+ fissionTab,
+ nonFissionTab
+ );
+ is(
+ swapOk,
+ false,
+ "Returns false swapping fission tab to a non-fission tabbrowser"
+ );
+ is(
+ fissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a fission tab to a non-fission tabbrowser"
+ );
+ is(
+ nonFissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a fission tab in a non-fission tabbrowser"
+ );
+
+ swapOk = nonFissionWin.gBrowser.swapBrowsersAndCloseOther(
+ nonFissionTab,
+ fissionTab
+ );
+ is(
+ swapOk,
+ false,
+ "Returns false swapping non-fission tab to a fission tabbrowser"
+ );
+ is(
+ nonFissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent moving a non-fission tab to a fission tabbrowser"
+ );
+ is(
+ fissionWin.gBrowser.tabs.length,
+ 2,
+ "Prevent accepting a non-fission tab in a fission tabbrowser"
+ );
+
+ await BrowserTestUtils.closeWindow(fissionWin);
+ await BrowserTestUtils.closeWindow(nonFissionWin);
+});
+
+/**
+ * Tests that remoteness-blacklisted tabs from e10s windows can
+ * be dragged between e10s windows.
+ */
+add_task(async function test_dragging_blacklisted() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ let remoteWin1 = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ });
+ remoteWin1.gBrowser.myID = "remoteWin1";
+ let remoteWin2 = await BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ });
+ remoteWin2.gBrowser.myID = "remoteWin2";
+
+ // Anything under chrome://mochitests/content/ will be blacklisted, and
+ // open in the parent process.
+ const BLACKLISTED_URL =
+ getRootDirectory(gTestPath) + "browser_tab_drag_drop_perwindow.js";
+ let blacklistedTab = await BrowserTestUtils.openNewForegroundTab(
+ remoteWin1.gBrowser,
+ BLACKLISTED_URL
+ );
+
+ ok(blacklistedTab.linkedBrowser, "Newly created tab should have a browser.");
+
+ ok(
+ !blacklistedTab.linkedBrowser.isRemoteBrowser,
+ `Expected a non-remote browser for URL: ${BLACKLISTED_URL}`
+ );
+
+ let otherTab = await BrowserTestUtils.openNewForegroundTab(
+ remoteWin2.gBrowser
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ blacklistedTab,
+ otherTab,
+ [[{ type: TAB_DROP_TYPE, data: blacklistedTab }]],
+ null,
+ remoteWin1,
+ remoteWin2
+ );
+ is(effect, "move", "Should be able to drag the blacklisted tab.");
+
+ // The synthesized drop should also do the work of swapping the
+ // browsers, so no need to call swapBrowsersAndCloseOther manually.
+
+ is(
+ remoteWin1.gBrowser.tabs.length,
+ 1,
+ "Should have moved the blacklisted tab out of this window."
+ );
+ is(
+ remoteWin2.gBrowser.tabs.length,
+ 3,
+ "Should have inserted the blacklisted tab into the other window."
+ );
+
+ // The currently selected tab in the second window should be the
+ // one we just dragged in.
+ let draggedBrowser = remoteWin2.gBrowser.selectedBrowser;
+ ok(
+ !draggedBrowser.isRemoteBrowser,
+ "The browser we just dragged in should not be remote."
+ );
+
+ is(
+ draggedBrowser.currentURI.spec,
+ BLACKLISTED_URL,
+ `Expected the URL of the dragged in tab to be ${BLACKLISTED_URL}`
+ );
+
+ await BrowserTestUtils.closeWindow(remoteWin1);
+ await BrowserTestUtils.closeWindow(remoteWin2);
+});
+
+/**
+ * Tests that tabs dragged between windows dispatch TabOpen and TabClose
+ * events with the appropriate adoption details.
+ */
+add_task(async function test_dragging_adoption_events() {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser);
+
+ let awaitCloseEvent = BrowserTestUtils.waitForEvent(tab1, "TabClose");
+ let awaitOpenEvent = BrowserTestUtils.waitForEvent(win2, "TabOpen");
+
+ let effect = EventUtils.synthesizeDrop(
+ tab1,
+ tab2,
+ [[{ type: TAB_DROP_TYPE, data: tab1 }]],
+ null,
+ win1,
+ win2
+ );
+ is(effect, "move", "Tab should be moved from win1 to win2.");
+
+ let closeEvent = await awaitCloseEvent;
+ let openEvent = await awaitOpenEvent;
+
+ is(openEvent.detail.adoptedTab, tab1, "New tab adopted old tab");
+ is(
+ closeEvent.detail.adoptedBy,
+ openEvent.target,
+ "Old tab adopted by new tab"
+ );
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
+
+/**
+ * Tests that per-site zoom settings remain active after a tab is
+ * dragged between windows.
+ */
+add_task(async function test_dragging_zoom_handling() {
+ const ZOOM_FACTOR = 1.62;
+
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ "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..a26a8f355c
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop.js
@@ -0,0 +1,258 @@
+// 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.loadURI(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() {
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+
+ // 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],
+ "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..b8cee947a2
--- /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..ac1c8ade83
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop2_frame1.xhtml
@@ -0,0 +1,169 @@
+<?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.width = 120;
+ button.height = 40;
+ button.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;");
+ 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();
+
+ var gotMouseEvent = false;
+ function mouseMoved(event)
+ {
+ // eslint-disable-next-line no-unused-vars
+ gotMouseEvent = true;
+ }
+
+ panel.addEventListener("mousemove", mouseMoved, true);
+ synthesizeMouse(panel, 10, 10, { type: "mousemove" });
+ panel.removeEventListener("mousemove", mouseMoved, true);
+
+ 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..49ba54f946
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabfocus.js
@@ -0,0 +1,817 @@
+/*
+ * This test checks that focus is adjusted properly when switching tabs.
+ */
+
+/* eslint-env mozilla/frame-script */
+
+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 => {
+ const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+ );
+
+ 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 (event.target instanceof Window) {
+ id = getWindowDocId(event.originalTarget) + "-window";
+ } else if (event.target instanceof Document) {
+ 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..9acb762a69
--- /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.recentlyUsedOrder", 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..bde31b9e8e
--- /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..3d5ef807c5
--- /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 =
+ "http://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..49966aa2d6
--- /dev/null
+++ b/browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js
@@ -0,0 +1,148 @@
+/* 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";
+
+const { PrivateBrowsingUtils } = ChromeUtils.import(
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+
+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.loadURI(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..41f24e2dbb
--- /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.loadURI(
+ 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..3e8f0d97fa
--- /dev/null
+++ b/browser/base/content/test/general/browser_unknownContentType_title.js
@@ -0,0 +1,38 @@
+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 =
+ "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_task(async function() {
+ 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.loadURI(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();
+});
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..8848066507
--- /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..ad86eb17a3
--- /dev/null
+++ b/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js
@@ -0,0 +1,59 @@
+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,
+ 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..d8b6646452
--- /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..c9a55b5b98
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 visible = gBrowser.visibleTabs;
+ is(visible.length, 3, "3 tabs should be open");
+ 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 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, 3, "3 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");
+
+ // Try showing all tabs
+ gBrowser.showOnlyTheseTabs(Array.from(gBrowser.tabs));
+ 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, 2, "still have 2 open tabs");
+
+ // 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..b3e044b9f8
--- /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.recentlyUsedOrder", 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..4717c820c8
--- /dev/null
+++ b/browser/base/content/test/general/browser_windowactivation.js
@@ -0,0 +1,113 @@
+/*
+ * This test checks that window activation state is set properly with multiple tabs.
+ */
+
+/* eslint-env mozilla/frame-script */
+
+const testPageChrome =
+ getRootDirectory(gTestPath) + "file_window_activation.html";
+const testPageHttp = testPageChrome.replace(
+ "chrome://mochitests/content",
+ "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..643ebbdcfa
--- /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/bug592338.html b/browser/base/content/test/general/bug592338.html
new file mode 100644
index 0000000000..c59d7ec1fd
--- /dev/null
+++ b/browser/base/content/test/general/bug592338.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+<script type="text/javascript">
+var theme = {
+ id: "test",
+ name: "Test Background",
+ headerURL: "http://example.com/firefox/personas/01/header.jpg",
+ textcolor: "#fff",
+ accentcolor: "#6b6b6b",
+};
+
+function setTheme(node) {
+ node.setAttribute("data-browsertheme", JSON.stringify(theme));
+ var event = document.createEvent("Events");
+ event.initEvent("InstallBrowserTheme", true, false);
+ node.dispatchEvent(event);
+}
+</script>
+</head>
+<body>
+<a id="theme-install" href="#" onclick="setTheme(this)">Install</a>
+</body>
+</html>
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..91e5aa23fe
--- /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..46a5a277a7
--- /dev/null
+++ b/browser/base/content/test/general/clipboard_pastefile.html
@@ -0,0 +1,35 @@
+<html><body>
+<script>
+function checkPaste(event) {
+ let output = document.getElementById("output");
+ output.textContent = checkPasteHelper(event);
+}
+
+function checkPasteHelper(event) {
+ let dt = event.clipboardData;
+ if (dt.types.length != 2)
+ return "Wrong number of types; got " + dt.types.length;
+
+ for (let type of dt.types) {
+ if (type != "Files" && type != "application/x-moz-file")
+ return "Invalid type for types; got" + type;
+ }
+
+ for (let type of dt.mozTypesAt(0)) {
+ if (type != "Files" && type != "application/x-moz-file")
+ return "Invalid type for mozTypesAt; got" + type;
+ }
+
+ if (dt.getData("text/plain"))
+ return "text/plain found with getData";
+ if (dt.mozGetDataAt("text/plain", 0))
+ return "text/plain found with mozGetDataAt";
+
+ return "Passed";
+}
+</script>
+
+<input id="input" onpaste="checkPaste(event)">
+<div id="output"></div>
+
+</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/discovery.html b/browser/base/content/test/general/discovery.html
new file mode 100644
index 0000000000..1679e6545e
--- /dev/null
+++ b/browser/base/content/test/general/discovery.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head id="linkparent">
+ <title>Autodiscovery Test</title>
+ </head>
+ <body>
+ </body>
+</html>
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..0583ddb061
--- /dev/null
+++ b/browser/base/content/test/general/download_page.html
@@ -0,0 +1,56 @@
+<!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>
+ </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..d8398282f3
--- /dev/null
+++ b/browser/base/content/test/general/download_with_content_disposition_header.sjs
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/gZipOfflineChild_uncompressed.html b/browser/base/content/test/general/gZipOfflineChild_uncompressed.html
new file mode 100644
index 0000000000..904cc5d9a5
--- /dev/null
+++ b/browser/base/content/test/general/gZipOfflineChild_uncompressed.html
@@ -0,0 +1,21 @@
+<html manifest="gZipOfflineChild.cacheManifest">
+<head>
+ <!-- This file is gzipped to create gZipOfflineChild.html -->
+<title></title>
+<script type="text/javascript">
+
+function finish(success) {
+ window.parent.postMessage(success, "*");
+}
+
+applicationCache.oncached = function() { finish("oncache"); };
+applicationCache.onnoupdate = function() { finish("onupdate"); };
+applicationCache.onerror = function() { finish("onerror"); };
+
+</script>
+</head>
+
+<body>
+<h1>Child</h1>
+</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..0c2dd778fa
--- /dev/null
+++ b/browser/base/content/test/general/head.js
@@ -0,0 +1,407 @@
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AboutNewTab",
+ "resource:///modules/AboutNewTab.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "BrowserTestUtils",
+ "resource://testing-common/BrowserTestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "TabCrashHandler",
+ "resource:///modules/ContentCrashHandlers.jsm"
+);
+
+/**
+ * Wait for a <notification> to be closed then call the specified callback.
+ */
+function waitForNotificationClose(notification, cb) {
+ let observer = new MutationObserver(function onMutatations(mutations) {
+ for (let mutation of mutations) {
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
+ let node = mutation.removedNodes.item(i);
+ if (node != notification) {
+ continue;
+ }
+ observer.disconnect();
+ cb();
+ }
+ }
+ });
+ observer.observe(notification.control.stack, { childList: true });
+}
+
+function closeAllNotifications() {
+ if (!gNotificationBox.currentNotification) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ for (let notification of gNotificationBox.allNotifications) {
+ waitForNotificationClose(notification, function() {
+ if (gNotificationBox.allNotifications.length === 0) {
+ resolve();
+ }
+ });
+ notification.close();
+ }
+ });
+}
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ }
+ }, "browser-delayed-startup-finished");
+}
+
+function openToolbarCustomizationUI(aCallback, aBrowserWin) {
+ if (!aBrowserWin) {
+ aBrowserWin = window;
+ }
+
+ aBrowserWin.gCustomizeMode.enter();
+
+ aBrowserWin.gNavToolbox.addEventListener(
+ "customizationready",
+ function() {
+ executeSoon(function() {
+ aCallback(aBrowserWin);
+ });
+ },
+ { once: true }
+ );
+}
+
+function closeToolbarCustomizationUI(aCallback, aBrowserWin) {
+ aBrowserWin.gNavToolbox.addEventListener(
+ "aftercustomization",
+ function() {
+ executeSoon(aCallback);
+ },
+ { once: true }
+ );
+
+ aBrowserWin.gCustomizeMode.exit();
+}
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+ retryTimes = typeof retryTimes !== "undefined" ? retryTimes : 30;
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= retryTimes) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function() {
+ clearInterval(interval);
+ nextTest();
+ };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+ return new Promise(resolve => {
+ waitForCondition(aConditionFn, resolve, "Condition didn't pass.");
+ });
+}
+
+function promiseWaitForEvent(
+ object,
+ eventName,
+ capturing = false,
+ chrome = false
+) {
+ return new Promise(resolve => {
+ function listener(event) {
+ info("Saw " + eventName);
+ object.removeEventListener(eventName, listener, capturing, chrome);
+ resolve(event);
+ }
+
+ info("Waiting for " + eventName);
+ object.addEventListener(eventName, listener, capturing, chrome);
+ });
+}
+
+/**
+ * Allows setting focus on a window, and waiting for that window to achieve
+ * focus.
+ *
+ * @param aWindow
+ * The window to focus and wait for.
+ *
+ * @return {Promise}
+ * @resolves When the window is focused.
+ * @rejects Never.
+ */
+function promiseWaitForFocus(aWindow) {
+ return new Promise(resolve => {
+ waitForFocus(resolve, aWindow);
+ });
+}
+
+function getTestPlugin(aName) {
+ var pluginName = aName || "Test Plug-in";
+ var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ var tags = ph.getPluginTags();
+
+ // Find the test plugin
+ for (var i = 0; i < tags.length; i++) {
+ if (tags[i].name == pluginName) {
+ return tags[i];
+ }
+ }
+ ok(false, "Unable to find plugin");
+ return null;
+}
+
+// call this to set the test plugin(s) initially expected enabled state.
+// it will automatically be reset to it's previous value after the test
+// ends
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+ var plugin = getTestPlugin(pluginName);
+ var oldEnabledState = plugin.enabledState;
+ plugin.enabledState = newEnabledState;
+ SimpleTest.registerCleanupFunction(function() {
+ getTestPlugin(pluginName).enabledState = oldEnabledState;
+ });
+}
+
+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.loadURI(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 (style.display == "-moz-popup") {
+ return ["hiding", "closed"].includes(element.state);
+ }
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument) {
+ return is_hidden(element.parentNode);
+ }
+
+ return false;
+}
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(BrowserTestUtils.is_visible(element), msg || "Element should be visible");
+}
+
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(element), msg || "Element should be hidden");
+}
+
+function promisePopupShown(popup) {
+ return BrowserTestUtils.waitForPopupEvent(popup, "shown");
+}
+
+function promisePopupHidden(popup) {
+ return BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = promisePopupShown(win.PopupNotifications.panel);
+ notification.reshow();
+ return panelPromise;
+}
+
+/**
+ * Resolves when a bookmark with the given uri is added.
+ */
+function promiseOnBookmarkItemAdded(aExpectedURI) {
+ return new Promise((resolve, reject) => {
+ let listener = events => {
+ is(events.length, 1, "Should only receive one event.");
+ info("Added a bookmark to " + events[0].url);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ if (events[0].url == aExpectedURI.spec) {
+ resolve();
+ } else {
+ reject(new Error("Added an unexpected bookmark"));
+ }
+ };
+ info("Waiting for a bookmark to be added");
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ });
+}
+
+async function loadBadCertPage(url) {
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ await loaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
+ content.document.getElementById("exceptionDialogButton").click();
+ });
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+}
+
+/**
+ * Waits for the stylesheets to be loaded into the browser menu.
+ *
+ * @param tab
+ * The tab that contains the webpage we're testing.
+ * @param styleSheetCount
+ * How many stylesheets we expect to be loaded.
+ * @return Promise
+ */
+async function promiseStylesheetsLoaded(tab, styleSheetCount) {
+ let styleMenu = tab.ownerGlobal.gPageStyleMenu;
+ let permanentKey = tab.permanentKey;
+
+ await TestUtils.waitForCondition(() => {
+ let menu = styleMenu._pageStyleSheets.get(permanentKey);
+ info(`waiting for sheets: ${menu && menu.filteredStyleSheets.length}`);
+ return menu && menu.filteredStyleSheets.length >= styleSheetCount;
+ }, "waiting for style sheets to load");
+}
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/page_style_only_alternates.html b/browser/base/content/test/general/page_style_only_alternates.html
new file mode 100644
index 0000000000..b5f4a8181c
--- /dev/null
+++ b/browser/base/content/test/general/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/general/page_style_sample.html b/browser/base/content/test/general/page_style_sample.html
new file mode 100644
index 0000000000..0447b6b07a
--- /dev/null
+++ b/browser/base/content/test/general/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="404.css" title="1" rel="alternate stylesheet">
+ <link data-state="0" title="2" rel="alternate stylesheet">
+ <link data-state="0" href="404.css" rel="alternate stylesheet">
+ <link data-state="0" href="404.css" title="" rel="alternate stylesheet">
+ <link data-state="1" href="404.css" title="3" rel="stylesheet alternate">
+ <link data-state="1" href="404.css" title="4" rel=" alternate stylesheet ">
+ <link data-state="1" href="404.css" title="5" rel="alternate stylesheet">
+ <link data-state="2" href="404.css" title="6" rel="stylesheet">
+ <link data-state="1" href="404.css" title="7" rel="foo stylesheet">
+ <link data-state="0" href="404.css" title="8" rel="alternate">
+ <link data-state="1" href="404.css" title="9" rel="alternate STYLEsheet">
+ <link data-state="1" href="404.css" title="10" rel="alternate stylesheet" media="">
+ <link data-state="1" href="404.css" title="11" rel="alternate stylesheet" media="all">
+ <link data-state="1" href="404.css" title="12" rel="alternate stylesheet" media="ALL ">
+ <link data-state="1" href="404.css" title="13" rel="alternate stylesheet" media="screen">
+ <link data-state="1" href="404.css" title="14" rel="alternate stylesheet" media=" Screen">
+ <link data-state="0" href="404.css" title="15" rel="alternate stylesheet" media="screen foo">
+ <link data-state="0" href="404.css" title="16" rel="alternate stylesheet" media="all screen">
+ <link data-state="0" href="404.css" title="17" rel="alternate stylesheet" media="foo bar">
+ <link data-state="1" href="404.css" title="18" rel="alternate stylesheet" media="all,screen">
+ <link data-state="1" href="404.css" title="19" rel="alternate stylesheet" media="all, screen">
+ <link data-state="0" href="404.css" title="20" rel="alternate stylesheet" media="all screen">
+ <link data-state="0" href="404.css" title="21" rel="alternate stylesheet" media="foo">
+ <link data-state="0" href="404.css" title="22" rel="alternate stylesheet" media="allscreen">
+ <link data-state="0" href="404.css" title="23" rel="alternate stylesheet" media="_all">
+ <link data-state="0" href="404.css" title="24" rel="alternate stylesheet" media="not screen">
+ <link data-state="1" href="404.css" title="25" rel="alternate stylesheet" media="only screen">
+ <link data-state="1" href="404.css" title="26" rel="alternate stylesheet" media="screen and (min-device-width: 1px)">
+ <link data-state="0" href="404.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="404.css" title="29" rel="alternate stylesheet" disabled>
+ </head>
+ <body></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..4175a24805
--- /dev/null
+++ b/browser/base/content/test/general/print_postdata.sjs
@@ -0,0 +1,22 @@
+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/refresh_header.sjs b/browser/base/content/test/general/refresh_header.sjs
new file mode 100644
index 0000000000..327372f9b3
--- /dev/null
+++ b/browser/base/content/test/general/refresh_header.sjs
@@ -0,0 +1,24 @@
+/**
+ * Will cause an auto-refresh to the URL provided in the query string
+ * after some delay using the refresh HTTP header.
+ *
+ * Expects the query string to be in the format:
+ *
+ * ?p=[URL of the page to redirect to]&d=[delay]
+ *
+ * Example:
+ *
+ * ?p=http%3A%2F%2Fexample.org%2Fbrowser%2Fbrowser%2Fbase%2Fcontent%2Ftest%2Fgeneral%2Frefresh_meta.sjs&d=200
+ */
+function handleRequest(request, response) {
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let page = query.get("p");
+ let delay = query.get("d");
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "200", "Found");
+ response.setHeader("refresh", `${delay}; url=${page}`);
+ response.write("OK");
+} \ No newline at end of file
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..648fac1a3d
--- /dev/null
+++ b/browser/base/content/test/general/refresh_meta.sjs
@@ -0,0 +1,36 @@
+/**
+ * Will cause an auto-refresh to the URL provided in the query string
+ * after some delay using a <meta> tag.
+ *
+ * Expects the query string to be in the format:
+ *
+ * ?p=[URL of the page to redirect to]&d=[delay]
+ *
+ * Example:
+ *
+ * ?p=http%3A%2F%2Fexample.org%2Fbrowser%2Fbrowser%2Fbase%2Fcontent%2Ftest%2Fgeneral%2Frefresh_meta.sjs&d=200
+ */
+function handleRequest(request, response) {
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let page = query.get("p");
+ let delay = query.get("d");
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <META http-equiv='refresh' content='${delay}; url=${page}'>
+ <title>Gonna refresh you, folks.</title>
+ </head>
+ <body>
+ <h1>Wait for it...</h1>
+ </body>
+ </html>`;
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "200", "Found");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(html);
+} \ No newline at end of file
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/historySwipeAnimation/.eslintrc.js b/browser/base/content/test/historySwipeAnimation/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/historySwipeAnimation/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/historySwipeAnimation/browser.ini b/browser/base/content/test/historySwipeAnimation/browser.ini
new file mode 100644
index 0000000000..c9fb8cd246
--- /dev/null
+++ b/browser/base/content/test/historySwipeAnimation/browser.ini
@@ -0,0 +1 @@
+[browser_historySwipeAnimation.js]
diff --git a/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js b/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js
new file mode 100644
index 0000000000..e97e379327
--- /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/.eslintrc.js b/browser/base/content/test/keyboard/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/keyboard/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/keyboard/browser.ini b/browser/base/content/test/keyboard/browser.ini
new file mode 100644
index 0000000000..355c67e43d
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_bookmarks_shortcut.js]
+[browser_popup_keyNav.js]
+support-files = focusableContent.html
+[browser_toolbarButtonKeyPress.js]
+skip-if = os == "linux" #Bug 1532501
+[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..3a8b050c26
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_bookmarks_shortcut.js
@@ -0,0 +1,144 @@
+/* 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() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.2h2020", true]],
+ });
+
+ 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 BrowserTestUtils.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");
+ EventUtils.synthesizeMouseAtCenter(bookmarksToolbarMenu, {});
+ 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_popup_keyNav.js b/browser/base/content/test/keyboard/browser_popup_keyNav.js
new file mode 100644
index 0000000000..8c3165c0cd
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_popup_keyNav.js
@@ -0,0 +1,49 @@
+/* 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",
+ "http://example.com"
+);
+
+/**
+ * Keyboard navigation has some edgecases in popups because
+ * there is no tabstrip or menubar. Check that tabbing forward
+ * and backward to/from the content document works:
+ */
+add_task(async function test_popup_keynav() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.toolbars.keyboard_navigation", true],
+ ["accessibility.tabfocus", 7],
+ ],
+ });
+
+ const kURL = TEST_PATH + "focusableContent.html";
+ await BrowserTestUtils.withNewTab(kURL, async browser => {
+ let windowPromise = BrowserTestUtils.waitForNewWindow({
+ url: kURL,
+ });
+ SpecialPowers.spawn(browser, [], () => {
+ content.window.open(
+ content.location.href,
+ "_blank",
+ "height=500,width=500,menubar=no,toolbar=no,status=1,resizable=1"
+ );
+ });
+ let win = await windowPromise;
+ let hamburgerButton = win.document.getElementById("PanelUI-menu-button");
+ forceFocus(hamburgerButton);
+ await expectFocusAfterKey("Tab", win.gBrowser.selectedBrowser, false, win);
+ // Focus the button inside the webpage.
+ EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ // Focus the first item in the URL bar
+ let firstButton = win.document
+ .getElementById("urlbar-container")
+ .querySelector("toolbarbutton,[role=button]");
+ await expectFocusAfterKey("Tab", firstButton, false, win);
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
diff --git a/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js b/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js
new file mode 100644
index 0000000000..95431b43dc
--- /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";
+
+/**
+ * Test the behavior of key presses on various toolbar buttons.
+ */
+
+function waitForLocationChange() {
+ let promise = new Promise(resolve => {
+ let wpl = {
+ onLocationChange(aWebProgress, aRequest, aLocation) {
+ gBrowser.removeProgressListener(wpl);
+ resolve();
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+ });
+ return promise;
+}
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.keyboard_navigation", true]],
+ });
+});
+
+// Test activation of the app menu button from the keyboard.
+// The app menu should appear and focus should move inside it.
+add_task(async function testAppMenuButtonPress() {
+ let button = document.getElementById("PanelUI-menu-button");
+ forceFocus(button);
+ let focused = BrowserTestUtils.waitForEvent(
+ window.PanelUI.mainView,
+ "focus",
+ true
+ );
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(true, "Focus inside app menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(
+ window.PanelUI.panel,
+ "popuphidden"
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ await hidden;
+});
+
+// Test that the app menu doesn't open when a key other than space or enter is
+// pressed .
+add_task(async function testAppMenuButtonWrongKey() {
+ let button = document.getElementById("PanelUI-menu-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey("KEY_Tab");
+ await TestUtils.waitForTick();
+ is(window.PanelUI.panel.state, "closed", "App menu is closed after tab");
+});
+
+// Test activation of the Library button from the keyboard.
+// The Library menu should appear and focus should move inside it.
+add_task(async function testLibraryButtonPress() {
+ let button = document.getElementById("library-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById("appMenu-libraryView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focused;
+ ok(true, "Focus inside Library menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+});
+
+// Test activation of the Developer button from the keyboard.
+// This is a customizable widget of type "view".
+// The Developer menu should appear and focus should move inside it.
+add_task(async function testDeveloperButtonPress() {
+ CustomizableUI.addWidgetToArea(
+ "developer-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ let button = document.getElementById("developer-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById("PanelUI-developer");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focused;
+ ok(true, "Focus inside Developer menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ CustomizableUI.reset();
+});
+
+// Test that the Developer menu doesn't open when a key other than space or
+// enter is pressed .
+add_task(async function testDeveloperButtonWrongKey() {
+ CustomizableUI.addWidgetToArea(
+ "developer-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ let button = document.getElementById("developer-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey("KEY_Tab");
+ await TestUtils.waitForTick();
+ let panel = document.getElementById("PanelUI-developer").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() {
+ await BrowserTestUtils.withNewTab("https://example.com", async function() {
+ let button = document.getElementById("pageActionButton");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById("pageActionPanelMainView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ await focused;
+ ok(true, "Focus inside Page Actions menu after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ });
+});
+
+// Test activation of the Back and Forward buttons from the keyboard.
+add_task(async function testBackForwardButtonPress() {
+ await BrowserTestUtils.withNewTab("https://example.com/1", async function(
+ aBrowser
+ ) {
+ BrowserTestUtils.loadURI(aBrowser, "https://example.com/2");
+
+ await BrowserTestUtils.browserLoaded(aBrowser);
+ let backButton = document.getElementById("back-button");
+ forceFocus(backButton);
+ let onLocationChange = waitForLocationChange();
+ EventUtils.synthesizeKey(" ");
+ await onLocationChange;
+ ok(true, "Location changed after back button pressed");
+
+ let forwardButton = document.getElementById("forward-button");
+ forceFocus(forwardButton);
+ onLocationChange = waitForLocationChange();
+ EventUtils.synthesizeKey(" ");
+ await onLocationChange;
+ ok(true, "Location changed after forward button pressed");
+ });
+});
+
+// Test activation of the Send Tab to Device button from the keyboard.
+// This is a page action button built at runtime by PageActions.
+// The Send Tab to Device menu should appear and focus should move inside it.
+add_task(async function testSendTabToDeviceButtonPress() {
+ await BrowserTestUtils.withNewTab("https://example.com", async function() {
+ PageActions.actionForID("sendToDevice").pinnedToUrlbar = true;
+ let button = document.getElementById("pageAction-urlbar-sendToDevice");
+ forceFocus(button);
+ let mainPopupSet = document.getElementById("mainPopupSet");
+ let focused = BrowserTestUtils.waitForEvent(mainPopupSet, "focus", true);
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ let view = document.getElementById(
+ "pageAction-urlbar-sendToDevice-subview"
+ );
+ ok(
+ view.contains(document.activeElement),
+ "Focus inside Page Actions menu after toolbar button pressed"
+ );
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+ PageActions.actionForID("sendToDevice").pinnedToUrlbar = false;
+ });
+});
+
+// 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", async function(
+ aBrowser
+ ) {
+ let button = document.getElementById("reload-button");
+ await TestUtils.waitForCondition(() => !button.disabled);
+ forceFocus(button);
+ let loaded = BrowserTestUtils.browserLoaded(aBrowser);
+ EventUtils.synthesizeKey(" ");
+ 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() {
+ let button = document.getElementById("sidebar-button");
+ ok(!button.checked, "Sidebars button not checked at start of test");
+ let sidebarBox = document.getElementById("sidebar-box");
+ ok(sidebarBox.hidden, "Sidebar hidden at start of test");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ await TestUtils.waitForCondition(() => button.checked);
+ ok(true, "Sidebars button checked after press");
+ ok(!sidebarBox.hidden, "Sidebar visible after press");
+ // Make sure the sidebar is fully loaded before we hide it.
+ // Otherwise, the unload event might call JS which isn't loaded yet.
+ // We can't use BrowserTestUtils.browserLoaded because it fails on non-tab
+ // docs. Instead, wait for something in the JS script.
+ let sidebarWin = document.getElementById("sidebar").contentWindow;
+ await TestUtils.waitForCondition(() => sidebarWin.PlacesUIUtils);
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ await TestUtils.waitForCondition(() => !button.checked);
+ ok(true, "Sidebars button not checked after press");
+ ok(sidebarBox.hidden, "Sidebar hidden after press");
+});
+
+// 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");
+ forceFocus(button);
+ StarUI._createPanelIfNeeded();
+ let panel = document.getElementById("editBookmarkPanel");
+ let focused = BrowserTestUtils.waitForEvent(panel, "focus", true);
+ // The button ignores activation while the bookmarked status is being
+ // updated. So, wait for it to finish updating.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING
+ );
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(true, "Focus inside edit bookmark panel after Bookmark button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await hidden;
+ });
+});
+
+// Test activation of the Bookmarks Menu button from the keyboard.
+// This is a button with type="menu".
+// The Bookmarks Menu should appear.
+add_task(async function testBookmarksmenuButtonPress() {
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ let button = document.getElementById("bookmarks-menu-button");
+ forceFocus(button);
+ let menu = document.getElementById("BMB_bookmarksPopup");
+ let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeKey(" ");
+ await shown;
+ ok(true, "Bookmarks Menu shown after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hidden;
+ CustomizableUI.reset();
+});
+
+// Test activation of the overflow button from the keyboard.
+// The overflow menu should appear and focus should move inside it.
+add_task(async function testOverflowButtonPress() {
+ // Move something to the overflow menu to make the button appear.
+ CustomizableUI.addWidgetToArea(
+ "developer-button",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+ let button = document.getElementById("nav-bar-overflow-button");
+ forceFocus(button);
+ let view = document.getElementById("widget-overflow-mainView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(true, "Focus inside overflow menu after toolbar button pressed");
+ let panel = document.getElementById("widget-overflow");
+ let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await hidden;
+ CustomizableUI.reset();
+});
+
+// Test activation of the Downloads button from the keyboard.
+// The Downloads panel should appear and focus should move inside it.
+add_task(async function testDownloadsButtonPress() {
+ DownloadsButton.unhide();
+ let button = document.getElementById("downloads-button");
+ forceFocus(button);
+ let panel = document.getElementById("downloadsPanel");
+ let focused = BrowserTestUtils.waitForEvent(panel, "focus", true);
+ EventUtils.synthesizeKey(" ");
+ await focused;
+ ok(true, "Focus inside Downloads panel after toolbar button pressed");
+ let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await hidden;
+ DownloadsButton.hide();
+});
+
+// Test activation of the Save to Pocket button from the keyboard.
+// This is a page action button which shows an iframe (wantsIframe: true).
+// The Pocket iframe 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("pocket-button");
+ forceFocus(button);
+ // The panel is created on the fly, so we can't simply wait for focus
+ // inside it.
+ let showing = BrowserTestUtils.waitForEvent(document, "popupshowing", true);
+ EventUtils.synthesizeKey(" ");
+ let event = await showing;
+ let panel = event.target;
+ is(panel.id, "pageActionActivatedActionPanel");
+ let focused = BrowserTestUtils.waitForEvent(panel, "focus", true);
+ await focused;
+ is(
+ document.activeElement.tagName,
+ "iframe",
+ "Focus inside Pocket iframe 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..580a92f740
--- /dev/null
+++ b/browser/base/content/test/keyboard/browser_toolbarKeyNav.js
@@ -0,0 +1,431 @@
+/* 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";
+
+// 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 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();
+ });
+}
+
+const BOOKMARKS_COUNT = 100;
+
+add_task(async function setup() {
+ 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) {
+ bookmarks[i] = { url: `http://test.places.${i}/` };
+ }
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarks,
+ });
+});
+
+// Test tab stops with no page loaded.
+add_task(async function testTabStopsNoPage() {
+ await withNewBlankTab(async function() {
+ startFromUrlBar();
+ await expectFocusAfterKey("Shift+Tab", "home-button");
+ await expectFocusAfterKey("Shift+Tab", "tabbrowser-tabs", true);
+ await expectFocusAfterKey("Tab", "home-button");
+ await expectFocusAfterKey("Tab", gURLBar.inputField);
+ await expectFocusAfterKey("Tab", "library-button");
+ await expectFocusAfterKey("Tab", gBrowser.selectedBrowser);
+ });
+});
+
+// Test tab stops with a page loaded.
+add_task(async function testTabStopsPageLoaded() {
+ 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", "tabbrowser-tabs", true);
+ await expectFocusAfterKey("Tab", "reload-button");
+ await expectFocusAfterKey("Tab", "tracking-protection-icon-container");
+ await expectFocusAfterKey("Tab", gURLBar.inputField);
+ await expectFocusAfterKey("Tab", "pageActionButton");
+ await expectFocusAfterKey("Tab", "library-button");
+ await expectFocusAfterKey("Tab", gBrowser.selectedBrowser);
+ });
+});
+
+// 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", "library-button");
+ 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", "library-button");
+ 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", "tabbrowser-tabs", true);
+ await expectFocusAfterKey("Tab", gURLBar.inputField);
+ resetToolbarWithoutDevEditionButtons();
+ // Make sure the button is reachable now that it has been re-added.
+ await expectFocusAfterKey("Shift+Tab", "home-button", true);
+ });
+});
+
+// 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() {
+ await BrowserTestUtils.withNewTab("about:blank", async function() {
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", "library-button");
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ document.activeElement.id,
+ "library-button",
+ "ArrowLeft at end of button group does nothing"
+ );
+ await expectFocusAfterKey("ArrowRight", "sidebar-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", "sidebar-button");
+ await expectFocusAfterKey("ArrowLeft", "library-button");
+ });
+});
+
+// Test that right/left arrows move through buttons wihch 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", "pocket-button");
+ await expectFocusAfterKey("ArrowRight", "star-button");
+ await expectFocusAfterKey("ArrowLeft", "pocket-button");
+ 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.loadURI(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() {
+ 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", "library-button");
+ await expectFocusAfterKey("ArrowRight", "sidebar-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;
+ await expectFocusAfterKey("ArrowLeft", "fxa-toolbar-menu-button");
+ });
+});
+
+// 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() {
+ let button = document.getElementById("library-button");
+ forceFocus(button);
+ EventUtils.synthesizeKey(" ");
+ let view = document.getElementById("appMenu-libraryView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ let focusEvt = await focused;
+ ok(true, "Focus inside Library menu after toolbar button pressed");
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ document.activeElement,
+ focusEvt.target,
+ "ArrowLeft inside panel does nothing"
+ );
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+});
+
+// Test that right/left arrows move in the expected direction for RTL locales.
+add_task(async function testArrowsRtl() {
+ 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", "library-button", false, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ is(
+ win.document.activeElement.id,
+ "library-button",
+ "ArrowRight at end of button group does nothing"
+ );
+ await expectFocusAfterKey("ArrowLeft", "sidebar-button", false, win);
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test that right arrow reaches the overflow menu button on the Bookmarks
+// toolbar when it is visible.
+add_task(async function testArrowsBookmarksOverflowButton() {
+ let toolbarOpened = TestUtils.waitForCondition(() => {
+ let toolbar = gNavToolbox.querySelector("#PersonalToolbar");
+ return !toolbar.collapsed;
+ }, "waiting for toolbar to become visible");
+ CustomizableUI.setToolbarVisibility("PersonalToolbar", true);
+ await toolbarOpened;
+ let items = document.getElementById("PlacesToolbarItems").children;
+ let lastVisible;
+ for (let item of items) {
+ if (item.style.visibility == "hidden") {
+ break;
+ }
+ lastVisible = item;
+ }
+ forceFocus(lastVisible);
+ await expectFocusAfterKey("ArrowRight", "PlacesChevron");
+ CustomizableUI.setToolbarVisibility("PersonalToolbar", 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() {
+ 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", "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"
+ );
+ });
+});
+
+// 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() {
+ await waitUntilReloadEnabled();
+ startFromUrlBar();
+ await expectFocusAfterKey(
+ "Shift+Tab",
+ "tracking-protection-icon-container"
+ );
+ await expectFocusAfterKey("ArrowRight", "identity-box");
+ await expectFocusAfterKey(
+ "ArrowLeft",
+ "tracking-protection-icon-container"
+ );
+ });
+});
+
+// Test navigation by typed characters.
+add_task(async function testCharacterNavigation() {
+ 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", "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", "pocket-button");
+ // Pressing s again should find the next button starting with s: Sidebars.
+ await expectFocusAfterKey("s", "sidebar-button");
+ });
+});
+
+// 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() {
+ let button = document.getElementById("library-button");
+ forceFocus(button);
+ let view = document.getElementById("appMenu-libraryView");
+ let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
+ EventUtils.synthesizeKey(" ");
+ let focusEvt = await focused;
+ ok(true, "Focus inside Library menu after toolbar button pressed");
+ EventUtils.synthesizeKey("s");
+ is(document.activeElement, focusEvt.target, "s inside panel does nothing");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ view.closest("panel").hidePopup();
+ await hidden;
+});
+
+// Test tab stops after the search bar is added.
+add_task(async function testTabStopsAfterSearchBarAdded() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", 1]],
+ });
+ await withNewBlankTab(async function() {
+ startFromUrlBar();
+ await expectFocusAfterKey("Tab", "searchbar", true);
+ await expectFocusAfterKey("Tab", "library-button");
+ });
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/base/content/test/keyboard/focusableContent.html b/browser/base/content/test/keyboard/focusableContent.html
new file mode 100644
index 0000000000..255512645c
--- /dev/null
+++ b/browser/base/content/test/keyboard/focusableContent.html
@@ -0,0 +1 @@
+<button>Just a button here to have something focusable.</button>
diff --git a/browser/base/content/test/keyboard/head.js b/browser/base/content/test/keyboard/head.js
new file mode 100644
index 0000000000..9d6f901f2c
--- /dev/null
+++ b/browser/base/content/test/keyboard/head.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Force focus to an element that isn't focusable.
+ * Toolbar buttons aren't focusable because if they were, clicking them would
+ * focus them, which is undesirable. Therefore, they're only made focusable
+ * when a user is navigating with the keyboard. This function forces focus as
+ * is done during toolbar keyboard navigation.
+ */
+function forceFocus(aElem) {
+ aElem.setAttribute("tabindex", "-1");
+ aElem.focus();
+ aElem.removeAttribute("tabindex");
+}
+
+async function expectFocusAfterKey(
+ aKey,
+ aFocus,
+ aAncestorOk = false,
+ aWindow = window
+) {
+ let res = aKey.match(/^(Shift\+)?(?:(.)|(.+))$/);
+ let shift = Boolean(res[1]);
+ let key;
+ if (res[2]) {
+ key = res[2]; // Character.
+ } else {
+ key = "KEY_" + res[3]; // Tab, ArrowRight, etc.
+ }
+ let expected;
+ let friendlyExpected;
+ if (typeof aFocus == "string") {
+ expected = aWindow.document.getElementById(aFocus);
+ friendlyExpected = aFocus;
+ } else {
+ expected = aFocus;
+ if (aFocus == aWindow.gURLBar.inputField) {
+ friendlyExpected = "URL bar input";
+ } else if (aFocus == aWindow.gBrowser.selectedBrowser) {
+ friendlyExpected = "Web document";
+ }
+ }
+ info("Listening on item " + (expected.id || expected.className));
+ let focused = BrowserTestUtils.waitForEvent(expected, "focus", aAncestorOk);
+ EventUtils.synthesizeKey(key, { shiftKey: shift }, aWindow);
+ let receivedEvent = await focused;
+ info(
+ "Got focus on item: " +
+ (receivedEvent.target.id || receivedEvent.target.className)
+ );
+ ok(true, friendlyExpected + " focused after " + aKey + " pressed");
+}
diff --git a/browser/base/content/test/menubar/.eslintrc.js b/browser/base/content/test/menubar/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/menubar/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/menubar/browser.ini b/browser/base/content/test/menubar/browser.ini
new file mode 100644
index 0000000000..0b904f9802
--- /dev/null
+++ b/browser/base/content/test/menubar/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+[browser_file_menu_import_wizard.js]
+[browser_window_menu_list.js]
+skip-if = os != "mac" # Mac only feature
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..67b8cf6c0b
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_file_menu_import_wizard.js
@@ -0,0 +1,19 @@
+add_task(async function file_menu_import_wizard() {
+ // We can't call this code directly or our JS execution will get blocked on Windows/Linux where
+ // the dialog is modal.
+ executeSoon(() =>
+ document.getElementById("menu_importFromAnotherBrowser").doCommand()
+ );
+
+ await TestUtils.waitForCondition(() => {
+ let win = Services.wm.getMostRecentWindow("Browser:MigrationWizard");
+ return win && win.document && win.document.readyState == "complete";
+ }, "Migrator window loaded");
+
+ let migratorWindow = Services.wm.getMostRecentWindow(
+ "Browser:MigrationWizard"
+ );
+ ok(migratorWindow, "Migrator window opened");
+
+ await BrowserTestUtils.closeWindow(migratorWindow);
+});
diff --git a/browser/base/content/test/menubar/browser_window_menu_list.js b/browser/base/content/test/menubar/browser_window_menu_list.js
new file mode 100644
index 0000000000..0e4e64d30f
--- /dev/null
+++ b/browser/base/content/test/menubar/browser_window_menu_list.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_window_menu_list() {
+ // This title is different depending on the build. For example, it's "Nightly"
+ // for a local build, "Mozilla Firefox" for an official release build.
+ const windowTitle = window.document.title;
+ await checkWindowMenu([windowTitle, "Browser chrome tests"]);
+ let newWindow = await BrowserTestUtils.openNewBrowserWindow();
+ await checkWindowMenu([windowTitle, "Browser chrome tests", windowTitle]);
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+async function checkWindowMenu(labels) {
+ let menu = document.querySelector("#windowMenu");
+ // We can't toggle menubar items on OSX, so mocking instead.
+ await new Promise(resolve => {
+ menu.addEventListener("popupshown", resolve, { once: true });
+ menu.dispatchEvent(new MouseEvent("popupshowing"));
+ menu.dispatchEvent(new MouseEvent("popupshown"));
+ });
+
+ let menuitems = [...menu.querySelectorAll("menuseparator ~ menuitem")];
+ is(menuitems.length, labels.length, "Correct number of windows in the menu");
+ is(
+ menuitems.map(item => item.label).join(","),
+ labels.join(","),
+ "Correct labels on menuitems"
+ );
+ for (let menuitem of menuitems) {
+ ok(
+ menuitem instanceof customElements.get("menuitem"),
+ "sibling is menuitem"
+ );
+ }
+
+ // We can't toggle menubar items on OSX, so mocking instead.
+ await 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/metaTags/.eslintrc.js b/browser/base/content/test/metaTags/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/metaTags/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/metaTags/bad_meta_tags.html b/browser/base/content/test/metaTags/bad_meta_tags.html
new file mode 100644
index 0000000000..ce687d7792
--- /dev/null
+++ b/browser/base/content/test/metaTags/bad_meta_tags.html
@@ -0,0 +1,14 @@
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>BadMetaTags</title>
+ <meta property="twitter:image" content="http://test.com/twitter-image.jpg" />
+ <meta property="og:image:url" content="ftp://test.com/og-image-url" />
+ <meta property="og:image" content="file:///Users/invalid/img.jpg" />
+ <meta property="twitter:description" />
+ <meta property="og:description" content="" />
+ <meta name="description" content="description" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/metaTags/browser.ini b/browser/base/content/test/metaTags/browser.ini
new file mode 100644
index 0000000000..4468d331f0
--- /dev/null
+++ b/browser/base/content/test/metaTags/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_bad_meta_tags.js]
+support-files = bad_meta_tags.html
+[browser_meta_tags.js]
+skip-if = tsan # Bug 1403403
+support-files = meta_tags.html
diff --git a/browser/base/content/test/metaTags/browser_bad_meta_tags.js b/browser/base/content/test/metaTags/browser_bad_meta_tags.js
new file mode 100644
index 0000000000..51c252cc5c
--- /dev/null
+++ b/browser/base/content/test/metaTags/browser_bad_meta_tags.js
@@ -0,0 +1,36 @@
+/* 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,
+ "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..09278cebe5
--- /dev/null
+++ b/browser/base/content/test/metaTags/head.js
@@ -0,0 +1,29 @@
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+/**
+ * Wait for url's page info (non-null description and preview url) to be set.
+ * Because there is debounce logic in ContentLinkHandler.jsm to only make one
+ * single SQL update, we have to wait for some time before checking that the page
+ * info was stored.
+ */
+async function waitForPageInfo(url) {
+ let pageInfo;
+ await BrowserTestUtils.waitForCondition(async () => {
+ pageInfo = await PlacesUtils.history.fetch(url, { includeMeta: true });
+ return pageInfo && pageInfo.description && pageInfo.previewImageURL;
+ });
+ return pageInfo;
+}
diff --git a/browser/base/content/test/metaTags/meta_tags.html b/browser/base/content/test/metaTags/meta_tags.html
new file mode 100644
index 0000000000..ad162da1f5
--- /dev/null
+++ b/browser/base/content/test/metaTags/meta_tags.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>MetaTags</title>
+ <meta property="twitter:description" content="twitter:description" />
+ <meta property="og:description" content="og:description" />
+ <meta name="description" content="description" />
+ <meta name="unknown:tag" content="unknown:tag" />
+ <meta property="og:image" content="https://test.com/og-image.jpg" />
+ <meta property="twitter:image" content="https://test.com/twitter-image.jpg" />
+ <meta property="og:image:url" content="https://test.com/og-image-url" />
+ <meta name="thumbnail" content="https://test.com/thumbnail.jpg" />
+ </head>
+ <body>
+ <script>
+ function addMeta(tag) {
+ const meta = document.createElement("meta");
+ meta.content = "https://test.com/og-image-secure-url.jpg";
+ meta.setAttribute("property", tag);
+ document.head.appendChild(meta);
+ }
+
+ // Delay adding this "best" image tag to test that later tags are used.
+ // Use a delay that is long enough for tests to check for wrong metadata.
+ setTimeout(() => addMeta("og:image:secure_url"), 100);
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/outOfProcess/.eslintrc.js b/browser/base/content/test/outOfProcess/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/outOfProcess/browser.ini b/browser/base/content/test/outOfProcess/browser.ini
new file mode 100644
index 0000000000..9e2e8d82fb
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser.ini
@@ -0,0 +1,14 @@
+[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" && webrender && debug # Bug 1663506
+ os == "win" && bits == 64 # Bug 1663506
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..2ab0b0d4ff
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js
@@ -0,0 +1,148 @@
+/**
+ * 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..cd939ae82e
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/browser_controller.js
@@ -0,0 +1,120 @@
+function checkCommandState(testid, undoEnabled, deleteEnabled) {
+ is(
+ !document.getElementById("cmd_undo").hasAttribute("disabled"),
+ undoEnabled,
+ testid + " undo"
+ );
+ is(
+ !document.getElementById("cmd_copy").hasAttribute("disabled"),
+ true,
+ testid + " copy"
+ ); // copy should always be enabled
+ 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"
+ );
+ });
+ checkCommandState("step " + stepNum + " root focused", false, 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);
+
+ // Type into the textbox.
+ await keyAndUpdate("a", {}, 1);
+ checkCommandState("step " + stepNum + " typed", true, false);
+
+ await SpecialPowers.spawn(browsingContexts[stepNum], [], () => {
+ Assert.equal(
+ content.document.activeElement,
+ content.document.getElementById("input"),
+ "input focused"
+ );
+ });
+
+ // Select all text.
+ await keyAndUpdate("a", { accelKey: true }, 1);
+ if (AppConstants.platform != "macosx") {
+ goUpdateGlobalEditMenuItems(true);
+ }
+
+ checkCommandState("step " + stepNum + " selected", 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/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..f7035ca7d1
--- /dev/null
+++ b/browser/base/content/test/outOfProcess/head.js
@@ -0,0 +1,86 @@
+const OOP_BASE_PAGE_URI =
+ "https://example.com/browser/browser/base/content/test/outOfProcess/file_base.html";
+
+// The number of frames and subframes that exist for the basic OOP test. If frames are
+// modified within file_base.html, update this value.
+const TOTAL_FRAME_COUNT = 8;
+
+// The frames are assigned different colors based on their process ids. If you add a
+// frame you might need to add more colors to this list.
+const FRAME_COLORS = ["white", "seashell", "lightcyan", "palegreen"];
+
+/**
+ * Set up a set of child frames for the given browser for testing
+ * out of process frames. 'OOP_BASE_PAGE_URI' is the base page and subframes
+ * contain pages from the same or other domains.
+ *
+ * @param browser browser containing frame hierarchy to set up
+ * @param insertHTML HTML or function that returns what to insert into each frame
+ * @returns array of all browsing contexts in depth-first order
+ *
+ * This function adds a browsing context and process id label to each
+ * child subframe. It also sets the background color of each frame to
+ * different colors based on the process id. The browser_basic_outofprocess.js
+ * test verifies these colors to ensure that the frame/process hierarchy
+ * has been set up as expected. Colors are used to help people visualize
+ * the process setup.
+ *
+ * The insertHTML argument may be either a fixed string of HTML to insert
+ * into each subframe, or a function that returns the string to insert. The
+ * function takes one argument, the browsing context being processed.
+ */
+async function initChildFrames(browser, insertHTML) {
+ let colors = FRAME_COLORS.slice();
+ let colorMap = new Map();
+
+ let browsingContexts = [];
+
+ async function processBC(bc) {
+ browsingContexts.push(bc);
+
+ let pid = bc.currentWindowGlobal.osPid;
+ let ident = "BrowsingContext: " + bc.id + "\nProcess: " + pid;
+
+ let color = colorMap.get(pid);
+ if (!color) {
+ if (!colors.length) {
+ ok(false, "ran out of available colors");
+ }
+
+ color = colors.shift();
+ colorMap.set(pid, color);
+ }
+
+ let insertHTMLString = insertHTML;
+ if (typeof insertHTML == "function") {
+ insertHTMLString = insertHTML(bc);
+ }
+
+ await SpecialPowers.spawn(
+ bc,
+ [ident, color, insertHTMLString],
+ (identChild, colorChild, insertHTMLChild) => {
+ let root = content.document.documentElement;
+ root.style = "background-color: " + colorChild;
+
+ let pre = content.document.createElement("pre");
+ pre.textContent = identChild;
+ root.insertBefore(pre, root.firstChild);
+
+ if (insertHTMLChild) {
+ // eslint-disable-next-line no-unsanitized/property
+ content.document.getElementById(
+ "insertPoint"
+ ).innerHTML = insertHTMLChild;
+ }
+ }
+ );
+
+ for (let childBC of bc.children) {
+ await processBC(childBC);
+ }
+ }
+ await processBC(browser.browsingContext);
+
+ return browsingContexts;
+}
diff --git a/browser/base/content/test/pageActions/.eslintrc.js b/browser/base/content/test/pageActions/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/pageActions/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/pageActions/browser.ini b/browser/base/content/test/pageActions/browser.ini
new file mode 100644
index 0000000000..627757c0ed
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_PageActions_removeExtension.js]
+[browser_page_action_menu_add_search_engine.js]
+support-files =
+ page_action_menu_add_search_engine_invalid.html
+ page_action_menu_add_search_engine_one.html
+ page_action_menu_add_search_engine_many.html
+ page_action_menu_add_search_engine_same_names.html
+ page_action_menu_add_search_engine_0.xml
+ page_action_menu_add_search_engine_1.xml
+ page_action_menu_add_search_engine_2.xml
+[browser_page_action_menu_clipboard.js]
+[browser_page_action_menu_share_mac.js]
+skip-if = os != "mac" # Mac only feature
+[browser_page_action_menu_share_win.js]
+support-files =
+ browser_page_action_menu_share_win.html
+skip-if = os != "win" # Windows only feature
+[browser_page_action_menu.js]
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..8efc9b077c
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_PageActions_removeExtension.js
@@ -0,0 +1,320 @@
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.import(
+ "resource://testing-common/EnterprisePolicyTesting.jsm"
+);
+
+const { ExtensionCommon } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionCommon.jsm"
+);
+
+const { TelemetryTestUtils } = ChromeUtils.import(
+ "resource://testing-common/TelemetryTestUtils.jsm"
+);
+
+const TELEMETRY_EVENTS_FILTERS = {
+ category: "addonsManager",
+ method: "action",
+};
+
+// Initialization. Must run first.
+add_task(async function init() {
+ // 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,
+ 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() {
+ Services.telemetry.clearEvents();
+
+ // 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);
+
+ // Open the panel and then open the context menu on the action's item.
+ await promisePageActionPanelOpen();
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(panelButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ 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");
+
+ // 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");
+ let addonUninstalledPromise = promiseAddonUninstalled(extension.id);
+ mockPromptService();
+ EventUtils.synthesizeMouseAtCenter(removeExtensionItem, {});
+ await Promise.all([contextMenuPromise, addonUninstalledPromise]);
+
+ // Done, clean up.
+ await extension.unload();
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "pageAction",
+ value: "accepted",
+ extra: { addonId: extension.id, action: "uninstall" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+
+ // 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_urlbar() {
+ Services.telemetry.clearEvents();
+
+ // 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,
+ });
+ await contextMenuPromise;
+
+ 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");
+
+ // 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();
+ });
+ EventUtils.synthesizeMouseAtCenter(removeExtensionItem, {});
+ await Promise.all([contextMenuPromise, promptCancelledPromise]);
+
+ // Done, clean up.
+ await extension.unload();
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "pageAction",
+ value: "cancelled",
+ extra: { addonId: extension.id, action: "uninstall" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+
+ // 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);
+
+ // Open the panel and then open the context menu on the action's item.
+ await promisePageActionPanelOpen();
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(panelButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ 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");
+
+ // Press escape to hide the context menu.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeKey("KEY_Escape");
+ 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_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,
+ });
+ await contextMenuPromise;
+
+ 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");
+
+ // Press escape to hide the context menu.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeKey("KEY_Escape");
+ 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();
+});
+
+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() {
+ return document.querySelector(
+ "#pageActionContextMenu > menuitem[label='Remove Extension']"
+ );
+}
+
+async function promiseAnimationFrame(win = window) {
+ await new Promise(resolve => win.requestAnimationFrame(resolve));
+
+ let { tm } = Services;
+ return new Promise(resolve => tm.dispatchToMainThread(resolve));
+}
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu.js b/browser/base/content/test/pageActions/browser_page_action_menu.js
new file mode 100644
index 0000000000..deb5dacee8
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu.js
@@ -0,0 +1,1241 @@
+"use strict";
+
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+/* global UIState */
+
+const lastModifiedFixture = 1507655615.87; // Approx Oct 10th 2017
+const mockTargets = [
+ {
+ id: "0",
+ name: "foo",
+ type: "phone",
+ clientRecord: {
+ id: "cli0",
+ serverLastModified: lastModifiedFixture,
+ type: "phone",
+ },
+ },
+ {
+ id: "1",
+ name: "bar",
+ type: "desktop",
+ clientRecord: {
+ id: "cli1",
+ serverLastModified: lastModifiedFixture,
+ type: "desktop",
+ },
+ },
+ {
+ id: "2",
+ name: "baz",
+ type: "phone",
+ clientRecord: {
+ id: "cli2",
+ serverLastModified: lastModifiedFixture,
+ type: "phone",
+ },
+ },
+ { id: "3", name: "no client record device", type: "phone" },
+];
+
+add_task(async function openPanel() {
+ if (AppConstants.platform == "macosx") {
+ // Ignore this test on Mac.
+ return;
+ }
+
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Should still open the panel when Ctrl key is pressed.
+ await promisePageActionPanelOpen({ ctrlKey: true });
+
+ // Done.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function starButtonCtrlClick() {
+ // On macOS, ctrl-click shouldn't open the panel because this normally opens
+ // the context menu. This happens via the `contextmenu` event which is created
+ // by widget code, so our simulated clicks do not do so, so we can't test
+ // anything on macOS.
+ if (AppConstants.platform == "macosx") {
+ return;
+ }
+
+ // Open a unique page.
+ 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() {
+ // Open a unique page.
+ let url = "http://example.com/browser_page_action_menu";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The bookmark button should read "Bookmark This Page" and not be starred.
+ let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
+ Assert.equal(bookmarkButton.label, "Bookmark This Page");
+ Assert.ok(!bookmarkButton.hasAttribute("starred"));
+
+ // Click the button.
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {});
+ await hiddenPromise;
+
+ // Make sure the edit-bookmark panel opens, then hide it.
+ await new Promise(resolve => {
+ if (StarUI.panel.state == "open") {
+ resolve();
+ return;
+ }
+ StarUI.panel.addEventListener("popupshown", resolve, { once: true });
+ });
+ Assert.equal(
+ BookmarkingUI.starBox.getAttribute("open"),
+ "true",
+ "Star has open attribute"
+ );
+ StarUI.panel.hidePopup();
+ Assert.ok(
+ !BookmarkingUI.starBox.hasAttribute("open"),
+ "Star no longer has open attribute"
+ );
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+
+ // The bookmark button should now read "Edit This Bookmark" and be starred.
+ Assert.equal(bookmarkButton.label, "Edit This Bookmark");
+ Assert.ok(bookmarkButton.hasAttribute("starred"));
+ Assert.equal(bookmarkButton.getAttribute("starred"), "true");
+
+ // Click it again.
+ hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {});
+ await hiddenPromise;
+
+ // The edit-bookmark panel should open again.
+ await new Promise(resolve => {
+ if (StarUI.panel.state == "open") {
+ resolve();
+ return;
+ }
+ StarUI.panel.addEventListener("popupshown", resolve, { once: true });
+ });
+
+ let onItemRemovedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => event.url == url),
+ "places"
+ );
+
+ // Click the remove-bookmark button in the panel.
+ StarUI._element("editBookmarkPanelRemoveButton").click();
+
+ // Wait for the bookmark to be removed before continuing.
+ await onItemRemovedPromise;
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+
+ // The bookmark button should read "Bookmark This Page" and not be starred.
+ Assert.equal(bookmarkButton.label, "Bookmark This Page");
+ Assert.ok(!bookmarkButton.hasAttribute("starred"));
+
+ // Done.
+ hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function pinTabFromPanel() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel and click Pin Tab.
+ await promisePageActionPanelOpen();
+
+ let pinTabButton = document.getElementById("pageAction-panel-pinTab");
+ Assert.equal(pinTabButton.label, "Pin Tab");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await hiddenPromise;
+
+ Assert.ok(gBrowser.selectedTab.pinned, "Tab was pinned");
+
+ // Open the panel and click Unpin Tab.
+ await promisePageActionPanelOpen();
+ Assert.equal(pinTabButton.label, "Unpin Tab");
+
+ hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await hiddenPromise;
+
+ Assert.ok(!gBrowser.selectedTab.pinned, "Tab was unpinned");
+ });
+});
+
+add_task(async function pinTabFromURLBar() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Add action to URL bar.
+ let action = PageActions._builtInActions.find(a => a.id == "pinTab");
+ action.pinnedToUrlbar = true;
+ registerCleanupFunction(() => (action.pinnedToUrlbar = false));
+
+ // Click the Pin Tab button.
+ let pinTabButton = document.getElementById("pageAction-urlbar-pinTab");
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await TestUtils.waitForCondition(
+ () => gBrowser.selectedTab.pinned,
+ "Tab was pinned"
+ );
+
+ // Click the Unpin Tab button
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await TestUtils.waitForCondition(
+ () => !gBrowser.selectedTab.pinned,
+ "Tab was unpinned"
+ );
+ });
+});
+
+add_task(async function emailLink() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Replace the email-link entry point to check whether it's called.
+ let originalFn = MailIntegration.sendLinkForBrowser;
+ let fnCalled = false;
+ MailIntegration.sendLinkForBrowser = () => {
+ fnCalled = true;
+ };
+ registerCleanupFunction(() => {
+ MailIntegration.sendLinkForBrowser = originalFn;
+ });
+
+ // Open the panel and click Email Link.
+ await promisePageActionPanelOpen();
+ let emailLinkButton = document.getElementById("pageAction-panel-emailLink");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(emailLinkButton, {});
+ await hiddenPromise;
+
+ Assert.ok(fnCalled);
+ });
+});
+
+add_task(async function copyURLFromPanel() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Add action to URL bar.
+ let action = PageActions._builtInActions.find(a => a.id == "copyURL");
+ action.pinnedToUrlbar = true;
+ registerCleanupFunction(() => (action.pinnedToUrlbar = false));
+
+ // Open the panel and click Copy URL.
+ await promisePageActionPanelOpen();
+ Assert.ok(true, "page action panel opened");
+
+ let copyURLButton = document.getElementById("pageAction-panel-copyURL");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
+ await hiddenPromise;
+
+ let feedbackPanel = ConfirmationHint._panel;
+ let feedbackShownPromise = BrowserTestUtils.waitForEvent(
+ feedbackPanel,
+ "popupshown"
+ );
+ await feedbackShownPromise;
+ Assert.equal(
+ feedbackPanel.anchorNode.id,
+ "pageActionButton",
+ "Feedback menu should be anchored on the main Page Action button"
+ );
+ let feedbackHiddenPromise = promisePanelHidden(feedbackPanel);
+ await feedbackHiddenPromise;
+
+ action.pinnedToUrlbar = false;
+ });
+});
+
+add_task(async function copyURLFromURLBar() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Add action to URL bar.
+ let action = PageActions._builtInActions.find(a => a.id == "copyURL");
+ action.pinnedToUrlbar = true;
+ registerCleanupFunction(() => (action.pinnedToUrlbar = false));
+
+ let copyURLButton = document.getElementById("pageAction-urlbar-copyURL");
+ let panel = ConfirmationHint._panel;
+ let feedbackShownPromise = promisePanelShown(panel);
+ EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
+
+ await feedbackShownPromise;
+ Assert.equal(
+ panel.anchorNode.id,
+ "pageAction-urlbar-copyURL",
+ "Feedback menu should be anchored on the main URL bar button"
+ );
+ let feedbackHiddenPromise = promisePanelHidden(panel);
+ await feedbackHiddenPromise;
+
+ action.pinnedToUrlbar = false;
+ });
+});
+
+add_task(async function sendToDevice_nonSendable() {
+ // Open a tab that's not sendable but where the page action buttons still
+ // appear. about:about is convenient.
+ await BrowserTestUtils.withNewTab("about:about", async () => {
+ await promiseSyncReady();
+ // Open the panel. Send to Device should be disabled.
+ await promisePageActionPanelOpen();
+ Assert.equal(
+ BrowserPageActions.mainButtonNode.getAttribute("open"),
+ "true",
+ "Main button has 'open' attribute"
+ );
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(
+ "sendToDevice"
+ );
+ Assert.equal(
+ panelButton.disabled,
+ true,
+ "The panel button should be disabled"
+ );
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ Assert.ok(
+ !BrowserPageActions.mainButtonNode.hasAttribute("open"),
+ "Main button no longer has 'open' attribute"
+ );
+ // The urlbar button shouldn't exist.
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
+ "sendToDevice"
+ );
+ Assert.equal(urlbarButton, null, "The urlbar button shouldn't exist");
+ });
+});
+
+add_task(async function sendToDevice_syncNotReady_other_states() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => null);
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_NOT_VERIFIED });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "Account Not Verified",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Verify Your Account...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+ });
+});
+
+add_task(async function sendToDevice_syncNotReady_configured() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ const recentDeviceList = sandbox
+ .stub(fxAccounts.device, "recentDeviceList")
+ .get(() => null);
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").callsFake(() => {
+ recentDeviceList.get(() =>
+ mockTargets.map(({ id, name, type }) => ({ id, name, type }))
+ );
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+ });
+
+ let onShowingSubview = BrowserPageActions.sendToDevice.onShowingSubview;
+ sandbox
+ .stub(BrowserPageActions.sendToDevice, "onShowingSubview")
+ .callsFake((...args) => {
+ this.numCall++ || (this.numCall = 1);
+ onShowingSubview.call(BrowserPageActions.sendToDevice, ...args);
+ testSendTabToDeviceMenu(this.numCall);
+ });
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ function testSendTabToDeviceMenu(numCall) {
+ if (numCall == 1) {
+ // "Syncing devices" should be shown.
+ checkSendToDeviceItems([
+ {
+ className: "pageAction-sendToDevice-notReady",
+ disabled: true,
+ },
+ ]);
+ } else if (numCall == 2) {
+ // The devices should be shown in the subview.
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ ];
+ for (let target of mockTargets) {
+ const attrs = {
+ clientId: target.id,
+ label: target.name,
+ clientType: target.type,
+ };
+ if (target.clientRecord && target.clientRecord.serverLastModified) {
+ attrs.tooltiptext = gSync.formatLastSyncDate(
+ new Date(target.clientRecord.serverLastModified * 1000)
+ );
+ }
+ expectedItems.push({
+ attrs,
+ });
+ }
+ expectedItems.push(null, {
+ attrs: {
+ label: "Send to All Devices",
+ },
+ });
+ expectedItems.push(null, {
+ attrs: {
+ label: "Manage Devices...",
+ },
+ });
+ checkSendToDeviceItems(expectedItems);
+ } else {
+ ok(false, "This should never happen");
+ }
+ }
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ cleanUp();
+ });
+});
+
+add_task(async function sendToDevice_notSignedIn() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "Not Signed In",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Sign in to Firefox...",
+ },
+ },
+ {
+ attrs: {
+ label: "Learn About Sending Tabs...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function sendToDevice_noDevices() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "No Devices Connected",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Connect Another Device...",
+ },
+ },
+ {
+ attrs: {
+ label: "Learn About Sending Tabs...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+
+ await UIState.reset();
+ });
+});
+
+add_task(async function sendToDevice_devices() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts.device, "recentDeviceList")
+ .get(() => mockTargets.map(({ id, name, type }) => ({ id, name, type })));
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox
+ .stub(fxAccounts.commands.sendTab, "isDeviceCompatible")
+ .returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox.spy(Weave.Service, "sync");
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ // The devices should be shown in the subview.
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ clientId: "1",
+ label: "bar",
+ clientType: "desktop",
+ },
+ },
+ {
+ attrs: {
+ clientId: "2",
+ label: "baz",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "0",
+ label: "foo",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "3",
+ label: "no client record device",
+ clientType: "phone",
+ },
+ },
+ null,
+ {
+ attrs: {
+ label: "Send to All Devices",
+ },
+ },
+ {
+ attrs: {
+ label: "Manage Devices...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ Assert.ok(Weave.Service.sync.notCalled);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+ });
+});
+
+add_task(async function sendTabToDevice_syncEnabled() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_SIGNED_IN, syncEnabled: true });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox.spy(fxAccounts.device, "refreshDeviceList");
+ sandbox.spy(Weave.Service, "sync");
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "No Devices Connected",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Connect Another Device...",
+ },
+ },
+ {
+ attrs: {
+ label: "Learn About Sending Tabs...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ Assert.ok(Weave.Service.sync.notCalled);
+ Assert.equal(fxAccounts.device.refreshDeviceList.callCount, 1);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+ });
+});
+
+add_task(async function sendToDevice_title() {
+ // Open two tabs that are sendable.
+ await BrowserTestUtils.withNewTab(
+ "http://example.com/a",
+ async otherBrowser => {
+ await BrowserTestUtils.withNewTab("http://example.com/b", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel. Only one tab is selected, so the action's title should
+ // be "Send Tab to Device".
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ Assert.equal(sendToDeviceButton.label, "Send Tab to Device");
+
+ // Hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ // Add the other tab to the selection.
+ gBrowser.addToMultiSelectedTabs(
+ gBrowser.getTabForBrowser(otherBrowser),
+ { isLastMultiSelectChange: true }
+ );
+
+ // Open the panel again. Now the action's title should be "Send 2 Tabs to
+ // Device".
+ await promisePageActionPanelOpen();
+ Assert.ok(!sendToDeviceButton.disabled);
+ Assert.equal(sendToDeviceButton.label, "Send 2 Tabs to Device");
+
+ // Hide the panel.
+ hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+
+ await UIState.reset();
+ });
+ }
+ );
+});
+
+add_task(async function sendToDevice_inUrlbar() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts.device, "recentDeviceList")
+ .get(() => mockTargets.map(({ id, name, type }) => ({ id, name, type })));
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox
+ .stub(fxAccounts.commands.sendTab, "isDeviceCompatible")
+ .returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+ sandbox.stub(gSync, "sendTabToDevice").resolves(true);
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Add Send to Device to the urlbar.
+ let action = PageActions.actionForID("sendToDevice");
+ action.pinnedToUrlbar = true;
+
+ // Click it to open its panel.
+ let urlbarButton = document.getElementById(
+ BrowserPageActions.urlbarButtonNodeIDForActionID(action.id)
+ );
+ Assert.notEqual(urlbarButton, null, "The urlbar button should exist");
+ Assert.ok(
+ !urlbarButton.disabled,
+ "The urlbar button should not be disabled"
+ );
+ EventUtils.synthesizeMouseAtCenter(urlbarButton, {});
+ // The panel element for _activatedActionPanelID is created synchronously
+ // only after the associated button has been clicked.
+ await promisePanelShown(BrowserPageActions._activatedActionPanelID);
+ Assert.equal(
+ urlbarButton.getAttribute("open"),
+ "true",
+ "Button has open attribute"
+ );
+
+ // The devices should be shown in the subview.
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ clientId: "1",
+ label: "bar",
+ clientType: "desktop",
+ },
+ },
+ {
+ attrs: {
+ clientId: "2",
+ label: "baz",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "0",
+ label: "foo",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "3",
+ label: "no client record device",
+ clientType: "phone",
+ },
+ },
+ null,
+ {
+ attrs: {
+ label: "Send to All Devices",
+ },
+ },
+ {
+ attrs: {
+ label: "Manage Devices...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems, true);
+
+ // Get the first device menu item in the panel.
+ let bodyID =
+ BrowserPageActions._panelViewNodeIDForActionID("sendToDevice", true) +
+ "-body";
+ let body = document.getElementById(bodyID);
+ let deviceMenuItem = body.querySelector(".sendtab-target");
+ Assert.notEqual(deviceMenuItem, null);
+
+ // For good measure, wait until it's visible.
+ let dwu = window.windowUtils;
+ await TestUtils.waitForCondition(() => {
+ let bounds = dwu.getBoundsWithoutFlushing(deviceMenuItem);
+ return bounds.height > 0 && bounds.width > 0;
+ }, "Waiting for first device menu item to appear");
+
+ // Click it, which should cause the panel to close.
+ let hiddenPromise = promisePanelHidden(
+ BrowserPageActions._activatedActionPanelID
+ );
+ EventUtils.synthesizeMouseAtCenter(deviceMenuItem, {});
+ info("Waiting for Send to Device panel to close after clicking a device");
+ await hiddenPromise;
+ Assert.ok(
+ !urlbarButton.hasAttribute("open"),
+ "URL bar button no longer has open attribute"
+ );
+
+ // And then the "Sent!" notification panel should open and close by itself
+ // after a moment.
+ info("Waiting for the Sent! notification panel to open");
+ await promisePanelShown(ConfirmationHint._panel.id);
+ Assert.equal(ConfirmationHint._panel.anchorNode.id, urlbarButton.id);
+ info("Waiting for the Sent! notification panel to close");
+ await promisePanelHidden(ConfirmationHint._panel.id);
+
+ // Remove Send to Device from the urlbar.
+ action.pinnedToUrlbar = false;
+
+ cleanUp();
+ });
+});
+
+add_task(async function contextMenu() {
+ // Open an actionable page so that the main page action button appears.
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel and then open the context menu on the bookmark button.
+ await promisePageActionPanelOpen();
+ let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // The context menu should show the "remove" item. Click it.
+ let menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Remove from Address Bar",
+ "Context menu is in the 'remove' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+
+ // The action should be removed from the urlbar. In this case, the bookmark
+ // star, the node in the urlbar should be hidden.
+ let starButtonBox = document.getElementById("star-button-box");
+ await TestUtils.waitForCondition(() => {
+ return starButtonBox.hidden;
+ }, "Waiting for star button to become hidden");
+
+ // Open the context menu again on the bookmark button. (The page action
+ // panel remains open.)
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // The context menu should show the "add" item. Click it.
+ menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Add to Address Bar",
+ "Context menu is in the 'add' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+
+ // The action should be added to the urlbar.
+ await TestUtils.waitForCondition(() => {
+ return !starButtonBox.hidden;
+ }, "Waiting for star button to become unhidden");
+
+ // Open the context menu on the bookmark star in the urlbar.
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(starButtonBox, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // The context menu should show the "remove" item. Click it.
+ menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Remove from Address Bar",
+ "Context menu is in the 'remove' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+
+ // The action should be removed from the urlbar.
+ await TestUtils.waitForCondition(() => {
+ return starButtonBox.hidden;
+ }, "Waiting for star button to become hidden");
+
+ // Finally, add the bookmark star back to the urlbar so that other tests
+ // that rely on it are OK.
+ await promisePageActionPanelOpen();
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Add to Address Bar",
+ "Context menu is in the 'add' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+ await TestUtils.waitForCondition(() => {
+ return !starButtonBox.hidden;
+ }, "Waiting for star button to become unhidden");
+ });
+
+ // 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.textbox, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+function promiseSyncReady() {
+ let service = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
+ .wrappedJSObject;
+ return service.whenLoaded().then(() => {
+ UIState.isReady();
+ return UIState.refresh();
+ });
+}
+
+function checkSendToDeviceItems(expectedItems, forUrlbar = false) {
+ let bodyID =
+ BrowserPageActions._panelViewNodeIDForActionID("sendToDevice", forUrlbar) +
+ "-body";
+ let body = document.getElementById(bodyID);
+ Assert.equal(body.children.length, expectedItems.length);
+ for (let i = 0; i < expectedItems.length; i++) {
+ let expected = expectedItems[i];
+ let actual = body.children[i];
+ if (!expected) {
+ Assert.equal(actual.localName, "toolbarseparator");
+ continue;
+ }
+ if ("id" in expected) {
+ Assert.equal(actual.id, expected.id);
+ }
+ if ("className" in expected) {
+ let expectedNames = expected.className.split(/\s+/);
+ for (let name of expectedNames) {
+ Assert.ok(
+ actual.classList.contains(name),
+ `classList contains: ${name}`
+ );
+ }
+ }
+ let display = "display" in expected ? expected.display : "-moz-box";
+ Assert.equal(getComputedStyle(actual).display, display);
+ let disabled = "disabled" in expected ? expected.disabled : false;
+ Assert.equal(actual.disabled, disabled);
+ if ("attrs" in expected) {
+ for (let name in expected.attrs) {
+ Assert.ok(actual.hasAttribute(name));
+ let attrVal = actual.getAttribute(name);
+ if (name == "label") {
+ attrVal = attrVal.normalize("NFKC"); // There's a bug with …
+ }
+ Assert.equal(attrVal, expected.attrs[name]);
+ }
+ }
+ }
+}
+
+function collectContextMenuItems() {
+ let contextMenu = document.getElementById("pageActionContextMenu");
+ return Array.prototype.filter.call(contextMenu.children, node => {
+ return window.getComputedStyle(node).visibility == "visible";
+ });
+}
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js b/browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js
new file mode 100644
index 0000000000..9dd88947ba
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js
@@ -0,0 +1,672 @@
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromptTestUtils.jsm"
+);
+
+// Checks the panel button with a page that doesn't offer any engines.
+add_task(async function none() {
+ let url = "http://mochi.test:8888/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // The action should not be present.
+ let actions = PageActions.actionsInPanel(window);
+ Assert.ok(
+ !actions.some(a => a.id == "addSearchEngine"),
+ "Action should not be present in panel"
+ );
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(!button, "Action button should not be in panel");
+ });
+});
+
+// Checks the panel button with a page that offers one engine.
+add_task(async function one() {
+ let url =
+ getRootDirectory(gTestPath) + "page_action_menu_add_search_engine_one.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The action should be present.
+ let actions = PageActions.actionsInPanel(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ let expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(button, "Button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Click the action's button.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let hiddenPromise = promisePageActionPanelHidden();
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+ let engine = await enginePromise;
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!");
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // The action should be gone.
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should not be present in panel");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Action button should not be in panel");
+
+ // Remove the engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ await Services.search.removeEngine(engine);
+ await enginePromise;
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // The action should be present again.
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Action button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+ });
+});
+
+// Checks the panel button with a page that offers an invalid engine.
+add_task(async function invalid() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ let url =
+ getRootDirectory(gTestPath) +
+ "page_action_menu_add_search_engine_invalid.html";
+ await BrowserTestUtils.withNewTab(url, async tab => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The action should be present.
+ let actions = PageActions.actionsInPanel(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ let expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(button, "Button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Click the action's button.
+ let hiddenPromise = promisePageActionPanelHidden();
+ let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
+ modalType: Ci.nsIPromptService.MODAL_TYPE_CONTENT,
+ promptType: "alert",
+ });
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+ let prompt = await promptPromise;
+
+ Assert.ok(
+ prompt.ui.infoBody.textContent.includes(
+ "http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_404.xml"
+ ),
+ "Should have included the url in the prompt body"
+ );
+
+ await PromptTestUtils.handlePrompt(prompt);
+ });
+});
+
+// Checks the panel button with a page that offers many engines.
+add_task(async function many() {
+ let url =
+ getRootDirectory(gTestPath) +
+ "page_action_menu_add_search_engine_many.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The action should be present.
+ let actions = PageActions.actionsInPanel(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ let expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(button, "Action button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ true,
+ "Button should expand into a subview"
+ );
+
+ // Click the action's button. The subview should be shown.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ let view = await viewPromise;
+ let viewID = BrowserPageActions._panelViewNodeIDForActionID(
+ "addSearchEngine",
+ false
+ );
+ Assert.equal(view.id, viewID, "View ID");
+ let bodyID = viewID + "-body";
+ let body = document.getElementById(bodyID);
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the first engine to install it.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let hiddenPromise = promisePageActionPanelHidden();
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ let engines = [];
+ let engine = await enginePromise;
+ engines.push(engine);
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Open the panel and show the subview again. The installed engine should
+ // be gone.
+ await promisePageActionPanelOpen();
+ viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await viewPromise;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the next engine to install it.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_1"
+ );
+ hiddenPromise = promisePageActionPanelHidden();
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Open the panel again. This time the action button should show the one
+ // remaining engine.
+ await promisePageActionPanelOpen();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Button should be present in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Click the button.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_2"
+ );
+ hiddenPromise = promisePageActionPanelHidden();
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // All engines are installed at this point. Open the panel and make sure
+ // the action is gone.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should be gone");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Button should not be in panel");
+
+ // Remove the first engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again. The action should be present and showing the first
+ // engine.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Button should be present in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Remove the second engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_1"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview. The subview should be
+ // present now that there are two offered engines again.
+ await promisePageActionPanelOpen();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ true,
+ "Button should expand into a subview"
+ );
+ viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await viewPromise;
+ body = document.getElementById(bodyID);
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ ],
+ "Subview children"
+ );
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // Remove the third engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_2"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview.
+ await promisePageActionPanelOpen();
+ viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await viewPromise;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ });
+});
+
+// Checks the urlbar button with a page that offers one engine.
+add_task(async function urlbarOne() {
+ let url =
+ getRootDirectory(gTestPath) + "page_action_menu_add_search_engine_one.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await promiseNodeVisible(BrowserPageActions.mainButtonNode);
+
+ // Pin the action to the urlbar.
+ let placedPromise = promisePlacedInUrlbar();
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = true;
+
+ // It should be placed.
+ let button = await placedPromise;
+ let actions = PageActions.actionsInUrlbar(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Action button should be in urlbar");
+
+ // Click the action's button.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ let engine = await enginePromise;
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!");
+
+ // The action should be gone.
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should not be present in urlbar");
+ button = BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Action button should not be in urlbar");
+
+ // Remove the engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ placedPromise = promisePlacedInUrlbar();
+ await Services.search.removeEngine(engine);
+ await enginePromise;
+
+ // The action should be present again.
+ button = await placedPromise;
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Action button should be in urlbar");
+
+ // Clean up.
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = false;
+ await TestUtils.waitForCondition(() => {
+ return !BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ });
+ });
+});
+
+// Checks the urlbar button with a page that offers many engines.
+add_task(async function urlbarMany() {
+ let url =
+ getRootDirectory(gTestPath) +
+ "page_action_menu_add_search_engine_many.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await promiseNodeVisible(BrowserPageActions.mainButtonNode);
+
+ // Pin the action to the urlbar.
+ let placedPromise = promisePlacedInUrlbar();
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = true;
+
+ // It should be placed.
+ let button = await placedPromise;
+ let actions = PageActions.actionsInUrlbar(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Action button should be in urlbar");
+
+ // Click the action's button. The activated-action panel should open, and
+ // it should contain the addSearchEngine subview.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ let view = await waitForActivatedActionPanel();
+ let viewID = BrowserPageActions._panelViewNodeIDForActionID(
+ "addSearchEngine",
+ true
+ );
+ Assert.equal(view.id, viewID, "View ID");
+ let body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the first engine to install it.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ let engines = [];
+ let engine = await enginePromise;
+ engines.push(engine);
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Open the panel again. The installed engine should be gone.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ view = await waitForActivatedActionPanel();
+ body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the next engine to install it.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_1"
+ );
+ hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Now there's only one engine left, so clicking the button should simply
+ // install it instead of opening the activated-action panel.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_2"
+ );
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // All engines are installed at this point. The action should be gone.
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should be gone");
+ button = BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Button should not be in urlbar");
+
+ // Remove the first engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ placedPromise = promisePlacedInUrlbar();
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // The action should be placed again.
+ button = await placedPromise;
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Button should be in urlbar");
+
+ // Remove the second engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_1"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview. The subview should be
+ // present now that there are two offered engines again.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ view = await waitForActivatedActionPanel();
+ body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ ],
+ "Subview children"
+ );
+
+ // Hide the panel.
+ hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+
+ // Remove the third engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_2"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ view = await waitForActivatedActionPanel();
+ body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Hide the panel.
+ hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+
+ // Clean up.
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = false;
+ await TestUtils.waitForCondition(() => {
+ return !BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ });
+ });
+});
+
+function promiseEngine(expectedData, expectedEngineName) {
+ info(`Waiting for engine ${expectedData}`);
+ return TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (engine, data) => {
+ info(`Got engine ${engine.wrappedJSObject.name} ${data}`);
+ return (
+ expectedData == data &&
+ expectedEngineName == engine.wrappedJSObject.name
+ );
+ }
+ ).then(([engine, data]) => engine);
+}
+
+function promiseFeedbackPanelShownAndHidden() {
+ info("Waiting for feedback panel popupshown");
+ return BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popupshown"
+ ).then(() => {
+ info("Got feedback panel popupshown. Now waiting for popuphidden");
+ return BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popuphidden"
+ ).then(() => ConfirmationHint._message.textContent);
+ });
+}
+
+function promisePlacedInUrlbar() {
+ let action = PageActions.actionForID("addSearchEngine");
+ return new Promise(resolve => {
+ let onPlaced = action._onPlacedInUrlbar;
+ action._onPlacedInUrlbar = button => {
+ action._onPlacedInUrlbar = onPlaced;
+ if (action._onPlacedInUrlbar) {
+ action._onPlacedInUrlbar(button);
+ }
+ promiseNodeVisible(button).then(() => resolve(button));
+ };
+ });
+}
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js b/browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js
new file mode 100644
index 0000000000..12d9ef8468
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js
@@ -0,0 +1,40 @@
+"use strict";
+
+const mockRemoteClients = [
+ { id: "0", name: "foo", type: "mobile" },
+ { id: "1", name: "bar", type: "desktop" },
+ { id: "2", name: "baz", type: "mobile" },
+];
+
+add_task(async function copyURL() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Copy URL.
+ let copyURLButton = document.getElementById("pageAction-panel-copyURL");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
+ await hiddenPromise;
+
+ // Check the clipboard.
+ let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ transferable.init(null);
+ let flavor = "text/unicode";
+ transferable.addDataFlavor(flavor);
+ Services.clipboard.getData(
+ transferable,
+ Services.clipboard.kGlobalClipboard
+ );
+ let strObj = {};
+ transferable.getTransferData(flavor, strObj);
+ Assert.ok(!!strObj.value);
+ strObj.value.QueryInterface(Ci.nsISupportsString);
+ Assert.equal(strObj.value.data, gBrowser.selectedBrowser.currentURI.spec);
+ });
+});
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js b/browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js
new file mode 100644
index 0000000000..e15e7619a8
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+const URL = "http://example.org/";
+
+// Keep track of title of service we chose to share with
+let serviceName, sharedUrl, sharedTitle;
+let sharingPreferencesCalled = false;
+
+let mockShareData = [
+ {
+ name: "NSA",
+ menuItemTitle: "National Security Agency",
+ image:
+ "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEA" +
+ "LAAAAAABAAEAAAICTAEAOw==",
+ },
+];
+
+let stub = sinon
+ .stub(BrowserPageActions.shareURL, "_sharingService")
+ .get(() => {
+ return {
+ getSharingProviders(url) {
+ return mockShareData;
+ },
+ shareUrl(name, url, title) {
+ serviceName = name;
+ sharedUrl = url;
+ sharedTitle = title;
+ },
+ openSharingPreferences() {
+ sharingPreferencesCalled = true;
+ },
+ };
+ });
+
+registerCleanupFunction(async function() {
+ stub.restore();
+ await EventUtils.synthesizeNativeMouseMove(document.documentElement, 0, 0);
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function shareURL() {
+ await BrowserTestUtils.withNewTab(URL, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Share URL.
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {});
+
+ let view = await viewPromise;
+ let body = document.getElementById(view.id + "-body");
+
+ // We should see 1 receiver and one extra node for the "More..." button
+ Assert.equal(body.children.length, 2, "Has correct share receivers");
+ let shareButton = body.children[0];
+ Assert.equal(shareButton.label, mockShareData[0].menuItemTitle);
+ let hiddenPromise = promisePageActionPanelHidden();
+ // Click on share, panel should hide and sharingService should be
+ // given the title of service to share with
+ EventUtils.synthesizeMouseAtCenter(shareButton, {});
+ await hiddenPromise;
+
+ Assert.equal(
+ serviceName,
+ mockShareData[0].name,
+ "Shared the correct service name"
+ );
+ Assert.equal(sharedUrl, "http://example.org/", "Shared correct URL");
+ Assert.equal(
+ sharedTitle,
+ "mochitest index /",
+ "Shared with the correct title"
+ );
+ });
+});
+
+add_task(async function shareURLAddressBar() {
+ await BrowserTestUtils.withNewTab(URL, async () => {
+ // Open pageAction panel
+ await promisePageActionPanelOpen();
+
+ // Right click the Share button
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // Click "Add to Address Bar"
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ let ctxMenuButton = document.querySelector(
+ "#pageActionContextMenu .pageActionContextMenuItem"
+ );
+ EventUtils.synthesizeMouseAtCenter(ctxMenuButton, {});
+ await contextMenuPromise;
+
+ // Wait for the Share button to be added
+ await BrowserTestUtils.waitForCondition(() => {
+ return document.getElementById("pageAction-urlbar-shareURL");
+ }, "Waiting for the share url button to be added to url bar");
+
+ // Press the Share button
+ let shareButton = document.getElementById("pageAction-urlbar-shareURL");
+ let viewPromise = promisePageActionPanelShown();
+ EventUtils.synthesizeMouseAtCenter(shareButton, {});
+ await viewPromise;
+
+ // Ensure we have share providers
+ let panel = document.getElementById(
+ "pageAction-urlbar-shareURL-subview-body"
+ );
+ // We should see 1 receiver and one extra node for the "More..." button
+ Assert.equal(panel.children.length, 2, "Has correct share receivers");
+
+ // Remove the Share URL button from the Address bar so we dont interfere
+ // with future tests
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(shareButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ ctxMenuButton = document.querySelector(
+ "#pageActionContextMenu .pageActionContextMenuItem"
+ );
+ EventUtils.synthesizeMouseAtCenter(ctxMenuButton, {});
+ await contextMenuPromise;
+ });
+});
+
+add_task(async function openSharingPreferences() {
+ await BrowserTestUtils.withNewTab(URL, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Share URL.
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {});
+
+ let view = await viewPromise;
+ let body = document.getElementById(view.id + "-body");
+
+ // We should see 1 receiver and one extra node for the "More..." button
+ Assert.equal(body.children.length, 2, "Has correct share receivers");
+ let moreButton = body.children[1];
+ let hiddenPromise = promisePageActionPanelHidden();
+ // Click on the "more" button, panel should hide and we should call
+ // the sharingService function to open preferences
+ EventUtils.synthesizeMouseAtCenter(moreButton, {});
+ await hiddenPromise;
+
+ Assert.equal(
+ sharingPreferencesCalled,
+ true,
+ "We called openSharingPreferences"
+ );
+ });
+});
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_share_win.html b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.html
new file mode 100644
index 0000000000..6c47f98c7e
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<title>Windows Sharing</title>
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_share_win.js b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.js
new file mode 100644
index 0000000000..973365086f
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+const TEST_URL =
+ getRootDirectory(gTestPath) + "browser_page_action_menu_share_win.html";
+
+// Keep track of site details we are sharing
+let sharedUrl, sharedTitle;
+
+let stub = sinon
+ .stub(BrowserPageActions.shareURL, "_windowsUIUtils")
+ .get(() => {
+ return {
+ shareUrl(url, title) {
+ sharedUrl = url;
+ sharedTitle = title;
+ },
+ };
+ });
+
+registerCleanupFunction(async function() {
+ stub.restore();
+});
+
+add_task(async function shareURL() {
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.4")) {
+ Assert.ok(true, "We only expose share on windows 10 and above");
+ return;
+ }
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Share URL.
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {});
+
+ await hiddenPromise;
+
+ Assert.equal(sharedUrl, TEST_URL, "Shared correct URL");
+ Assert.equal(
+ sharedTitle,
+ "Windows Sharing",
+ "Shared with the correct title"
+ );
+ });
+});
diff --git a/browser/base/content/test/pageActions/head.js b/browser/base/content/test/pageActions/head.js
new file mode 100644
index 0000000000..297ca00f65
--- /dev/null
+++ b/browser/base/content/test/pageActions/head.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { PlacesTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+function promisePageActionPanelOpen(eventDict = {}) {
+ let dwu = window.windowUtils;
+ return 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 = dwu.getBoundsWithoutFlushing(
+ BrowserPageActions.mainButtonNode
+ );
+ return bounds.width > 0 && bounds.height > 0;
+ })
+ .then(() => {
+ // Wait for the panel to become open, by clicking the button if necessary.
+ info("Waiting for main page action panel to be open");
+ if (BrowserPageActions.panelNode.state == "open") {
+ return Promise.resolve();
+ }
+ let shownPromise = promisePageActionPanelShown();
+ EventUtils.synthesizeMouseAtCenter(
+ BrowserPageActions.mainButtonNode,
+ eventDict
+ );
+ return shownPromise;
+ })
+ .then(() => {
+ // Wait for items in the panel to become visible.
+ return promisePageActionViewChildrenVisible(
+ BrowserPageActions.mainViewNode
+ );
+ });
+}
+
+async function waitForActivatedActionPanel() {
+ if (!BrowserPageActions.activatedActionPanelNode) {
+ info("Waiting for activated-action panel to be added to mainPopupSet");
+ await new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ if (BrowserPageActions.activatedActionPanelNode) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ let popupSet = document.getElementById("mainPopupSet");
+ observer.observe(popupSet, { childList: true });
+ });
+ info("Activated-action panel added to mainPopupSet");
+ }
+ if (!BrowserPageActions.activatedActionPanelNode.state == "open") {
+ info("Waiting for activated-action panel popupshown");
+ await promisePanelShown(BrowserPageActions.activatedActionPanelNode);
+ info("Got activated-action panel popupshown");
+ }
+ let panelView = BrowserPageActions.activatedActionPanelNode.querySelector(
+ "panelview"
+ );
+ if (panelView) {
+ await BrowserTestUtils.waitForEvent(
+ BrowserPageActions.activatedActionPanelNode,
+ "ViewShown"
+ );
+ await promisePageActionViewChildrenVisible(panelView);
+ }
+ return panelView;
+}
+
+function promisePageActionPanelShown() {
+ return promisePanelShown(BrowserPageActions.panelNode);
+}
+
+function promisePageActionPanelHidden() {
+ return promisePanelHidden(BrowserPageActions.panelNode);
+}
+
+function promisePanelShown(panelIDOrNode) {
+ return promisePanelEvent(panelIDOrNode, "popupshown");
+}
+
+function promisePanelHidden(panelIDOrNode) {
+ return promisePanelEvent(panelIDOrNode, "popuphidden");
+}
+
+function promisePanelEvent(panelIDOrNode, eventType) {
+ return new Promise(resolve => {
+ let panel = panelIDOrNode;
+ if (typeof panel == "string") {
+ panel = 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);
+ return;
+ }
+ panel.addEventListener(
+ eventType,
+ () => {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function promisePageActionViewShown() {
+ info("promisePageActionViewShown waiting for ViewShown");
+ return BrowserTestUtils.waitForEvent(
+ BrowserPageActions.panelNode,
+ "ViewShown"
+ ).then(async event => {
+ let panelViewNode = event.originalTarget;
+ await promisePageActionViewChildrenVisible(panelViewNode);
+ return panelViewNode;
+ });
+}
+
+function promisePageActionViewChildrenVisible(panelViewNode) {
+ return promiseNodeVisible(panelViewNode.firstElementChild.firstElementChild);
+}
+
+function promiseNodeVisible(node) {
+ info(
+ `promiseNodeVisible waiting, node.id=${node.id} node.localeName=${node.localName}\n`
+ );
+ let dwu = window.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ let bounds = dwu.getBoundsWithoutFlushing(node);
+ if (bounds.width > 0 && bounds.height > 0) {
+ info(
+ `promiseNodeVisible OK, node.id=${node.id} node.localeName=${node.localName}\n`
+ );
+ return true;
+ }
+ return false;
+ });
+}
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml
new file mode 100644
index 0000000000..7e3e732ec6
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>page_action_menu_add_search_engine_0</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml
new file mode 100644
index 0000000000..d7306b3f91
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>page_action_menu_add_search_engine_1</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml
new file mode 100644
index 0000000000..eacd28334e
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>page_action_menu_add_search_engine_2</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html
new file mode 100644
index 0000000000..97efc667bd
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_404.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html
new file mode 100644
index 0000000000..a2e1c6bfa8
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_1" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_2" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html
new file mode 100644
index 0000000000..1ef425d523
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html
new file mode 100644
index 0000000000..281f4a610a
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_1" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/base/content/test/pageStyle/.eslintrc.js b/browser/base/content/test/pageStyle/.eslintrc.js
new file mode 100644
index 0000000000..7612459de1
--- /dev/null
+++ b/browser/base/content/test/pageStyle/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test", "plugin:mozilla/mochitest-test"],
+};
diff --git a/browser/base/content/test/pageStyle/browser.ini b/browser/base/content/test/pageStyle/browser.ini
new file mode 100644
index 0000000000..3fc190616b
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser.ini
@@ -0,0 +1,4 @@
+[browser_disable_author_style_oop.js]
+support-files =
+ page_style.html
+
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..bf2b1ffae2
--- /dev/null
+++ b/browser/base/content/test/pageStyle/browser_disable_author_style_oop.js
@@ -0,0 +1,75 @@
+"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;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() {
+ return new Promise(function(resolve) {
+ let e = content.document.createElement("iframe");
+ e.src =
+ "http://mochi.test:8888/browser/browser/base/content/test/pageStyle/page_style.html";
+ 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,
+ "http://example.com/browser/browser/base/content/test/pageStyle/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"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
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/pageinfo/.eslintrc.js b/browser/base/content/test/pageinfo/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/pageinfo/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/pageinfo/all_images.html b/browser/base/content/test/pageinfo/all_images.html
new file mode 100644
index 0000000000..c246e25519
--- /dev/null
+++ b/browser/base/content/test/pageinfo/all_images.html
@@ -0,0 +1,15 @@
+<html>
+ <head>
+ <title>Test for media tab</title>
+ <link rel='shortcut icon' href='dummy_icon.ico'>
+ </head>
+ <body style='background-image:url(about:logo?a);'>
+ <img src='dummy_image.gif'>
+ <ul>
+ <li style='list-style:url(about:logo?b);'>List Item 1</li>
+ </ul>
+ <div style='-moz-border-image: url(about:logo?c) 20 20 20 20;'>test</div>
+ <a href='' style='cursor: url(about:logo?d),default;'>test link</a>
+ <object type='image/svg+xml' width=20 height=20 data='dummy_object.svg'></object>
+ </body>
+</html>");
diff --git a/browser/base/content/test/pageinfo/browser.ini b/browser/base/content/test/pageinfo/browser.ini
new file mode 100644
index 0000000000..510817bddb
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser.ini
@@ -0,0 +1,24 @@
+[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_images.js]
+support-files =
+ all_images.html
+[browser_pageinfo_image_info.js]
+skip-if = (os == 'linux' && e10s) # bug 1161699
+[browser_pageinfo_permissions.js]
+[browser_pageinfo_security.js]
+support-files =
+ ../general/moz.png
+[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..de90004e5f
--- /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 loadend 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, "loadend");
+ } 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.loadURI(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..a9dd8f6480
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js
@@ -0,0 +1,30 @@
+/* Check proper media data retrieval in case of iframe */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+add_task(async function test_all_images_mentioned() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "iframes.html",
+ async function() {
+ let pageInfo = BrowserPageInfo(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab"
+ );
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ let imageTree = pageInfo.document.getElementById("imagetree");
+ let imageRowsNum = imageTree.view.rowCount;
+
+ ok(imageTree, "Image tree is null (media tab is broken)");
+ ok(
+ imageRowsNum == 2,
+ "Number of media items listed: " + imageRowsNum + ", should be 2"
+ );
+
+ pageInfo.close();
+ }
+ );
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js b/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
new file mode 100644
index 0000000000..026326e36a
--- /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, "loadend");
+ 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..d244268f7e
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_images.js
@@ -0,0 +1,31 @@
+/* Check proper image url retrieval from all kinds of elements/styles */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+add_task(async function test_all_images_mentioned() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "all_images.html",
+ async function() {
+ let pageInfo = BrowserPageInfo(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab"
+ );
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ let imageTree = pageInfo.document.getElementById("imagetree");
+ let imageRowsNum = imageTree.view.rowCount;
+
+ ok(imageTree, "Image tree is null (media tab is broken)");
+
+ ok(
+ imageRowsNum == 7,
+ "Number of images listed: " + imageRowsNum + ", should be 7"
+ );
+
+ 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..d87829d54f
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js
@@ -0,0 +1,261 @@
+const { SitePermissions } = ChromeUtils.import(
+ "resource:///modules/SitePermissions.jsm"
+);
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const TEST_ORIGIN = "https://example.com";
+const TEST_ORIGIN_CERT_ERROR = "https://expired.example.com";
+const LOW_TLS_VERSION = "https://tls1.example.com/";
+
+async function testPermissions(defaultPermission) {
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+
+ let defaultCheckbox = await TestUtils.waitForCondition(() =>
+ pageInfo.document.getElementById("geoDef")
+ );
+ let radioGroup = pageInfo.document.getElementById("geoRadioGroup");
+ let defaultRadioButton = pageInfo.document.getElementById(
+ "geo#" + defaultPermission
+ );
+ let blockRadioButton = pageInfo.document.getElementById("geo#2");
+
+ ok(defaultCheckbox.checked, "The default checkbox should be checked.");
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.DENY_ACTION
+ );
+
+ ok(!defaultCheckbox.checked, "The default checkbox should not be checked.");
+
+ defaultCheckbox.checked = true;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ ok(
+ !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
+ "Checking the default checkbox should reset the permission."
+ );
+
+ defaultCheckbox.checked = false;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ ok(
+ !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
+ "Unchecking the default checkbox should pick the default permission."
+ );
+ is(
+ radioGroup.selectedItem,
+ defaultRadioButton,
+ "The unknown radio button should be selected."
+ );
+
+ radioGroup.selectedItem = blockRadioButton;
+ blockRadioButton.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo")
+ .capability,
+ Services.perms.DENY_ACTION,
+ "Selecting a value in the radio group should set the corresponding permission"
+ );
+
+ radioGroup.selectedItem = defaultRadioButton;
+ defaultRadioButton.dispatchEvent(new Event("command"));
+
+ ok(
+ !PermissionTestUtils.getPermissionObject(gBrowser.currentURI, "geo"),
+ "Selecting the default value should reset the permission."
+ );
+ ok(defaultCheckbox.checked, "The default checkbox should be checked.");
+
+ pageInfo.close();
+ PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+ });
+}
+
+// Test displaying website permissions on certificate error pages.
+add_task(async function test_CertificateError() {
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ORIGIN_CERT_ERROR
+ );
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ await pageLoaded;
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN_CERT_ERROR, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let permissionTab = pageInfo.document.getElementById("permTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(permissionTab),
+ "Permission tab should be visible."
+ );
+
+ let hostText = pageInfo.document.getElementById("hostText");
+ let permList = pageInfo.document.getElementById("permList");
+ let excludedPermissions = pageInfo.window.getExcludedPermissions();
+ let permissions = SitePermissions.listPermissions().filter(
+ p =>
+ SitePermissions.getPermissionLabel(p) != null &&
+ !excludedPermissions.includes(p)
+ );
+
+ await TestUtils.waitForCondition(
+ () => hostText.value === browser.currentURI.displayPrePath,
+ `Value of owner should be "${browser.currentURI.displayPrePath}" instead got "${hostText.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => permList.childElementCount === permissions.length,
+ `Value of verifier should be ${permissions.length}, instead got ${permList.childElementCount}.`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying website permissions on network error pages.
+add_task(async function test_NetworkError() {
+ // Setup for TLS error
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+ Services.prefs.setIntPref("security.tls.version.min", 3);
+
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, LOW_TLS_VERSION);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ await pageLoaded;
+
+ let pageInfo = BrowserPageInfo(LOW_TLS_VERSION, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let permissionTab = pageInfo.document.getElementById("permTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(permissionTab),
+ "Permission tab should be visible."
+ );
+
+ let hostText = pageInfo.document.getElementById("hostText");
+ let permList = pageInfo.document.getElementById("permList");
+ let excludedPermissions = pageInfo.window.getExcludedPermissions();
+ let permissions = SitePermissions.listPermissions().filter(
+ p =>
+ SitePermissions.getPermissionLabel(p) != null &&
+ !excludedPermissions.includes(p)
+ );
+
+ await TestUtils.waitForCondition(
+ () => hostText.value === browser.currentURI.displayPrePath,
+ `Value of host should be should be "${browser.currentURI.displayPrePath}" instead got "${hostText.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => permList.childElementCount === permissions.length,
+ `Value of permissions list should be ${permissions.length}, instead got ${permList.childElementCount}.`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test some standard operations in the permission tab.
+add_task(async function test_geo_permission() {
+ await testPermissions(Services.perms.UNKNOWN_ACTION);
+});
+
+// Test some standard operations in the permission tab, falling back to a custom
+// default permission instead of UNKNOWN.
+add_task(async function test_default_geo_permission() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["permissions.default.geo", SitePermissions.ALLOW]],
+ });
+ await testPermissions(Services.perms.ALLOW_ACTION);
+});
+
+// Test special behavior for cookie permissions.
+add_task(async function test_cookie_permission() {
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+
+ let defaultCheckbox = await TestUtils.waitForCondition(() =>
+ pageInfo.document.getElementById("cookieDef")
+ );
+ let radioGroup = pageInfo.document.getElementById("cookieRadioGroup");
+ let allowRadioButton = pageInfo.document.getElementById("cookie#1");
+ let blockRadioButton = pageInfo.document.getElementById("cookie#2");
+
+ ok(defaultCheckbox.checked, "The default checkbox should be checked.");
+
+ defaultCheckbox.checked = false;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.ALLOW,
+ "Unchecking the default checkbox should pick the default permission."
+ );
+ is(
+ radioGroup.selectedItem,
+ allowRadioButton,
+ "The unknown radio button should be selected."
+ );
+
+ radioGroup.selectedItem = blockRadioButton;
+ blockRadioButton.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.BLOCK,
+ "Selecting a value in the radio group should set the corresponding permission"
+ );
+
+ radioGroup.selectedItem = allowRadioButton;
+ allowRadioButton.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.ALLOW,
+ "Selecting a value in the radio group should set the corresponding permission"
+ );
+ ok(!defaultCheckbox.checked, "The default checkbox should not be checked.");
+
+ defaultCheckbox.checked = true;
+ defaultCheckbox.dispatchEvent(new Event("command"));
+
+ is(
+ PermissionTestUtils.testPermission(gBrowser.currentURI, "cookie"),
+ SitePermissions.UNKNOWN,
+ "Checking the default checkbox should reset the permission."
+ );
+ is(
+ radioGroup.selectedItem,
+ null,
+ "For cookies, no item should be selected when the checkbox is checked."
+ );
+
+ pageInfo.close();
+ PermissionTestUtils.remove(gBrowser.currentURI, "cookie");
+ });
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_security.js b/browser/base/content/test/pageinfo/browser_pageinfo_security.js
new file mode 100644
index 0000000000..60c85b7c89
--- /dev/null
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_security.js
@@ -0,0 +1,364 @@
+ChromeUtils.defineModuleGetter(
+ this,
+ "SiteDataTestUtils",
+ "resource://testing-common/SiteDataTestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "DownloadUtils",
+ "resource://gre/modules/DownloadUtils.jsm"
+);
+
+const TEST_ORIGIN = "https://example.com";
+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;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ORIGIN_CERT_ERROR
+ );
+ let browser = gBrowser.selectedBrowser;
+ tab2 = gBrowser.selectedTab;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ await pageLoaded;
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN_CERT_ERROR, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ async function openAboutCertificate() {
+ let loaded = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+ let viewCertButton = pageInfoDoc.getElementById("security-view-cert");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(viewCertButton),
+ "view cert button should be visible."
+ );
+ viewCertButton.click();
+ await loaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
+ let certificateSection = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.querySelector("certificate-section");
+ }, "Certificate section found");
+
+ let commonName = certificateSection.shadowRoot
+ .querySelector(".subject-name")
+ .shadowRoot.querySelector(".common-name")
+ .shadowRoot.querySelector(".info").textContent;
+ is(
+ commonName,
+ "expired.example.com",
+ "Should have the same common name."
+ );
+ });
+
+ gBrowser.removeCurrentTab(); // closes about:certificate
+ }
+
+ await openAboutCertificate();
+
+ gBrowser.selectedTab = tab1;
+
+ await openAboutCertificate();
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Test displaying website identity information when loading images.
+add_task(async function test_image() {
+ let url = TEST_PATH + "moz.png";
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ let pageInfo = BrowserPageInfo(url, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be should be "This website does not supply ownership information." instead got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Mozilla Testing",
+ `Value of verifier should be "Mozilla Testing", instead got "${verifier.value}".`
+ );
+
+ let browser = gBrowser.selectedBrowser;
+
+ await TestUtils.waitForCondition(
+ () => domain.value === browser.currentURI.displayHost,
+ `Value of domain should be ${browser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying website identity information on certificate error pages.
+add_task(async function test_CertificateError() {
+ let browser;
+ let pageLoaded;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ORIGIN_CERT_ERROR
+ );
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+
+ await pageLoaded;
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN_CERT_ERROR, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be should be "This website does not supply ownership information." instead got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Mozilla Testing",
+ `Value of verifier should be "Mozilla Testing", instead got "${verifier.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => domain.value === browser.currentURI.displayHost,
+ `Value of domain should be ${browser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying website identity information on http pages.
+add_task(async function test_SecurityHTTP() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_HTTP_ORIGIN);
+
+ let pageInfo = BrowserPageInfo(TEST_HTTP_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be should be "This website does not supply ownership information." instead got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Not specified",
+ `Value of verifier should be "Not specified", instead got "${verifier.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => domain.value === gBrowser.selectedBrowser.currentURI.displayHost,
+ `Value of domain should be ${gBrowser.selectedBrowser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying valid certificate information in page info.
+add_task(async function test_ValidCert() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_ORIGIN);
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let owner = pageInfoDoc.getElementById("security-identity-owner-value");
+ let verifier = pageInfoDoc.getElementById("security-identity-verifier-value");
+ let domain = pageInfoDoc.getElementById("security-identity-domain-value");
+
+ await TestUtils.waitForCondition(
+ () => owner.value === "This website does not supply ownership information.",
+ `Value of owner should be "This website does not supply ownership information.", got "${owner.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => verifier.value === "Mozilla Testing",
+ `Value of verifier should be "Mozilla Testing", got "${verifier.value}".`
+ );
+
+ await TestUtils.waitForCondition(
+ () => domain.value === gBrowser.selectedBrowser.currentURI.displayHost,
+ `Value of domain should be ${gBrowser.selectedBrowser.currentURI.displayHost}, instead got "${domain.value}".`
+ );
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test displaying and removing quota managed data.
+add_task(async function test_SiteData() {
+ await SiteDataTestUtils.addToIndexedDB(TEST_ORIGIN);
+
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
+ let totalUsage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
+ Assert.greater(totalUsage, 0, "The total usage should not be 0");
+
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+
+ let label = pageInfoDoc.getElementById("security-privacy-sitedata-value");
+ let clearButton = pageInfoDoc.getElementById("security-clear-sitedata");
+
+ let size = DownloadUtils.convertByteUnits(totalUsage);
+
+ // The usage details are filled asynchronously, so we assert that they're present by
+ // waiting for them to be filled in.
+ // We only wait for the right unit to appear, since this number is intermittently
+ // varying by slight amounts on infra machines.
+ await TestUtils.waitForCondition(
+ () => label.textContent.includes(size[1]),
+ "Should show site data usage in the security section."
+ );
+ let siteDataUpdated = TestUtils.topicObserved(
+ "sitedatamanager:sites-updated"
+ );
+
+ let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ clearButton.click();
+ await removeDialogPromise;
+
+ await siteDataUpdated;
+
+ totalUsage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
+ is(totalUsage, 0, "The total usage should be 0");
+
+ await TestUtils.waitForCondition(
+ () => label.textContent == "No",
+ "Should show no site data usage in the security section."
+ );
+
+ pageInfo.close();
+ });
+});
+
+// Test displaying and removing cookies.
+add_task(async function test_Cookies() {
+ // Add some test cookies.
+ SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test1", "1");
+ SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test2", "2");
+ SiteDataTestUtils.addToCookies(TEST_SUB_ORIGIN, "test1", "1");
+
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+
+ let pageInfoDoc = pageInfo.document;
+
+ let label = pageInfoDoc.getElementById("security-privacy-sitedata-value");
+ let clearButton = pageInfoDoc.getElementById("security-clear-sitedata");
+
+ // The usage details are filled asynchronously, so we assert that they're present by
+ // waiting for them to be filled in.
+ await TestUtils.waitForCondition(
+ () => label.textContent.includes("cookies"),
+ "Should show cookies in the security section."
+ );
+
+ let cookiesCleared = TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted"
+ );
+
+ let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ clearButton.click();
+ await removeDialogPromise;
+
+ await cookiesCleared;
+
+ let uri = Services.io.newURI(TEST_ORIGIN);
+ is(
+ Services.cookies.countCookiesFromHost(uri.host),
+ 0,
+ "Cookies from the base domain should be cleared"
+ );
+
+ await TestUtils.waitForCondition(
+ () => label.textContent == "No",
+ "Should show no cookies in the security section."
+ );
+
+ pageInfo.close();
+ });
+});
+
+// Clean up in case we missed anything...
+add_task(async function cleanup() {
+ await SiteDataTestUtils.clear();
+});
diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js b/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js
new file mode 100644
index 0000000000..a654681107
--- /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.loadURI(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/.eslintrc.js b/browser/base/content/test/performance/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/performance/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/performance/StartupContentSubframe.jsm b/browser/base/content/test/performance/StartupContentSubframe.jsm
new file mode 100644
index 0000000000..79cd01d542
--- /dev/null
+++ b/browser/base/content/test/performance/StartupContentSubframe.jsm
@@ -0,0 +1,68 @@
+/* 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.
+ */
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var EXPORTED_SYMBOLS = [
+ "StartupContentSubframeParent",
+ "StartupContentSubframeChild",
+];
+
+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"
+ );
+ }
+}
+
+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.import(
+ "resource://gre/modules/AppConstants.jsm"
+ );
+ let collectStacks = AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG;
+
+ let components = {};
+ for (let component of Cu.loadedComponents) {
+ // Keep only the file name for components, as the path is an absolute file
+ // URL rather than a resource:// URL like for modules.
+ components[component.replace(/.*\//, "")] = collectStacks
+ ? Cu.getComponentLoadStack(component)
+ : "";
+ }
+
+ let modules = {};
+ for (let module of Cu.loadedModules) {
+ 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", {
+ components,
+ modules,
+ services,
+ });
+ }
+}
diff --git a/browser/base/content/test/performance/browser.ini b/browser/base/content/test/performance/browser.ini
new file mode 100644
index 0000000000..879745d831
--- /dev/null
+++ b/browser/base/content/test/performance/browser.ini
@@ -0,0 +1,58 @@
+[DEFAULT]
+# to avoid overhead when running the browser normally, startupRecorder.js 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.js
+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.available='on'
+ browser.urlbar.disableExtendForTests=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' && bits == 32) || (os == 'win' && processor == 'aarch64') # Bug 1382809, bug 1369959, Win32 because of intermittent OOM failures, bug 1533141 for aarch64
+[browser_preferences_usage.js]
+skip-if = !debug
+[browser_startup.js]
+[browser_startup_content.js]
+skip-if = !e10s
+support-files =
+ file_empty.html
+[browser_startup_content_subframe.js]
+skip-if = !fission
+support-files =
+ file_empty.html
+ StartupContentSubframe.jsm
+[browser_startup_flicker.js]
+run-if = debug || nightly_build # Requires startupRecorder.js, which isn't shipped everywhere by default
+[browser_startup_hiddenwindow.js]
+skip-if = os == 'mac'
+[browser_tabclose_grow.js]
+[browser_tabclose.js]
+skip-if = (os == 'win') || (os == 'mac') # Bug 1488537, Bug 1531417, Bug 1497713
+[browser_tabdetach.js]
+[browser_tabopen.js]
+skip-if = (verify && (os == 'mac'))
+[browser_tabopen_squeeze.js]
+[browser_tabstrip_overflow_underflow.js]
+skip-if = (verify && !debug && (os == 'win')) || (!debug && (os == 'win') && (bits == 32)) # Bug 1502255
+[browser_tabswitch.js]
+skip-if = os == 'win' #Bug 1455054
+[browser_toolbariconcolor_restyles.js]
+[browser_urlbar_keyed_search.js]
+skip-if = (os == 'win' && bits == 32) || (os == 'mac') # Disabled on Win32 because of intermittent OOM failures (bug 1448241), macosx1014 due to 1565619
+[browser_urlbar_search.js]
+skip-if = (debug || ccov) && (os == 'linux' || os == 'win') || (os == 'win' && bits == 32) # Disabled on Linux and Windows debug and ccov due to intermittent timeouts. Bug 1414126, bug 1426611. Disabled on Win32 because of intermittent OOM failures (bug 1448241)
+[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..9d4229e5ea
--- /dev/null
+++ b/browser/base/content/test/performance/browser_appmenu.js
@@ -0,0 +1,146 @@
+"use strict";
+/* global PanelUI */
+
+ChromeUtils.import(
+ "resource://testing-common/CustomizableUITestUtils.jsm",
+ this
+);
+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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_APPMENU_OPEN_REFLOWS = [
+ {
+ stack: [
+ "openPopup/this._openPopupPromise<@resource:///modules/PanelMultiView.jsm",
+ ],
+ },
+
+ {
+ stack: [
+ "_calculateMaxHeight@resource:///modules/PanelMultiView.jsm",
+ "handleEvent@resource:///modules/PanelMultiView.jsm",
+ ],
+
+ maxCount: 7, // This number should only ever go down - never up.
+ },
+];
+
+add_task(async function() {
+ await ensureNoPreloadedBrowser();
+ await disableFxaBadge();
+
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .getBoundingClientRect();
+ let menuButtonRect = document
+ .getElementById("PanelUI-menu-button")
+ .getBoundingClientRect();
+ let firstTabRect = gBrowser.selectedTab.getBoundingClientRect();
+ let frameExpectations = {
+ filter: rects =>
+ rects.filter(
+ r =>
+ !(
+ // We expect the menu button to get into the active state.
+ (
+ r.y1 >= menuButtonRect.top &&
+ r.y2 <= menuButtonRect.bottom &&
+ r.x1 >= menuButtonRect.left &&
+ r.x2 <= menuButtonRect.right
+ )
+ )
+ // XXX For some reason the menu panel isn't in our screenshots,
+ // but that's where we actually expect many changes.
+ ),
+ exceptions: [
+ {
+ name: "the urlbar placeholder 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 1547341 - a first tab gets drawn early",
+ condition: r =>
+ r.x1 >= firstTabRect.left &&
+ r.x2 <= firstTabRect.right &&
+ r.y1 >= firstTabRect.top &&
+ r.y2 <= firstTabRect.bottom,
+ },
+ ],
+ };
+
+ // 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_preferences_usage.js b/browser/base/content/test/performance/browser_preferences_usage.js
new file mode 100644
index 0000000000..b0a26011a0
--- /dev/null
+++ b/browser/base/content/test/performance/browser_preferences_usage.js
@@ -0,0 +1,275 @@
+/* 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": {
+ min: 200,
+ max: 350,
+ },
+ "layout.css.dpi": {
+ min: 45,
+ max: 81,
+ },
+ "network.loadinfo.skip_type_assertion": {
+ // This is accessed in debug only.
+ },
+ "extensions.getAddons.cache.enabled": {
+ min: 4,
+ max: 55,
+ },
+ "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 = {
+ "layout.css.dpi": {
+ max: 35,
+ },
+ "browser.zoom.full": {
+ min: 10,
+ max: 25,
+ },
+ "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,
+ "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() {
+ let max = 40;
+
+ let knownProblematicPrefs = {
+ "browser.zoom.full": {
+ min: 100,
+ max: 110,
+ },
+ "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: 50,
+ max: 51,
+ };
+ 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: 49,
+ 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: 100,
+ max: 101,
+ };
+ knownProblematicPrefs["gfx.content.azure.backends"] = {
+ min: 100,
+ max: 101,
+ };
+ // 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: 49,
+ max: 55,
+ };
+ knownProblematicPrefs["security.sandbox.logging.enabled"] = {
+ min: 49,
+ max: 55,
+ };
+ }
+ }
+
+ Services.prefs.resetStats();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com",
+ true,
+ true
+ );
+
+ let urls = [
+ "http://example.com/",
+ "https://example.com/",
+ "http://example.org/",
+ "https://example.org/",
+ ];
+
+ for (let i = 0; i < 50; i++) {
+ let url = urls[i % urls.length];
+ info(`Navigating to ${url}...`);
+ BrowserTestUtils.loadURI(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..2af066cbc0
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -0,0 +1,244 @@
+/* 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 components and 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.jsm' _onFirstWindowLoaded method).
+ "before profile selection": {
+ allowlist: {
+ modules: new Set([
+ "resource:///modules/BrowserGlue.jsm",
+ "resource://gre/modules/AppConstants.jsm",
+ "resource://gre/modules/ActorManagerParent.jsm",
+ "resource://gre/modules/ComponentUtils.jsm",
+ "resource://gre/modules/CustomElementsListener.jsm",
+ "resource://gre/modules/MainProcessSingleton.jsm",
+ "resource://gre/modules/XPCOMUtils.jsm",
+ "resource://gre/modules/Services.jsm",
+ ]),
+ },
+ },
+
+ // 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: {
+ components: new Set(["nsSearchService.js"]),
+ modules: new Set([
+ "chrome://webcompat/content/data/ua_overrides.jsm",
+ "chrome://webcompat/content/lib/ua_overrider.jsm",
+ "resource:///modules/AboutNewTab.jsm",
+ "resource:///modules/BrowserUsageTelemetry.jsm",
+ "resource:///modules/ContentCrashHandlers.jsm",
+ "resource:///modules/ShellService.jsm",
+ "resource://gre/modules/NewTabUtils.jsm",
+ "resource://gre/modules/PageThumbs.jsm",
+ "resource://gre/modules/PlacesUtils.jsm",
+ "resource://gre/modules/Promise.jsm", // imported by devtools during _delayedStartup
+ "resource://gre/modules/Preferences.jsm",
+ "resource://gre/modules/Sqlite.jsm",
+ ]),
+ 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: {
+ components: new Set([
+ "PageIconProtocolHandler.js",
+ "nsPlacesExpiration.js",
+ ]),
+ modules: new Set([
+ "resource://gre/modules/Blocklist.jsm",
+ // Bug 1391495 - BrowserWindowTracker.jsm is intermittently used.
+ // "resource:///modules/BrowserWindowTracker.jsm",
+ "resource://gre/modules/BookmarkHTMLUtils.jsm",
+ "resource://gre/modules/Bookmarks.jsm",
+ "resource://gre/modules/ContextualIdentityService.jsm",
+ "resource://gre/modules/CrashSubmit.jsm",
+ "resource://gre/modules/FxAccounts.jsm",
+ "resource://gre/modules/FxAccountsStorage.jsm",
+ "resource://gre/modules/PlacesBackups.jsm",
+ "resource://gre/modules/PlacesSyncUtils.jsm",
+ "resource://gre/modules/PushComponents.jsm",
+ ]),
+ services: new Set([
+ "@mozilla.org/browser/annotation-service;1",
+ "@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: {
+ components: new Set(["UnifiedComplete.js"]),
+ modules: new Set([
+ "resource://gre/modules/AsyncPrefs.jsm",
+ "resource://gre/modules/LoginManagerContextMenu.jsm",
+ "resource://pdf.js/PdfStreamConverter.jsm",
+ ]),
+ },
+ },
+};
+
+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.jsm"
+ );
+}
+
+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 componentStacks = new Map();
+ let data = Cu.cloneInto(startupRecorder.data.code, {});
+ // Keep only the file name for components, as the path is an absolute file
+ // URL rather than a resource:// URL like for modules.
+ for (let phase in data) {
+ data[phase].components = data[phase].components
+ .map(uri => {
+ let fileName = uri.replace(/.*\//, "");
+ componentStacks.set(fileName, Cu.getComponentLoadStack(uri));
+ return fileName;
+ })
+ .filter(c => c != "startupRecorder.js");
+ }
+
+ function getStack(scriptType, name) {
+ if (scriptType == "modules") {
+ return Cu.getModuleImportStack(name);
+ }
+ if (scriptType == "components") {
+ return componentStacks.get(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));
+ }
+ }
+ }
+ }
+ }
+});
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..1ebcb66654
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -0,0 +1,188 @@
+/* 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.
+ *
+ * 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.jsm",
+
+ // General utilities
+ "resource://gre/modules/AppConstants.jsm",
+ "resource://gre/modules/DeferredTask.jsm",
+ "resource://gre/modules/Services.jsm", // bug 1464542
+ "resource://gre/modules/Timer.jsm",
+ "resource://gre/modules/XPCOMUtils.jsm",
+
+ // Logging related
+ "resource://gre/modules/Log.jsm",
+
+ // Session store
+ "resource:///modules/sessionstore/ContentSessionStore.jsm",
+
+ // Browser front-end
+ "resource:///actors/AboutReaderChild.jsm",
+ "resource:///actors/BrowserTabChild.jsm",
+ "resource:///actors/LinkHandlerChild.jsm",
+ "resource:///actors/PageStyleChild.jsm",
+ "resource:///actors/SearchSERPTelemetryChild.jsm",
+ "resource://gre/modules/Readerable.jsm",
+
+ // Telemetry
+ "resource://gre/modules/TelemetryControllerBase.jsm", // bug 1470339
+ "resource://gre/modules/TelemetryControllerContent.jsm", // bug 1470339
+
+ // Extensions
+ "resource://gre/modules/ExtensionProcessScript.jsm",
+ "resource://gre/modules/ExtensionUtils.jsm",
+ "resource://gre/modules/MessageChannel.jsm",
+ ]),
+ frameScripts: new Set([
+ // Test related
+ "chrome://mochikit/content/shutdown-leaks-collector.js",
+
+ // Extensions
+ "resource://gre/modules/addons/Content.js",
+ ]),
+ 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.jsm",
+ "resource://gre/modules/sessionstore/Utils.jsm",
+
+ // Session store.
+ "resource://gre/modules/sessionstore/SessionHistory.jsm",
+
+ // Webcompat about:config front-end. This is part of a system add-on which
+ // may not load early enough for the test.
+ "resource://webcompat/AboutCompat.jsm",
+
+ // Test related
+ "resource://testing-common/BrowserTestUtilsChild.jsm",
+ "resource://testing-common/ContentEventListenerChild.jsm",
+ "resource://specialpowers/SpecialPowersChild.jsm",
+ "resource://specialpowers/WrapPrivileged.jsm",
+ ]),
+ frameScripts: new Set([]),
+ processScripts: new Set([
+ // Webcompat about:config front-end. This is presently nightly-only and
+ // part of a system add-on which may not load early enough for the test.
+ "resource://webcompat/aboutPageProcessScript.js",
+ ]),
+};
+
+const forbiddenScripts = {
+ services: new Set([
+ "@mozilla.org/base/telemetry-startup;1",
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1",
+ "@mozilla.org/push/Service;1",
+ ]),
+};
+
+add_task(async function() {
+ SimpleTest.requestCompleteLog();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url:
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://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.import(
+ "resource://gre/modules/AppConstants.jsm"
+ );
+ let collectStacks = AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG;
+ let components = {};
+ for (let component of Cu.loadedComponents) {
+ /* Keep only the file name for components, as the path is an absolute file
+ URL rather than a resource:// URL like for modules. */
+ components[component.replace(/.*\//, "")] = collectStacks
+ ? Cu.getComponentLoadStack(component)
+ : "";
+ }
+ let modules = {};
+ for (let module of Cu.loadedModules) {
+ 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", {
+ components,
+ 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] = "";
+ }
+
+ 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..d00d4cf9ef
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js
@@ -0,0 +1,447 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records I/O syscalls done on the main thread during startup.
+ *
+ * To run this test similar to try server, you need to run:
+ * ./mach package
+ * ./mach test --appname=dist <path to test>
+ *
+ * If you made changes that cause this test to fail, it's likely because you
+ * are touching more files or directories during startup.
+ * Most code has no reason to use main thread I/O.
+ * If for some reason accessing the file system on the main thread is currently
+ * unavoidable, consider defering the I/O as long as you can, ideally after
+ * the end of startup.
+ */
+
+"use strict";
+
+/* Set this to true only for debugging purpose; it makes the output noisy. */
+const kDumpAllStacks = false;
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+
+/* This is an object mapping string process types to lists of known cases
+ * of IO happening on the main thread. Ideally, IO should not be on the main
+ * thread, and should happen as late as possible (see above).
+ *
+ * Paths in the entries in these lists can:
+ * - be a full path, eg. "/etc/mime.types"
+ * - have a prefix which will be resolved using Services.dirsvc
+ * eg. "GreD:omni.ja"
+ * It's possible to have only a prefix, in thise case the directory will
+ * still be resolved, eg. "UAppData:"
+ * - use * at the begining and/or end as a wildcard
+ * The folder separator is '/' even for Windows paths, where it'll be
+ * automatically converted to '\'.
+ *
+ * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
+ * without this the test is strict and will fail if the described IO does not
+ * happen.
+ *
+ * Each entry specifies the maximum number of times an operation is expected to
+ * occur.
+ * The operations currently reported by the I/O interposer are:
+ * create/open: only supported on Windows currently. The test currently
+ * ignores these markers to have a shorter initial list of IO operations.
+ * Adding Unix support is bug 1533779.
+ * stat: supported on all platforms when checking the last modified date or
+ * file size. Supported only on Windows when checking if a file exists;
+ * fixing this inconsistency is bug 1536109.
+ * read: supported on all platforms, but unix platforms will only report read
+ * calls going through NSPR.
+ * write: supported on all platforms, but Linux will only report write calls
+ * going through NSPR.
+ * close: supported only on Unix, and only for close calls going through NSPR.
+ * Adding Windows support is bug 1524574.
+ * fsync: supported only on Windows.
+ *
+ * If an entry specifies more than one operation, if at least one of them is
+ * encountered, the test won't report a failure for the entry if other
+ * operations are not encountered. This helps when listing cases where the
+ * reported operations aren't the same on all platforms due to the I/O
+ * interposer inconsistencies across platforms documented above.
+ */
+const processes = {
+ "Web Content": [
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1357205
+ path: "XREAppFeat:formautofill@mozilla.org.xpi",
+ condition: !WIN,
+ ignoreIfUnused: true,
+ stat: 1,
+ },
+ {
+ path: "*ShaderCache*", // Bug 1660480 - seen on hardware
+ condition: WIN,
+ ignoreIfUnused: true,
+ stat: 3,
+ },
+ ],
+ "Privileged Content": [
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+ WebExtensions: [
+ {
+ path: "GreD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // bug 1376994
+ path: "XCurProcD:omni.ja",
+ condition: !WIN, // Visible on Windows with an open marker
+ stat: 1,
+ },
+ {
+ // Exists call in ScopedXREEmbed::SetAppDir
+ path: "XCurProcD:",
+ condition: WIN,
+ stat: 1,
+ },
+ ],
+};
+
+function expandPathWithDirServiceKey(path) {
+ if (path.includes(":")) {
+ let [prefix, suffix] = path.split(":");
+ let [key, property] = prefix.split(".");
+ let dir = Services.dirsvc.get(key, Ci.nsIFile);
+ if (property) {
+ dir = dir[property];
+ }
+
+ // Resolve symLinks.
+ let dirPath = dir.path;
+ while (dir && !dir.isSymlink()) {
+ dir = dir.parent;
+ }
+ if (dir) {
+ dirPath = dirPath.replace(dir.path, dir.target);
+ }
+
+ path = dirPath;
+
+ if (suffix) {
+ path += "/" + suffix;
+ }
+ }
+ if (AppConstants.platform == "win") {
+ path = path.replace(/\//g, "\\");
+ }
+ return path;
+}
+
+function getStackFromProfile(profile, stack) {
+ const stackPrefixCol = profile.stackTable.schema.prefix;
+ const stackFrameCol = profile.stackTable.schema.frame;
+ const frameLocationCol = profile.frameTable.schema.location;
+
+ let result = [];
+ while (stack) {
+ let sp = profile.stackTable.data[stack];
+ let frame = profile.frameTable.data[sp[stackFrameCol]];
+ stack = sp[stackPrefixCol];
+ frame = profile.stringTable[frame[frameLocationCol]];
+ if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
+ result.push(frame);
+ }
+ }
+ return result;
+}
+
+function getIOMarkersFromProfile(profile) {
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+
+ let markers = [];
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+
+ if (markerName != "FileIO") {
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (markerData.source == "sqlite-mainthread") {
+ continue;
+ }
+
+ let samples = markerData.stack.samples;
+ let stack = samples.data[0][samples.schema.stack];
+ markers.push({
+ operation: markerData.operation,
+ filename: markerData.filename,
+ source: markerData.source,
+ stackId: stack,
+ });
+ }
+
+ return markers;
+}
+
+function pathMatches(path, filename) {
+ path = path.toLowerCase();
+ return (
+ path == filename || // Full match
+ // Wildcard on both sides of the path
+ (path.startsWith("*") &&
+ path.endsWith("*") &&
+ filename.includes(path.slice(1, -1))) ||
+ // Wildcard suffix
+ (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
+ // Wildcard prefix
+ (path.startsWith("*") && filename.endsWith(path.slice(1)))
+ );
+}
+
+add_task(async function() {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ {
+ let omniJa = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ omniJa.append("omni.ja");
+ if (!omniJa.exists()) {
+ ok(
+ false,
+ "This test requires a packaged build, " +
+ "run 'mach package' and then use --appname=dist"
+ );
+ return;
+ }
+ }
+
+ 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, entry.canonicalize);
+ });
+ }
+
+ 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;
+ }
+
+ // Shared memory uses temporary files on MacOS <= 10.11 to avoid
+ // a kernel security bug that will never be patched (see
+ // https://crbug.com/project-zero/1671 for details). This can
+ // be removed when we no longer support those OS versions.
+ if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
+ continue;
+ }
+
+ let expected = false;
+ for (let entry of knownIOList) {
+ if (pathMatches(entry.path, filename)) {
+ entry[marker.operation] = (entry[marker.operation] || 0) - 1;
+ entry._used = true;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ record(
+ false,
+ `unexpected ${marker.operation} on ${marker.filename} in ${procName} process`,
+ undefined,
+ " " + getStackFromProfile(profile, marker.stackId).join("\n ")
+ );
+ shouldPass = false;
+ }
+ info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
+ if (kDumpAllStacks) {
+ info(
+ getStackFromProfile(profile, marker.stackId)
+ .map(f => " " + f)
+ .join("\n")
+ );
+ }
+ }
+
+ if (!knownIOList.length) {
+ continue;
+ }
+ // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have
+ // no I/O marker in that case, but it's good to keep the test running to check
+ // that we are still able to produce startup profiles.
+ is(
+ !!markers.length,
+ !AppConstants.RELEASE_OR_BETA,
+ procName +
+ " startup profiles should have IO markers in builds that are not RELEASE_OR_BETA"
+ );
+ if (!markers.length) {
+ // If a profile unexpectedly contains no I/O marker, it's better to return
+ // early to avoid having a lot of of confusing "no main thread IO when we
+ // expected some" failures.
+ continue;
+ }
+
+ for (let entry of knownIOList) {
+ for (let op in entry) {
+ if (
+ [
+ "listedPath",
+ "path",
+ "condition",
+ "ignoreIfUnused",
+ "_used",
+ ].includes(op)
+ ) {
+ continue;
+ }
+ let message = `${op} on ${entry.path} `;
+ if (entry[op] == 0) {
+ message += "as many times as expected";
+ } else if (entry[op] > 0) {
+ message += `allowed ${entry[op]} more times`;
+ } else {
+ message += `${entry[op] * -1} more times than expected`;
+ }
+ ok(entry[op] >= 0, `${message} in ${procName} process`);
+ }
+ if (!("_used" in entry) && !entry.ignoreIfUnused) {
+ ok(
+ false,
+ `no main thread IO when we expected some for process ${procName}: ${entry.path} (${entry.listedPath})`
+ );
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected main thread I/O during startup");
+ } else {
+ const filename = "profile_startup_content_mainthreadio.json";
+ let path = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment)
+ .get("MOZ_UPLOAD_DIR");
+ let encoder = new TextEncoder();
+ let profilePath = OS.Path.join(path, filename);
+ await OS.File.writeAtomic(
+ profilePath,
+ encoder.encode(JSON.stringify(startupRecorder.data.profile))
+ );
+ ok(
+ false,
+ "Unexpected main thread I/O behavior during child process startup; " +
+ `open the ${filename} artifact in the Firefox Profiler to see what happened`
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_content_subframe.js b/browser/base/content/test/performance/browser_startup_content_subframe.js
new file mode 100644
index 0000000000..112aa504bc
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_content_subframe.js
@@ -0,0 +1,149 @@
+/* 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.jsm";
+const subframeURI =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://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.jsm",
+ "resource://gre/modules/DeferredTask.jsm",
+ "resource://gre/modules/Services.jsm", // bug 1464542
+ "resource://gre/modules/XPCOMUtils.jsm",
+
+ // Logging related
+ "resource://gre/modules/Log.jsm",
+
+ // Browser front-end
+ "resource:///actors/PageStyleChild.jsm",
+
+ // Telemetry
+ "resource://gre/modules/TelemetryControllerBase.jsm", // bug 1470339
+ "resource://gre/modules/TelemetryControllerContent.jsm", // bug 1470339
+
+ // Extensions
+ "resource://gre/modules/ExtensionProcessScript.jsm",
+ "resource://gre/modules/ExtensionUtils.jsm",
+ "resource://gre/modules/MessageChannel.jsm",
+ ]),
+ 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.jsm",
+
+ // Test related
+ "resource://testing-common/BrowserTestUtilsChild.jsm",
+ "resource://testing-common/ContentEventListenerChild.jsm",
+ "resource://specialpowers/SpecialPowersChild.jsm",
+ "resource://specialpowers/WrapPrivileged.jsm",
+ ]),
+ 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: {
+ moduleURI: actorModuleURI,
+ },
+ child: {
+ moduleURI: 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] = "";
+ }
+
+ 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..4b30593f67
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_flicker.js
@@ -0,0 +1,92 @@
+/* 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() {
+ const isWebRenderEnabled = Services.prefs.getBoolPref("gfx.webrender.all");
+ const isFissionEnabled = SpecialPowers.useRemoteSubframes;
+ if (isFissionEnabled && !isWebRenderEnabled) {
+ // This configuration is not supported.
+ // Also, in this specific configuration, we're displaying a warning, which looks like a flicker.
+ // Deactivating test.
+ ok(
+ true,
+ "Detected Fission without WebRender. Flicker expected, deactivating flicker test"
+ );
+ return;
+ }
+
+ 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);
+
+ // The first screenshot we get in OSX / Windows shows an unfocused browser
+ // window for some reason. See bug 1445161.
+ //
+ // We'll assume the changes we are seeing are due to this focus change if
+ // there are at least 5 areas that changed near the top of the screen, but
+ // will only ignore this once (hence the alreadyFocused variable).
+ if (!alreadyFocused && rects.length > 5 && rects.every(r => r.y2 < 100)) {
+ alreadyFocused = true;
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ continue;
+ }
+
+ rects = rects.filter(rect => {
+ let width = frame.width;
+
+ let exceptions = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+ ];
+
+ 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..aa7f0aea92
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_hiddenwindow.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function() {
+ if (
+ !AppConstants.NIGHTLY_BUILD &&
+ !AppConstants.MOZ_DEV_EDITION &&
+ !AppConstants.DEBUG
+ ) {
+ ok(
+ !("@mozilla.org/test/startuprecorder;1" in Cc),
+ "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
+ "non-debug build."
+ );
+ return;
+ }
+
+ let startupRecorder = Cc["@mozilla.org/test/startuprecorder;1"].getService()
+ .wrappedJSObject;
+ await startupRecorder.done;
+
+ let extras = Cu.cloneInto(startupRecorder.data.extras, {});
+
+ let phasesExpectations = {
+ "before profile selection": false,
+ "before opening first browser window": false,
+ "before first paint": !Services.prefs.getBoolPref(
+ "toolkit.lazyHiddenWindow"
+ ),
+
+ // Bug 1531854
+ "before handling user events": true,
+ "before becoming idle": true,
+ };
+
+ for (let phase in extras) {
+ if (!(phase in phasesExpectations)) {
+ ok(false, `Startup phase '${phase}' should be specified.`);
+ continue;
+ }
+
+ is(
+ extras[phase].hiddenWindowLoaded,
+ phasesExpectations[phase],
+ `Hidden window loaded at '${phase}': ${phasesExpectations[phase]}`
+ );
+ }
+});
diff --git a/browser/base/content/test/performance/browser_startup_images.js b/browser/base/content/test/performance/browser_startup_images.js
new file mode 100644
index 0000000000..0d713394f3
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_images.js
@@ -0,0 +1,134 @@
+/* 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: "resource://gre-resources/loading-image.png",
+ platforms: ["win", "macosx"],
+ intermittentNotLoaded: ["win", "macosx"],
+ },
+ {
+ file: "resource://gre-resources/broken-image.png",
+ platforms: ["win", "macosx"],
+ intermittentNotLoaded: ["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 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..5c156b25c7
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_mainthreadio.js
@@ -0,0 +1,890 @@
+/* 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";
+
+const { AddonManager } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+/* 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
+ * - For Windows specific entries that require resolving the path to its
+ * canonical form, ie. the old DOS 8.3 format, use canonicalize: true.
+ * This is needed for stat calls to non-existent files.
+ * 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,
+ },
+ {
+ // bug 1546838
+ path: "ProfD:xulstore/data.mdb",
+ condition: WIN,
+ read: 1,
+ write: 1,
+ },
+ ],
+
+ "before opening first browser window": [
+ {
+ // bug 1541226
+ path: "ProfD:",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-journal",
+ condition: !LINUX,
+ stat: 3,
+ write: 4,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite",
+ condition: !LINUX,
+ stat: 2,
+ read: 3,
+ write: 1,
+ },
+ {
+ // bug 1534745
+ path: "ProfD:cookies.sqlite-wal",
+ 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",
+ condition: WIN,
+ stat: 1,
+ },
+ {
+ // bug 1541246
+ path: "UAppData:",
+ ignoreIfUnused: true, // sometimes before opening first browser window,
+ // sometimes before first paint
+ condition: WIN,
+ 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,
+ },
+ {
+ // We only hit this for new profiles.
+ path: "XREAppDist:distribution.ini",
+ condition: WIN,
+ stat: 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 1546838
+ path: "ProfD:xulstore/data.mdb",
+ condition: MAC,
+ write: 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": [
+ {
+ path: "GreD:update.test",
+ ignoreIfUnused: true,
+ condition: LINUX,
+ close: 1,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:cert9.db",
+ condition: WIN,
+ read: 5,
+ stat: 2,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:cert9.db",
+ condition: WIN,
+ ignoreIfUnused: true, // if canonicalize(ProfD) == ProfD, we'll use the previous entry.
+ canonicalize: true,
+ stat: 2,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:cert9.db-journal",
+ condition: WIN,
+ canonicalize: true,
+ stat: 3,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:cert9.db-wal",
+ condition: WIN,
+ canonicalize: true,
+ 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: 8,
+ stat: 2,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:key4.db",
+ condition: WIN,
+ ignoreIfUnused: true, // if canonicalize(ProfD) == ProfD, we'll use the previous entry.
+ canonicalize: true,
+ stat: 2,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:key4.db-journal",
+ condition: WIN,
+ canonicalize: true,
+ stat: 5,
+ },
+ {
+ // bug 1370516 - NSS should be initialized off main thread.
+ path: "ProfD:key4.db-wal",
+ condition: WIN,
+ canonicalize: true,
+ stat: 5,
+ },
+ {
+ 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,
+ },
+ ],
+
+ // 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": [
+ {
+ 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: 36,
+ write: 148,
+ },
+ {
+ // 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: 1310,
+ },
+ {
+ // 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:key4.db-journal",
+ condition: WIN,
+ canonicalize: true,
+ stat: 2,
+ },
+ {
+ path: "ProfD:key4.db-wal",
+ condition: WIN,
+ canonicalize: true,
+ stat: 2,
+ },
+ {
+ 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, canonicalize = false) {
+ 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];
+ }
+
+ if (canonicalize) {
+ path = dir.QueryInterface(Ci.nsILocalFileWin).canonicalPath;
+ } else {
+ // 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;
+ }
+
+ {
+ let omniJa = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ omniJa.append("omni.ja");
+ if (!omniJa.exists()) {
+ ok(
+ false,
+ "This test requires a packaged build, " +
+ "run 'mach package' and then use --appname=dist"
+ );
+ return;
+ }
+ }
+
+ 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, entry.canonicalize);
+ });
+ }
+
+ 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",
+ "canonicalize",
+ "ignoreIfUnused",
+ "_used",
+ ].includes(op)
+ ) {
+ continue;
+ }
+ let message = `${op} on ${entry.path} `;
+ if (entry[op] == 0) {
+ message += "as many times as expected";
+ } else if (entry[op] > 0) {
+ message += `allowed ${entry[op]} more times`;
+ } else {
+ message += `${entry[op] * -1} more times than expected`;
+ }
+ ok(entry[op] >= 0, `${message} ${phase}`);
+ }
+ if (!("_used" in entry) && !entry.ignoreIfUnused) {
+ ok(
+ false,
+ `no main thread IO when we expected some during ${phase}: ${entry.path} (${entry.listedPath})`
+ );
+ shouldPass = false;
+ }
+ }
+ }
+
+ if (shouldPass) {
+ ok(shouldPass, "No unexpected main thread I/O during startup");
+ } else {
+ const filename = "profile_startup_mainthreadio.json";
+ let path = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment)
+ .get("MOZ_UPLOAD_DIR");
+ let encoder = new TextEncoder();
+ let profilePath = OS.Path.join(path, filename);
+ await OS.File.writeAtomic(
+ profilePath,
+ encoder.encode(JSON.stringify(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..14a48806b4
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_syncIPC.js
@@ -0,0 +1,418 @@
+/* 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 == "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 && !WEBRENDER) || LINUX,
+ maxCount: 1,
+ },
+ {
+ name: "PLayerTransaction::Msg_GetTextureFactoryIdentifier",
+ condition: WIN && !WEBRENDER,
+ maxCount: 3,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: WIN && WEBRENDER,
+ maxCount: 2,
+ },
+ {
+ name: "PWebRenderBridge::Msg_EnsureConnected",
+ condition: MAC && WEBRENDER,
+ maxCount: 1,
+ },
+ {
+ // bug 1373773
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ condition: !WIN,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_NotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_MapAndNotifyChildCreated",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 2,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: MAC,
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_FlushRendering",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win7 32
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 3,
+ },
+ {
+ name: "PCompositorWidget::Msg_Initialize",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 3,
+ },
+ {
+ name: "PGPU::Msg_AddLayerTreeIdMapping",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 5,
+ },
+ {
+ name: "PCompositorBridge::Msg_MakeSnapshot",
+ condition: WIN && !WEBRENDER,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PWebRenderBridge::Msg_GetSnapshot",
+ condition: WIN && WEBRENDER,
+ ignoreIfUnused: true, // Sometimes in the next phase on Windows10 QR
+ maxCount: 1,
+ },
+ {
+ name: "PCompositorBridge::Msg_WillClose",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 2,
+ },
+ {
+ name: "PAPZInputBridge::Msg_ProcessUnhandledEvent",
+ condition: WIN,
+ ignoreIfUnused: true, // Only on Win10 64
+ maxCount: 1,
+ },
+ {
+ name: "PGPU::Msg_GetDeviceStatus",
+ // bug 1553740 might want to drop the WEBRENDER clause here.
+ // Additionally, the skeleton UI causes us to attach "before first paint" to a
+ // later event, which lets this sneak in.
+ condition: WIN && (WEBRENDER || SKELETONUI),
+ // If Init() completes before we call EnsureGPUReady we won't send GetDeviceStatus
+ // so we can safely ignore if unused.
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ ],
+
+ // 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: !WIN,
+ ignoreIfUnused: true,
+ maxCount: 1,
+ },
+ {
+ 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,
+ },
+ ],
+
+ // 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 && 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 || LINUX || SKELETONUI,
+ ignoreIfUnused: true,
+ 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,
+ },
+ ],
+};
+
+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;
+
+ 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.type != "IPC" ||
+ !markerData.sync ||
+ markerData.direction != "sending"
+ ) {
+ continue;
+ }
+
+ markersForCurrentPhase.push(markerData.messageType);
+ }
+ }
+
+ 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.maxCount = (entry.maxCount || 0) - 1;
+ entry._used = true;
+ expected = true;
+ break;
+ }
+ }
+ if (!expected) {
+ ok(false, `unexpected ${marker} sync IPC ${phase}`);
+ shouldPass = false;
+ }
+ }
+
+ for (let entry of knownIPCList) {
+ let message = `sync IPC ${entry.name} `;
+ if (entry.maxCount == 0) {
+ message += "happened as many times as expected";
+ } else if (entry.maxCount > 0) {
+ message += `allowed ${entry.maxCount} more times`;
+ } else {
+ message += `${entry.maxCount * -1} more times than expected`;
+ shouldPass = false;
+ }
+ ok(entry.maxCount >= 0, `${message} ${phase}`);
+
+ if (!("_used" in entry) && !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 = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment)
+ .get("MOZ_UPLOAD_DIR");
+ let encoder = new TextEncoder();
+ let profilePath = OS.Path.join(path, filename);
+ await OS.File.writeAtomic(
+ profilePath,
+ encoder.encode(JSON.stringify(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..3067fa2685
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabclose.js
@@ -0,0 +1,106 @@
+"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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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 == 14 && r.w <= 2 * 14 + 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..7cca76f1a4
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabclose_grow.js
@@ -0,0 +1,93 @@
+"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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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();
+
+ await withPerfObserver(
+ async function() {
+ let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
+ let tab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ gBrowser.removeTab(tab, { animate: true, byMouse: true });
+ await BrowserTestUtils.waitForEvent(tab, "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)
+ )
+ )
+ ),
+ },
+ }
+ );
+
+ 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..0788b8b0dd
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabdetach.js
@@ -0,0 +1,116 @@
+"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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ {
+ stack: [
+ "clientX@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: [
+ "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..37b7ad3844
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabopen.js
@@ -0,0 +1,143 @@
+/* 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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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;
+
+ 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 firstTabLabelRect = gBrowser.selectedTab.textLabel.getBoundingClientRect();
+ let textBoxRect = gURLBar
+ .querySelector("moz-input-box")
+ .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");
+ BrowserOpenTab();
+ await BrowserTestUtils.waitForEvent(
+ gBrowser.selectedTab,
+ "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 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.
+ ((inRange(r.w, firstTabRect.width, firstTabRect.width * 2) &&
+ r.x1 == firstTabRect.x) ||
+ // The second tab gets painted several times due to tabopen animation.
+ (inRange(
+ r.x1,
+ firstTabRect.right - 1, // -1 for the border on Win7
+ firstTabRect.right + firstTabRect.width
+ ) &&
+ r.x2 < firstTabRect.right + firstTabRect.width + 25) || // The + 25 is because sometimes the '+' is in the same rect.
+ // 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 == 14 && r.w <= 2 * 14 + kMaxEmptyPixels) ||
+ // We sometimes have a rect for the right most 2px of the '+' button.
+ (r.h == 2 && r.w == 2) ||
+ // Same for the 'X' icon.
+ (r.h == 10 && r.w <= 2 * 10))
+ )
+ )
+ ),
+ 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..4e280cda49
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabopen_squeeze.js
@@ -0,0 +1,99 @@
+"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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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..7d0eeda677
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
@@ -0,0 +1,199 @@
+"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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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..de67bbe330
--- /dev/null
+++ b/browser/base/content/test/performance/browser_tabswitch.js
@@ -0,0 +1,112 @@
+/* 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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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() {
+ 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 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,
+ (origTab.clientWidth - 1) * 2, // -1 for the border on Win7
+ origTab.clientWidth * 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..041c46c652
--- /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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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..2b14cd161f
--- /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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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_window_resize.js b/browser/base/content/test/performance/browser_window_resize.js
new file mode 100644
index 0000000000..b47bca95a1
--- /dev/null
+++ b/browser/base/content/test/performance/browser_window_resize.js
@@ -0,0 +1,131 @@
+/* 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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * 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("")
+ .map((_, i) => ({ url: `http://test.places.${i}/` })),
+ });
+
+ let wasCollapsed = gToolbar.collapsed;
+ Assert.ok(wasCollapsed, "The toolbar is collapsed by default");
+ if (wasCollapsed) {
+ let promiseReady = BrowserTestUtils.waitForEvent(
+ gToolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ await toggleBookmarksToolbar(true);
+ await promiseReady;
+ }
+
+ registerCleanupFunction(async () => {
+ if (wasCollapsed) {
+ await toggleBookmarksToolbar(false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+
+ let win = await prepareSettledWindow();
+
+ if (
+ win.screen.availWidth < STARTING_WIDTH ||
+ win.screen.availHeight < STARTING_HEIGHT
+ ) {
+ Assert.ok(
+ false,
+ "This test is running on too small a display - " +
+ `(${STARTING_WIDTH}x${STARTING_HEIGHT} min)`
+ );
+ return;
+ }
+
+ await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+
+ await withPerfObserver(
+ async function() {
+ await resizeWindow(win, SMALL_WIDTH, SMALL_HEIGHT);
+ await resizeWindow(win, STARTING_WIDTH, STARTING_HEIGHT);
+ },
+ { expectedReflows: EXPECTED_REFLOWS, frames: { filter: () => [] } },
+ win
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/performance/browser_windowclose.js b/browser/base/content/test/performance/browser_windowclose.js
new file mode 100644
index 0000000000..7426a4c298
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowclose.js
@@ -0,0 +1,58 @@
+"use strict";
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS.
+ * Instead of adding reflows to the list, you should be modifying your code to
+ * avoid the reflow.
+ *
+ * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when closing windows. When the
+ * window is closed, the test waits until the original window
+ * has activated.
+ */
+add_task(async function() {
+ // Ensure that this browser window starts focused. This seems to be
+ // necessary to avoid intermittent failures when running this test
+ // on repeat.
+ await new Promise(resolve => {
+ waitForFocus(resolve, window);
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await new Promise(resolve => {
+ waitForFocus(resolve, win);
+ });
+
+ // At the time of writing, there are no reflows on window closing.
+ // Mochitest will fail if we have no assertions, so we add one here
+ // to make sure nobody adds any new ones.
+ Assert.equal(
+ EXPECTED_REFLOWS.length,
+ 0,
+ "We shouldn't have added any new expected reflows for window close."
+ );
+
+ await withPerfObserver(
+ async function() {
+ let promiseOrigBrowserFocused = TestUtils.waitForCondition(() => {
+ return Services.focus.activeWindow == window;
+ });
+ await BrowserTestUtils.closeWindow(win);
+ await promiseOrigBrowserFocused;
+ },
+ {
+ expectedReflows: EXPECTED_REFLOWS,
+ },
+ win
+ );
+});
diff --git a/browser/base/content/test/performance/browser_windowopen.js b/browser/base/content/test/performance/browser_windowopen.js
new file mode 100644
index 0000000000..c149c0f245
--- /dev/null
+++ b/browser/base/content/test/performance/browser_windowopen.js
@@ -0,0 +1,183 @@
+/* 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://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+const EXPECTED_REFLOWS = [
+ /**
+ * Nothing here! Please don't add anything new!
+ */
+];
+
+// We'll assume the changes we are seeing are due to this focus change if
+// there are at least 5 areas that changed near the top of the screen, or if
+// the toolbar background is involved on OSX, but will only ignore this once.
+function isLikelyFocusChange(rects) {
+ if (rects.length > 5 && rects.every(r => r.y2 < 100)) {
+ return true;
+ }
+ if (
+ Services.appinfo.OS == "Darwin" &&
+ rects.length == 2 &&
+ rects.every(r => r.y1 == 0 && r.h == 33)
+ ) {
+ return true;
+ }
+ return false;
+}
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows or flickering areas when opening new windows.
+ */
+add_task(async function() {
+ // Flushing all caches helps to ensure that we get consistent
+ // behaviour when opening a new window, even if windows have been
+ // opened in previous tests.
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ Services.obs.notifyObservers(null, "chrome-flush-caches");
+
+ let bookmarksToolbarRect = await getBookmarksToolbarRect();
+
+ let win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no,remote,suppressanimation",
+ "about:home"
+ );
+
+ await disableFxaBadge();
+
+ let alreadyFocused = false;
+ let inRange = (val, min, max) => min <= val && val <= max;
+ let expectations = {
+ expectedReflows: EXPECTED_REFLOWS,
+ frames: {
+ filter(rects, frame, previousFrame) {
+ // The first screenshot we get in OSX / Windows shows an unfocused browser
+ // window for some reason. See bug 1445161.
+ if (!alreadyFocused && isLikelyFocusChange(rects)) {
+ alreadyFocused = true;
+ todo(
+ false,
+ "bug 1445161 - the window should be focused at first paint, " +
+ rects.toSource()
+ );
+ return [];
+ }
+
+ return rects;
+ },
+ exceptions: [
+ {
+ name: "bug 1421463 - reload toolbar icon shouldn't flicker",
+ condition: r =>
+ inRange(r.h, 13, 14) &&
+ inRange(r.w, 14, 16) && // icon size
+ inRange(r.y1, 40, 80) && // in the toolbar
+ inRange(r.x1, 65, 100), // near the left side of the screen
+ },
+ {
+ name: "bug 1555842 - the urlbar shouldn't flicker",
+ condition: r => {
+ let inputFieldRect = win.gURLBar.inputField.getBoundingClientRect();
+
+ return (
+ (!AppConstants.DEBUG ||
+ (AppConstants.platform == "linux" && AppConstants.ASAN)) &&
+ r.x1 >= inputFieldRect.left &&
+ r.x2 <= inputFieldRect.right &&
+ r.y1 >= inputFieldRect.top &&
+ r.y2 <= inputFieldRect.bottom
+ );
+ },
+ },
+ {
+ name: "Initial bookmark icon appearing after startup",
+ condition: r =>
+ r.w == 16 &&
+ r.h == 16 && // icon size
+ inRange(
+ r.y1,
+ bookmarksToolbarRect.top,
+ bookmarksToolbarRect.top + bookmarksToolbarRect.height / 2
+ ) && // in the toolbar
+ inRange(r.x1, 11, 13), // very close to the left of the screen
+ },
+ {
+ // Note that the length and x values here are a bit weird because on
+ // some fonts, we appear to detect the two words separately.
+ name:
+ "Initial bookmark text ('Getting Started' or 'Get Involved') appearing after startup",
+ condition: r =>
+ inRange(r.w, 25, 120) && // length of text
+ inRange(r.h, 9, 15) && // height of text
+ inRange(
+ r.y1,
+ bookmarksToolbarRect.top,
+ bookmarksToolbarRect.top + bookmarksToolbarRect.height / 2
+ ) && // in the toolbar
+ inRange(r.x1, 30, 90), // close to the left of the screen
+ },
+ ],
+ },
+ };
+
+ await withPerfObserver(
+ async function() {
+ // Avoid showing the remotecontrol UI.
+ await new Promise(resolve => {
+ win.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ delete win.Marionette;
+ win.Marionette = { running: false };
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == win
+ );
+
+ let promises = [
+ BrowserTestUtils.firstBrowserLoaded(win, false),
+ BrowserTestUtils.browserStopped(
+ win.gBrowser.selectedBrowser,
+ "about:home"
+ ),
+ ];
+
+ await Promise.all(promises);
+
+ await new Promise(resolve => {
+ // 10 is an arbitrary value here, it needs to be at least 2 to avoid
+ // races with code initializing itself using idle callbacks.
+ (function waitForIdle(count = 10) {
+ if (!count) {
+ resolve();
+ return;
+ }
+ Services.tm.idleDispatchToMainThread(() => {
+ waitForIdle(count - 1);
+ });
+ })();
+ });
+ },
+ expectations,
+ win
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/performance/file_empty.html b/browser/base/content/test/performance/file_empty.html
new file mode 100644
index 0000000000..865879c583
--- /dev/null
+++ b/browser/base/content/test/performance/file_empty.html
@@ -0,0 +1 @@
+<!-- this file intentionally left blank -->
diff --git a/browser/base/content/test/performance/head.js b/browser/base/content/test/performance/head.js
new file mode 100644
index 0000000000..3b268dfbb9
--- /dev/null
+++ b/browser/base/content/test/performance/head.js
@@ -0,0 +1,923 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+/**
+ * This function can be called if the test needs to trigger frame dirtying
+ * outside of the normal mechanism.
+ *
+ * @param win (dom window)
+ * The window in which the frame tree needs to be marked as dirty.
+ */
+function dirtyFrame(win) {
+ let dwu = win.windowUtils;
+ try {
+ dwu.ensureDirtyRootFrame();
+ } catch (e) {
+ // If this fails, we should probably make note of it, but it's not fatal.
+ info("Note: ensureDirtyRootFrame threw an exception:" + e);
+ }
+}
+
+/**
+ * Async utility function to collect the stacks of uninterruptible reflows
+ * occuring during some period of time in a window.
+ *
+ * @param testPromise (Promise)
+ * A promise that is resolved when the data collection should stop.
+ *
+ * @param win (browser window, optional)
+ * The browser window to monitor. Defaults to the current window.
+ *
+ * @return An array of reflow stacks
+ */
+async function recordReflows(testPromise, win = window) {
+ // Collect all reflow stacks, we'll process them later.
+ let reflows = [];
+
+ let observer = {
+ reflow(start, end) {
+ // Gather information about the current code path.
+ reflows.push(new Error().stack);
+
+ // Just in case, dirty the frame now that we've reflowed.
+ dirtyFrame(win);
+ },
+
+ reflowInterruptible(start, end) {
+ // Interruptible reflows are the reflows caused by the refresh
+ // driver ticking. These are fine.
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIReflowObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ let docShell = win.docShell;
+ docShell.addWeakReflowObserver(observer);
+
+ let dirtyFrameFn = event => {
+ if (event.type != "MozAfterPaint") {
+ dirtyFrame(win);
+ }
+ };
+ Services.els.addListenerForAllEvents(win, dirtyFrameFn, true);
+
+ try {
+ dirtyFrame(win);
+ await testPromise;
+ } finally {
+ Services.els.removeListenerForAllEvents(win, dirtyFrameFn, true);
+ docShell.removeWeakReflowObserver(observer);
+ }
+
+ return reflows;
+}
+
+/**
+ * Utility function to report unexpected reflows.
+ *
+ * @param reflows (Array)
+ * An array of reflow stacks returned by recordReflows.
+ *
+ * @param expectedReflows (Array, optional)
+ * An Array of Objects representing reflows.
+ *
+ * Example:
+ *
+ * [
+ * {
+ * // This reflow is caused by lorem ipsum.
+ * // Sometimes, due to unpredictable timings, the reflow may be hit
+ * // less times.
+ * stack: [
+ * "select@chrome://global/content/bindings/textbox.xml",
+ * "focusAndSelectUrlBar@chrome://browser/content/browser.js",
+ * "openLinkIn@chrome://browser/content/utilityOverlay.js",
+ * "openUILinkIn@chrome://browser/content/utilityOverlay.js",
+ * "BrowserOpenTab@chrome://browser/content/browser.js",
+ * ],
+ * // We expect this particular reflow to happen up to 2 times.
+ * maxCount: 2,
+ * },
+ *
+ * {
+ * // This reflow is caused by lorem ipsum. We expect this reflow
+ * // to only happen once, so we can omit the "maxCount" property.
+ * stack: [
+ * "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml",
+ * "_fillTrailingGap@chrome://browser/content/tabbrowser.xml",
+ * "_handleNewTab@chrome://browser/content/tabbrowser.xml",
+ * "onxbltransitionend@chrome://browser/content/tabbrowser.xml",
+ * ],
+ * }
+ * ]
+ *
+ * Note that line numbers are not included in the stacks.
+ *
+ * Order of the reflows doesn't matter. Expected reflows that aren't seen
+ * will cause an assertion failure. When this argument is not passed,
+ * it defaults to the empty Array, meaning no reflows are expected.
+ */
+function reportUnexpectedReflows(reflows, expectedReflows = []) {
+ let knownReflows = expectedReflows.map(r => {
+ return {
+ stack: r.stack,
+ path: r.stack.join("|"),
+ count: 0,
+ maxCount: r.maxCount || 1,
+ actualStacks: new Map(),
+ };
+ });
+ let unexpectedReflows = new Map();
+
+ if (knownReflows.some(r => r.path.includes("*"))) {
+ Assert.ok(
+ false,
+ "Do not include async frames in the stack, as " +
+ "that feature is not available on all trees."
+ );
+ }
+
+ for (let stack of reflows) {
+ let path = stack
+ .split("\n")
+ .slice(1) // the first frame which is our test code.
+ .map(line => line.replace(/:\d+:\d+$/, "")) // strip line numbers.
+ .join("|");
+
+ // Stack trace is empty. Reflow was triggered by native code, which
+ // we ignore.
+ if (path === "") {
+ continue;
+ }
+
+ // Functions from EventUtils.js calculate coordinates and
+ // dimensions, causing us to reflow. That's the test
+ // harness and we don't care about that, so we'll filter that out.
+ if (
+ /^(synthesize|send|createDragEventObject).*?@chrome:\/\/mochikit.*?EventUtils\.js/.test(
+ path
+ )
+ ) {
+ continue;
+ }
+
+ let index = knownReflows.findIndex(reflow => path.startsWith(reflow.path));
+ if (index != -1) {
+ let reflow = knownReflows[index];
+ ++reflow.count;
+ reflow.actualStacks.set(stack, (reflow.actualStacks.get(stack) || 0) + 1);
+ } else {
+ unexpectedReflows.set(stack, (unexpectedReflows.get(stack) || 0) + 1);
+ }
+ }
+
+ let formatStack = stack =>
+ stack
+ .split("\n")
+ .slice(1)
+ .map(frame => " " + frame)
+ .join("\n");
+ for (let reflow of knownReflows) {
+ let firstFrame = reflow.stack[0];
+ if (!reflow.count) {
+ Assert.ok(
+ false,
+ `Unused expected reflow at ${firstFrame}:\nStack:\n` +
+ reflow.stack.map(frame => " " + frame).join("\n") +
+ "\n" +
+ "This is probably a good thing - just remove it from the list of reflows."
+ );
+ } else {
+ if (reflow.count > reflow.maxCount) {
+ Assert.ok(
+ false,
+ `reflow at ${firstFrame} was encountered ${reflow.count} times,\n` +
+ `it was expected to happen up to ${reflow.maxCount} times.`
+ );
+ } else {
+ todo(
+ false,
+ `known reflow at ${firstFrame} was encountered ${reflow.count} times`
+ );
+ }
+ for (let [stack, count] of reflow.actualStacks) {
+ info(
+ "Full stack" +
+ (count > 1 ? ` (hit ${count} times)` : "") +
+ ":\n" +
+ formatStack(stack)
+ );
+ }
+ }
+ }
+
+ for (let [stack, count] of unexpectedReflows) {
+ let location = stack.split("\n")[1].replace(/:\d+:\d+$/, "");
+ Assert.ok(
+ false,
+ `unexpected reflow at ${location} hit ${count} times\n` +
+ "Stack:\n" +
+ formatStack(stack)
+ );
+ }
+ Assert.ok(
+ !unexpectedReflows.size,
+ unexpectedReflows.size + " unexpected reflows"
+ );
+}
+
+async function ensureNoPreloadedBrowser(win = window) {
+ // If we've got a preloaded browser, get rid of it so that it
+ // doesn't interfere with the test if it's loading. We have to
+ // do this before we disable preloading or changing the new tab
+ // URL, otherwise _getPreloadedBrowser will return null, despite
+ // the preloaded browser existing.
+ NewTabPagePreloading.removePreloadedBrowser(win);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtab.preload", false]],
+ });
+
+ AboutNewTab.newTabURL = "about:blank";
+
+ registerCleanupFunction(() => {
+ AboutNewTab.resetNewTabURL();
+ });
+}
+
+// Onboarding puts a badge on the fxa toolbar button a while after startup
+// which confuses tests that look at repaints in the toolbar. Use this
+// function to cancel the badge update.
+function disableFxaBadge() {
+ let { ToolbarBadgeHub } = ChromeUtils.import(
+ "resource://activity-stream/lib/ToolbarBadgeHub.jsm"
+ );
+ ToolbarBadgeHub.removeAllNotifications();
+
+ // Also prevent a new timer from being set
+ return SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.toolbar.accessed", true]],
+ });
+}
+
+async function getBookmarksToolbarRect() {
+ // Temporarily open the bookmarks toolbar to measure its rect
+ let bookmarksToolbar = gNavToolbox.querySelector("#PersonalToolbar");
+ let wasVisible = !bookmarksToolbar.collapsed;
+ if (!wasVisible) {
+ setToolbarVisibility(bookmarksToolbar, true, false, false);
+ await TestUtils.waitForCondition(
+ () => bookmarksToolbar.getBoundingClientRect().height > 0,
+ "wait for non-zero bookmarks toolbar height"
+ );
+ }
+ let bookmarksToolbarRect = bookmarksToolbar.getBoundingClientRect();
+ if (!wasVisible) {
+ setToolbarVisibility(bookmarksToolbar, false, false, false);
+ await TestUtils.waitForCondition(
+ () => bookmarksToolbar.getBoundingClientRect().height == 0,
+ "wait for zero bookmarks toolbar height"
+ );
+ }
+ return bookmarksToolbarRect;
+}
+
+async function prepareSettledWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureNoPreloadedBrowser(win);
+ return win;
+}
+
+/**
+ * Calculate and return how many additional tabs can be fit into the
+ * tabstrip without causing it to overflow.
+ *
+ * @return int
+ * The maximum additional tabs that can be fit into the
+ * tabstrip without causing it to overflow.
+ */
+function computeMaxTabCount() {
+ let currentTabCount = gBrowser.tabs.length;
+ let newTabButton = gBrowser.tabContainer.newTabButton;
+ let newTabRect = newTabButton.getBoundingClientRect();
+ let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
+ let availableTabStripWidth = tabStripRect.width - newTabRect.width;
+
+ let tabMinWidth = parseInt(
+ getComputedStyle(gBrowser.selectedTab, null).minWidth,
+ 10
+ );
+
+ let maxTabCount =
+ Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount;
+ Assert.ok(
+ maxTabCount > 0,
+ "Tabstrip needs to be wide enough to accomodate at least 1 more tab " +
+ "without overflowing."
+ );
+ return maxTabCount;
+}
+
+/**
+ * Helper function that opens up some number of about:blank tabs, and wait
+ * until they're all fully open.
+ *
+ * @param howMany (int)
+ * How many about:blank tabs to open.
+ */
+async function createTabs(howMany) {
+ let uris = [];
+ while (howMany--) {
+ uris.push("about:blank");
+ }
+
+ gBrowser.loadTabs(uris, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await TestUtils.waitForCondition(() => {
+ return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
+ });
+}
+
+/**
+ * Removes all of the tabs except the originally selected
+ * tab, and waits until all of the DOM nodes have been
+ * completely removed from the tab strip.
+ */
+async function removeAllButFirstTab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.warnOnCloseOtherTabs", false]],
+ });
+ gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+ await TestUtils.waitForCondition(() => gBrowser.tabs.length == 1);
+ await SpecialPowers.popPrefEnv();
+}
+
+/**
+ * Adds some entries to the Places database so that we can
+ * do semi-realistic look-ups in the URL bar.
+ *
+ * @param searchStr (string)
+ * Optional text to add to the search history items.
+ */
+async function addDummyHistoryEntries(searchStr = "") {
+ await PlacesUtils.history.clear();
+ const NUM_VISITS = 10;
+ let visits = [];
+
+ for (let i = 0; i < NUM_VISITS; ++i) {
+ visits.push({
+ uri: `http://example.com/urlbar-reflows-${i}`,
+ title: `Reflow test for URL bar entry #${i} - ${searchStr}`,
+ });
+ }
+
+ await PlacesTestUtils.addVisits(visits);
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ });
+}
+
+/**
+ * Async utility function to capture a screenshot of each painted frame.
+ *
+ * @param testPromise (Promise)
+ * A promise that is resolved when the data collection should stop.
+ *
+ * @param win (browser window, optional)
+ * The browser window to monitor. Defaults to the current window.
+ *
+ * @return An array of screenshots
+ */
+async function recordFrames(testPromise, win = window) {
+ let canvas = win.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.mozOpaque = true;
+ let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
+
+ let frames = [];
+
+ let afterPaintListener = event => {
+ let width, height;
+ canvas.width = width = win.innerWidth;
+ canvas.height = height = win.innerHeight;
+ ctx.drawWindow(
+ win,
+ 0,
+ 0,
+ width,
+ height,
+ "white",
+ ctx.DRAWWINDOW_DO_NOT_FLUSH |
+ ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS
+ );
+ let data = Cu.cloneInto(ctx.getImageData(0, 0, width, height).data, {});
+ if (frames.length) {
+ // Compare this frame with the previous one to avoid storing duplicate
+ // frames and running out of memory.
+ let previous = frames[frames.length - 1];
+ if (previous.width == width && previous.height == height) {
+ let equals = true;
+ for (let i = 0; i < data.length; ++i) {
+ if (data[i] != previous.data[i]) {
+ equals = false;
+ break;
+ }
+ }
+ if (equals) {
+ return;
+ }
+ }
+ }
+ frames.push({ data, width, height });
+ };
+ win.addEventListener("MozAfterPaint", afterPaintListener);
+
+ // If the test is using an existing window, capture a frame immediately.
+ if (win.document.readyState == "complete") {
+ afterPaintListener();
+ }
+
+ try {
+ await testPromise;
+ } finally {
+ win.removeEventListener("MozAfterPaint", afterPaintListener);
+ }
+
+ return frames;
+}
+
+// How many identical pixels to accept between 2 rects when deciding to merge
+// them.
+const kMaxEmptyPixels = 3;
+function compareFrames(frame, previousFrame) {
+ // Accessing the Math global is expensive as the test executes in a
+ // non-syntactic scope. Accessing it as a lexical variable is enough
+ // to make the code JIT well.
+ const M = Math;
+
+ function expandRect(x, y, rect) {
+ if (rect.x2 < x) {
+ rect.x2 = x;
+ } else if (rect.x1 > x) {
+ rect.x1 = x;
+ }
+ if (rect.y2 < y) {
+ rect.y2 = y;
+ }
+ }
+
+ function isInRect(x, y, rect) {
+ return (
+ (rect.y2 == y || rect.y2 == y - 1) && rect.x1 - 1 <= x && x <= rect.x2 + 1
+ );
+ }
+
+ if (
+ frame.height != previousFrame.height ||
+ frame.width != previousFrame.width
+ ) {
+ // If the frames have different sizes, assume the whole window has
+ // been repainted when the window was resized.
+ return [{ x1: 0, x2: frame.width, y1: 0, y2: frame.height }];
+ }
+
+ let l = frame.data.length;
+ let different = [];
+ let rects = [];
+ for (let i = 0; i < l; i += 4) {
+ let x = (i / 4) % frame.width;
+ let y = M.floor(i / 4 / frame.width);
+ for (let j = 0; j < 4; ++j) {
+ let index = i + j;
+
+ if (frame.data[index] != previousFrame.data[index]) {
+ let found = false;
+ for (let rect of rects) {
+ if (isInRect(x, y, rect)) {
+ expandRect(x, y, rect);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ rects.unshift({ x1: x, x2: x, y1: y, y2: y });
+ }
+
+ different.push(i);
+ break;
+ }
+ }
+ }
+ rects.reverse();
+
+ // The following code block merges rects that are close to each other
+ // (less than kMaxEmptyPixels away).
+ // This is needed to avoid having a rect for each letter when a label moves.
+ let areRectsContiguous = function(r1, r2) {
+ return (
+ r1.y2 >= r2.y1 - 1 - kMaxEmptyPixels &&
+ r2.x1 - 1 - kMaxEmptyPixels <= r1.x2 &&
+ r2.x2 >= r1.x1 - 1 - kMaxEmptyPixels
+ );
+ };
+ let hasMergedRects;
+ do {
+ hasMergedRects = false;
+ for (let r = rects.length - 1; r > 0; --r) {
+ let rr = rects[r];
+ for (let s = r - 1; s >= 0; --s) {
+ let rs = rects[s];
+ if (areRectsContiguous(rs, rr)) {
+ rs.x1 = Math.min(rs.x1, rr.x1);
+ rs.y1 = Math.min(rs.y1, rr.y1);
+ rs.x2 = Math.max(rs.x2, rr.x2);
+ rs.y2 = Math.max(rs.y2, rr.y2);
+ rects.splice(r, 1);
+ hasMergedRects = true;
+ break;
+ }
+ }
+ }
+ } while (hasMergedRects);
+
+ // For convenience, pre-compute the width and height of each rect.
+ rects.forEach(r => {
+ r.w = r.x2 - r.x1 + 1;
+ r.h = r.y2 - r.y1 + 1;
+ });
+
+ return rects;
+}
+
+function dumpFrame({ data, width, height }) {
+ let canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.mozOpaque = true;
+ canvas.width = width;
+ canvas.height = height;
+
+ canvas
+ .getContext("2d", { alpha: false, willReadFrequently: true })
+ .putImageData(new ImageData(data, width, height), 0, 0);
+
+ info(canvas.toDataURL());
+}
+
+/**
+ * Utility function to report unexpected changed areas on screen.
+ *
+ * @param frames (Array)
+ * An array of frames captured by recordFrames.
+ *
+ * @param expectations (Object)
+ * An Object indicating which changes on screen are expected.
+ * If can contain the following optional fields:
+ * - filter: a function used to exclude changed rects that are expected.
+ * It takes the following parameters:
+ * - rects: an array of changed rects
+ * - frame: the current frame
+ * - previousFrame: the previous frame
+ * It returns an array of rects. This array is typically a copy of
+ * the rects parameter, from which identified expected changes have
+ * been excluded.
+ * - exceptions: an array of objects describing known flicker bugs.
+ * Example:
+ * exceptions: [
+ * {name: "bug 1nnnnnn - the foo icon shouldn't flicker",
+ * condition: r => r.w == 14 && r.y1 == 0 && ... }
+ * },
+ * {name: "bug ...
+ * ]
+ */
+function reportUnexpectedFlicker(frames, expectations) {
+ info("comparing " + frames.length + " frames");
+
+ let unexpectedRects = 0;
+ for (let i = 1; i < frames.length; ++i) {
+ let frame = frames[i],
+ previousFrame = frames[i - 1];
+ let rects = compareFrames(frame, previousFrame);
+
+ if (expectations.filter) {
+ rects = expectations.filter(rects, frame, previousFrame);
+ }
+
+ rects = rects.filter(rect => {
+ let rectText = `${rect.toSource()}, window width: ${frame.width}`;
+ for (let e of expectations.exceptions || []) {
+ if (e.condition(rect)) {
+ todo(false, e.name + ", " + rectText);
+ return false;
+ }
+ }
+
+ ok(false, "unexpected changed rect: " + rectText);
+ return true;
+ });
+
+ if (!rects.length) {
+ continue;
+ }
+
+ // Before dumping a frame with unexpected differences for the first time,
+ // ensure at least one previous frame has been logged so that it's possible
+ // to see the differences when examining the log.
+ if (!unexpectedRects) {
+ dumpFrame(previousFrame);
+ }
+ unexpectedRects += rects.length;
+ dumpFrame(frame);
+ }
+ is(unexpectedRects, 0, "should have 0 unknown flickering areas");
+}
+
+/**
+ * This is the main function that performance tests in this folder will call.
+ *
+ * The general idea is that individual tests provide a test function (testFn)
+ * that will perform some user interactions we care about (eg. open a tab), and
+ * this withPerfObserver function takes care of setting up and removing the
+ * observers and listener we need to detect common performance issues.
+ *
+ * Once testFn is done, withPerfObserver will analyse the collected data and
+ * report anything unexpected.
+ *
+ * @param testFn (async function)
+ * An async function that exercises some part of the browser UI.
+ *
+ * @param exceptions (object, optional)
+ * An Array of Objects representing expectations and known issues.
+ * It can contain the following fields:
+ * - expectedReflows: an array of expected reflow stacks.
+ * (see the comment above reportUnexpectedReflows for an example)
+ * - frames: an object setting expectations for what will change
+ * on screen during the test, and the known flicker bugs.
+ * (see the comment above reportUnexpectedFlicker for an example)
+ */
+async function withPerfObserver(testFn, exceptions = {}, win = window) {
+ let resolveFn, rejectFn;
+ let promiseTestDone = new Promise((resolve, reject) => {
+ resolveFn = resolve;
+ rejectFn = reject;
+ });
+
+ let promiseReflows = recordReflows(promiseTestDone, win);
+ let promiseFrames = recordFrames(promiseTestDone, win);
+
+ testFn().then(resolveFn, rejectFn);
+ await promiseTestDone;
+
+ let reflows = await promiseReflows;
+ reportUnexpectedReflows(reflows, exceptions.expectedReflows);
+
+ let frames = await promiseFrames;
+ reportUnexpectedFlicker(frames, exceptions.frames);
+}
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when typing into the URL bar
+ * with the default values in Places.
+ *
+ * @param {bool} keyed
+ * Pass true to synthesize typing the search string one key at a time.
+ * @param {array} expectedReflowsFirstOpen
+ * The array of expected reflow stacks when the panel is first opened.
+ * @param {array} [expectedReflowsSecondOpen]
+ * The array of expected reflow stacks when the panel is subsequently
+ * opened, if you're testing opening the panel twice.
+ */
+async function runUrlbarTest(
+ keyed,
+ expectedReflowsFirstOpen,
+ expectedReflowsSecondOpen = null
+) {
+ const SEARCH_TERM = keyed ? "" : "urlbar-reflows-" + Date.now();
+ await addDummyHistoryEntries(SEARCH_TERM);
+
+ let win = await prepareSettledWindow();
+
+ let URLBar = win.gURLBar;
+
+ URLBar.focus();
+ URLBar.value = SEARCH_TERM;
+ let testFn = async function() {
+ let popup = URLBar.view;
+ let oldOnQueryResults = popup.onQueryResults.bind(popup);
+ let oldOnQueryFinished = popup.onQueryFinished.bind(popup);
+
+ // We need to invalidate the frame tree outside of the normal
+ // mechanism since invalidations and result additions to the
+ // URL bar occur without firing JS events (which is how we
+ // normally know to dirty the frame tree).
+ popup.onQueryResults = context => {
+ dirtyFrame(win);
+ oldOnQueryResults(context);
+ };
+
+ popup.onQueryFinished = context => {
+ dirtyFrame(win);
+ oldOnQueryFinished(context);
+ };
+
+ let waitExtra = async () => {
+ // There are several setTimeout(fn, 0); calls inside autocomplete.xml
+ // that we need to wait for. Since those have higher priority than
+ // idle callbacks, we can be sure they will have run once this
+ // idle callback is called. The timeout seems to be required in
+ // automation - presumably because the machines can be pretty busy
+ // especially if it's GC'ing from previous tests.
+ await new Promise(resolve =>
+ win.requestIdleCallback(resolve, { timeout: 1000 })
+ );
+ };
+
+ if (keyed) {
+ // Only keying in 6 characters because the number of reflows triggered
+ // is so high that we risk timing out the test if we key in any more.
+ let searchTerm = "ows-10";
+ for (let i = 0; i < searchTerm.length; ++i) {
+ let char = searchTerm[i];
+ EventUtils.synthesizeKey(char, {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ await waitExtra();
+ }
+ } else {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus: SimpleTest.waitForFocus,
+ value: URLBar.value,
+ });
+ await waitExtra();
+ }
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ };
+
+ let urlbarRect = URLBar.textbox.getBoundingClientRect();
+ const SHADOW_SIZE = 4;
+ 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.
+ */
+function checkLoadedScripts({
+ loadedInfo,
+ known,
+ intermittent,
+ forbidden,
+ dumpAllStacks,
+}) {
+ let loadedList = {};
+
+ 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);
+ });
+
+ 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]
+ );
+ }
+
+ 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]
+ );
+ }
+ }
+ }
+}
diff --git a/browser/base/content/test/performance/hidpi/browser.ini b/browser/base/content/test/performance/hidpi/browser.ini
new file mode 100644
index 0000000000..5375700ee8
--- /dev/null
+++ b/browser/base/content/test/performance/hidpi/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+prefs =
+ browser.startup.recordImages=true
+ layout.css.devPixelsPerPx='2'
+
+[../browser_startup_images.js]
+skip-if = !debug || (os == 'win' && (os_version == '6.1'))
diff --git a/browser/base/content/test/performance/io/browser.ini b/browser/base/content/test/performance/io/browser.ini
new file mode 100644
index 0000000000..44d850820b
--- /dev/null
+++ b/browser/base/content/test/performance/io/browser.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+# Currently disabled on debug due to debug-only failures, see bug 1549723.
+# Disabled on Linux asan due to bug 1549729.
+# Disabled on Windows Arm64 due to bug 1551493.
+# Disabled on Windows asan due to intermittent startup hangs, bug 1629824.
+skip-if = debug || tsan || (os == "linux" && asan) || (os == "win" && (asan || processor == "aarch64"))
+# to avoid overhead when running the browser normally, startupRecorder.js 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.js
+prefs =
+ browser.startup.record=true
+ gfx.canvas.willReadFrequently.enable=true
+ # The Screenshots extension is disabled by default in Mochitests. We re-enable
+ # it here, since it's a more realistic configuration.
+ extensions.screenshots.disabled=false
+environment =
+ GNOME_ACCESSIBILITY=0
+ MOZ_PROFILER_STARTUP=1
+ MOZ_PROFILER_STARTUP_PERFORMANCE_TEST=1
+ MOZ_PROFILER_STARTUP_FEATURES=js,mainthreadio,ipcmessages
+ MOZ_PROFILER_STARTUP_ENTRIES=10000000
+[../browser_startup_mainthreadio.js]
+[../browser_startup_content_mainthreadio.js]
+[../browser_startup_syncIPC.js]
diff --git a/browser/base/content/test/performance/lowdpi/browser.ini b/browser/base/content/test/performance/lowdpi/browser.ini
new file mode 100644
index 0000000000..5dc4efc5ac
--- /dev/null
+++ b/browser/base/content/test/performance/lowdpi/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs =
+ browser.startup.recordImages=true
+ layout.css.devPixelsPerPx='1'
+
+[../browser_startup_images.js]
+skip-if = !debug
+
diff --git a/browser/base/content/test/perftest.ini b/browser/base/content/test/perftest.ini
new file mode 100644
index 0000000000..d4351967e4
--- /dev/null
+++ b/browser/base/content/test/perftest.ini
@@ -0,0 +1 @@
+[perftest_browser_xhtml_dom.js]
diff --git a/browser/base/content/test/perftest_browser_xhtml_dom.js b/browser/base/content/test/perftest_browser_xhtml_dom.js
new file mode 100644
index 0000000000..2de208df19
--- /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/.eslintrc.js b/browser/base/content/test/permissions/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/permissions/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/permissions/browser.ini b/browser/base/content/test/permissions/browser.ini
new file mode 100644
index 0000000000..bc3d59d231
--- /dev/null
+++ b/browser/base/content/test/permissions/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files=
+ head.js
+ permissions.html
+ temporary_permissions_subframe.html
+ temporary_permissions_frame.html
+[browser_canvas_fingerprinting_resistance.js]
+skip-if = debug || os == "linux" && asan # Bug 1522069
+[browser_permissions.js]
+[browser_permissions_delegate_vibrate.js]
+support-files=
+ empty.html
+[browser_permission_delegate_geo.js]
+[browser_permissions_postPrompt.js]
+support-files=
+ dummy.js
+[browser_permissions_handling_user_input.js]
+support-files=
+ dummy.js
+[browser_reservedkey.js]
+[browser_temporary_permissions.js]
+support-files =
+ ../webrtc/get_user_media.html
+[browser_autoplay_blocked.js]
+support-files =
+ browser_autoplay_blocked.html
+ browser_autoplay_blocked_slow.sjs
+ browser_autoplay_muted.html
+ ../general/audio.ogg
+skip-if = true # Bug 1538602
+[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..a77a761148
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked.js
@@ -0,0 +1,338 @@
+/*
+ * 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 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 openIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ return promise;
+}
+
+function closeIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ gIdentityHandler._identityPopup.hidePopup();
+ return promise;
+}
+
+function autoplayBlockedIcon() {
+ return document.querySelector(
+ "#blocked-permissions-container " +
+ ".blocked-permission-icon.autoplay-media-icon"
+ );
+}
+
+function permissionListBlockedIcons() {
+ return document.querySelectorAll(
+ "image.identity-popup-permission-icon.blocked-permission-icon"
+ );
+}
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function blockedIconShown() {
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(autoplayBlockedIcon());
+ }, "Blocked icon is shown");
+}
+
+async function blockedIconHidden() {
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_hidden(autoplayBlockedIcon());
+ }, "Blocked icon is hidden");
+}
+
+add_task(async function setup() {
+ 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() {
+ let permissionsList = document.getElementById(
+ "identity-popup-permission-list"
+ );
+ let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
+
+ ok(
+ BrowserTestUtils.is_hidden(autoplayBlockedIcon()),
+ "Blocked icon not shown"
+ );
+
+ await openIdentityPopup();
+ ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
+ await closeIdentityPopup();
+ });
+
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function(browser) {
+ let permissionsList = document.getElementById(
+ "identity-popup-permission-list"
+ );
+ let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
+
+ await blockedIconShown();
+
+ await openIdentityPopup();
+ ok(
+ BrowserTestUtils.is_hidden(emptyLabel),
+ "List of permissions is not empty"
+ );
+ let labelText = SitePermissions.getPermissionLabel(AUTOPLAY_PERM);
+ let labels = permissionsList.querySelectorAll(
+ ".identity-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+ is(labels[0].textContent, labelText, "Correct value");
+
+ let menulist = document.getElementById("identity-popup-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 closeIdentityPopup();
+
+ 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.loadURI(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.loadURI(browser, AUTOPLAY_PAGE);
+ await blockedIconShown();
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.ALLOWED);
+
+ gBrowser.goBack();
+ await blockedIconHidden();
+
+ gBrowser.goForward();
+
+ // Sleep here to prevent false positives, the icon gets shown with an
+ // async `GloballyAutoplayBlocked` event. The sleep gives it a little
+ // time for it to show otherwise there is a chance it passes before it
+ // would have shown.
+ await sleep(100);
+ ok(
+ BrowserTestUtils.is_hidden(autoplayBlockedIcon()),
+ "Blocked icon is hidden"
+ );
+ });
+
+ Services.perms.removeAll();
+});
+
+add_task(async function testSlowLoadingPage() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED);
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SLOW_AUTOPLAY_PAGE
+ );
+ await blockedIconShown();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ // Wait until the blocked icon is hidden by switching tabs
+ await blockedIconHidden();
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await blockedIconShown();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ Services.perms.removeAll();
+});
+
+add_task(async function testBlockedAll() {
+ Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED_ALL);
+
+ await BrowserTestUtils.withNewTab("about:home", async function(browser) {
+ await blockedIconHidden();
+ BrowserTestUtils.loadURI(browser, MUTED_AUTOPLAY_PAGE);
+ await blockedIconShown();
+
+ await openIdentityPopup();
+
+ Assert.equal(
+ permissionListBlockedIcons().length,
+ 1,
+ "Blocked icon is shown"
+ );
+
+ let menulist = document.getElementById("identity-popup-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 closeIdentityPopup();
+ gBrowser.reload();
+ await blockedIconHidden();
+ });
+ 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..901fce7d7e
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs
@@ -0,0 +1,32 @@
+/* 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_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..618d67fd37
--- /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_permission_delegate_geo.js b/browser/base/content/test/permissions/browser_permission_delegate_geo.js
new file mode 100644
index 0000000000..cb725c8409
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permission_delegate_geo.js
@@ -0,0 +1,266 @@
+/* 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 waitForPrompt = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ let isPrompt = expect == PromptResult.PROMPT;
+ 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.import(
+ "resource://gre/modules/E10SUtils.jsm"
+ );
+
+ 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_task(async function setup() {
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ["permissions.delegation.enabled", true],
+ ],
+ },
+ 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..de45626937
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions.js
@@ -0,0 +1,590 @@
+/*
+ * Test the Permissions section in the Control Center.
+ */
+
+const PERMISSIONS_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "permissions.html";
+const kStrictKeyPressEvents = SpecialPowers.getBoolPref(
+ "dom.keyboardevent.keypress.dispatch_non_printable_keys_only_system_group_in_content"
+);
+
+function openIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ return promise;
+}
+
+function closeIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ gIdentityHandler._identityPopup.hidePopup();
+ return promise;
+}
+
+add_task(async function testMainViewVisible() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function() {
+ await openIdentityPopup();
+
+ let permissionsList = document.getElementById(
+ "identity-popup-permission-list"
+ );
+ let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
+ ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
+
+ await closeIdentityPopup();
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openIdentityPopup();
+
+ ok(
+ BrowserTestUtils.is_hidden(emptyLabel),
+ "List of permissions is not empty"
+ );
+
+ let labelText = SitePermissions.getPermissionLabel("camera");
+ let labels = permissionsList.querySelectorAll(
+ ".identity-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+ is(labels[0].textContent, labelText, "Correct value");
+
+ let img = permissionsList.querySelector(
+ "image.identity-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 closeIdentityPopup();
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+
+ await openIdentityPopup();
+
+ ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
+
+ await closeIdentityPopup();
+ });
+});
+
+add_task(async function testIdentityIcon() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function() {
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.ALLOW_ACTION
+ );
+
+ ok(
+ gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box signals granted permissions"
+ );
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+
+ ok(
+ !gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box doesn't signal granted permissions"
+ );
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.DENY_ACTION
+ );
+
+ ok(
+ !gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box doesn't signal granted permissions"
+ );
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ ok(
+ gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box signals granted permissions"
+ );
+
+ PermissionTestUtils.remove(gBrowser.currentURI, "geo");
+ PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+ PermissionTestUtils.remove(gBrowser.currentURI, "cookie");
+ });
+});
+
+add_task(async function testCancelPermission() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function() {
+ let permissionsList = document.getElementById(
+ "identity-popup-permission-list"
+ );
+ let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.DENY_ACTION
+ );
+
+ await openIdentityPopup();
+
+ ok(
+ BrowserTestUtils.is_hidden(emptyLabel),
+ "List of permissions is not empty"
+ );
+
+ permissionsList
+ .querySelector(".identity-popup-permission-remove-button")
+ .click();
+
+ is(
+ permissionsList.querySelectorAll(".identity-popup-permission-label")
+ .length,
+ 1,
+ "First permission should be removed"
+ );
+
+ permissionsList
+ .querySelector(".identity-popup-permission-remove-button")
+ .click();
+
+ is(
+ permissionsList.querySelectorAll(".identity-popup-permission-label")
+ .length,
+ 0,
+ "Second permission should be removed"
+ );
+
+ await closeIdentityPopup();
+ });
+});
+
+add_task(async function testPermissionHints() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function(browser) {
+ let permissionsList = document.getElementById(
+ "identity-popup-permission-list"
+ );
+ let emptyHint = document.getElementById(
+ "identity-popup-permission-empty-hint"
+ );
+ let reloadHint = document.getElementById(
+ "identity-popup-permission-reload-hint"
+ );
+
+ await openIdentityPopup();
+
+ ok(!BrowserTestUtils.is_hidden(emptyHint), "Empty hint is visible");
+ ok(BrowserTestUtils.is_hidden(reloadHint), "Reload hint is hidden");
+
+ await closeIdentityPopup();
+
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "geo",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "camera",
+ Services.perms.DENY_ACTION
+ );
+
+ await openIdentityPopup();
+
+ ok(BrowserTestUtils.is_hidden(emptyHint), "Empty hint is hidden");
+ ok(BrowserTestUtils.is_hidden(reloadHint), "Reload hint is hidden");
+
+ let cancelButtons = permissionsList.querySelectorAll(
+ ".identity-popup-permission-remove-button"
+ );
+ PermissionTestUtils.remove(gBrowser.currentURI, "camera");
+
+ cancelButtons[0].click();
+ ok(BrowserTestUtils.is_hidden(emptyHint), "Empty hint is hidden");
+ ok(!BrowserTestUtils.is_hidden(reloadHint), "Reload hint is visible");
+
+ cancelButtons[1].click();
+ ok(BrowserTestUtils.is_hidden(emptyHint), "Empty hint is hidden");
+ ok(!BrowserTestUtils.is_hidden(reloadHint), "Reload hint is visible");
+
+ await closeIdentityPopup();
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURI(browser, PERMISSIONS_PAGE);
+ await loaded;
+ await openIdentityPopup();
+
+ ok(
+ !BrowserTestUtils.is_hidden(emptyHint),
+ "Empty hint is visible after reloading"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(reloadHint),
+ "Reload hint is hidden after reloading"
+ );
+
+ await closeIdentityPopup();
+ });
+});
+
+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 = gIdentityHandler._identityBox.querySelector(
+ ".blocked-permission-icon[data-permission-id='geo']"
+ );
+ ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
+
+ let cameraIcon = gIdentityHandler._identityBox.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
+ );
+ if (kStrictKeyPressEvents) {
+ is(
+ result.keypresses,
+ 0,
+ "keypress event shouldn't be fired for shortcut key, " + desc
+ );
+ } else {
+ is(
+ result.keypresses,
+ expectedValue,
+ "keypress event should be fired even 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(
+ "identity-popup-permission-list"
+ );
+ PermissionTestUtils.add(
+ gBrowser.currentURI,
+ "popup",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_POLICY
+ );
+
+ await openIdentityPopup();
+
+ // Check if the icon, nameLabel and stateLabel are visible.
+ let img, labelText, labels;
+
+ img = permissionsList.querySelector("image.identity-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(
+ ".identity-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+ is(labels[0].textContent, labelText, "Correct name label value");
+
+ labelText = SitePermissions.getCurrentStateLabel(
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_POLICY
+ );
+ labels = permissionsList.querySelectorAll(
+ ".identity-popup-permission-state-label"
+ );
+ is(labels[0].textContent, 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("identity-popup-popup-menulist");
+ ok(menulist == null, "The popup permission menulist is not visible");
+
+ let removeButton = permissionsList.querySelector(
+ ".identity-popup-permission-remove-button"
+ );
+ ok(removeButton == null, "The permission remove button is not visible");
+
+ Services.perms.removeAll();
+ await closeIdentityPopup();
+ });
+});
+
+add_task(async function testHiddenAfterRefresh() {
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function(browser) {
+ ok(
+ BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup),
+ "Popup is hidden"
+ );
+
+ await openIdentityPopup();
+
+ ok(
+ !BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup),
+ "Popup is shown"
+ );
+
+ let reloaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ PERMISSIONS_PAGE
+ );
+ EventUtils.synthesizeKey("VK_F5", {}, browser.ownerGlobal);
+ await reloaded;
+
+ ok(
+ BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup),
+ "Popup is hidden"
+ );
+ });
+});
+
+add_task(async function test3rdPartyStoragePermission() {
+ // 3rdPartyStorage permissions are listed under an anchor container - test
+ // that this works correctly, i.e. the permission items are added to the
+ // anchor when relevant, and other permission items are added to the default
+ // anchor, and adding/removing permissions preserves this behavior correctly.
+ SpecialPowers.pushPrefEnv({
+ set: [["browser.contentblocking.state-partitioning.mvp.ui.enabled", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function(browser) {
+ await openIdentityPopup();
+
+ let permissionsList = document.getElementById(
+ "identity-popup-permission-list"
+ );
+ let storagePermissionAnchor = permissionsList.querySelector(
+ `.identity-popup-permission-list-anchor[anchorfor="3rdPartyStorage"]`
+ );
+ let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
+ ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
+ ok(
+ BrowserTestUtils.is_hidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ await closeIdentityPopup();
+
+ let storagePermissionID = "3rdPartyStorage^example2.com";
+ PermissionTestUtils.add(
+ browser.currentURI,
+ storagePermissionID,
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openIdentityPopup();
+
+ ok(
+ BrowserTestUtils.is_hidden(emptyLabel),
+ "List of permissions is not empty"
+ );
+ ok(
+ BrowserTestUtils.is_visible(storagePermissionAnchor.firstElementChild),
+ "Anchor header is visible"
+ );
+
+ let labelText = SitePermissions.getPermissionLabel(storagePermissionID);
+ let labels = storagePermissionAnchor.querySelectorAll(
+ ".identity-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 closeIdentityPopup();
+
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "camera",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openIdentityPopup();
+
+ ok(
+ BrowserTestUtils.is_hidden(emptyLabel),
+ "List of permissions is not empty"
+ );
+ ok(
+ BrowserTestUtils.is_visible(storagePermissionAnchor.firstElementChild),
+ "Anchor header is visible"
+ );
+
+ labels = permissionsList.querySelectorAll(
+ ".identity-popup-permission-label"
+ );
+ is(labels.length, 2, "Two permissions visible in main view");
+ labels = storagePermissionAnchor.querySelectorAll(
+ ".identity-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in 3rdPartyStorage anchor");
+
+ storagePermissionAnchor
+ .querySelector(".identity-popup-permission-remove-button")
+ .click();
+ is(
+ storagePermissionAnchor.querySelectorAll(
+ ".identity-popup-permission-label"
+ ).length,
+ 0,
+ "Permission item should be removed"
+ );
+ is(
+ PermissionTestUtils.testPermission(
+ browser.currentURI,
+ storagePermissionID
+ ),
+ SitePermissions.UNKNOWN,
+ "Permission removed from permission manager"
+ );
+
+ await closeIdentityPopup();
+
+ await openIdentityPopup();
+
+ ok(
+ BrowserTestUtils.is_hidden(emptyLabel),
+ "List of permissions is not empty"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ labels = permissionsList.querySelectorAll(
+ ".identity-popup-permission-label"
+ );
+ is(labels.length, 1, "One permission visible in main view");
+
+ await closeIdentityPopup();
+
+ PermissionTestUtils.remove(browser.currentURI, "camera");
+
+ await openIdentityPopup();
+
+ ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
+ ok(
+ BrowserTestUtils.is_hidden(storagePermissionAnchor.firstElementChild),
+ "Anchor header is hidden"
+ );
+
+ await closeIdentityPopup();
+ });
+});
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..4e61cd436d
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js
@@ -0,0 +1,46 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_PAGE =
+ "https://example.com/browser/browser/base/content/test/permissions/empty.html";
+
+add_task(async function testNoPermissionPrompt() {
+ info("Creating tab");
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async function(browser) {
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.vibrator.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ },
+ r
+ );
+ });
+
+ await ContentTask.spawn(browser, null, async function() {
+ let frame = content.document.createElement("iframe");
+ // Cross origin src
+ frame.src =
+ "https://example.org/browser/browser/base/content/test/permissions/empty.html";
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ await content.SpecialPowers.spawn(frame, [], async function() {
+ // Request a permission.
+ let result = this.content.navigator.vibrate([100, 100]);
+ Assert.equal(result, false, "navigator.vibrate has been denied");
+ });
+ content.document.body.removeChild(frame);
+ });
+ });
+});
diff --git a/browser/base/content/test/permissions/browser_permissions_handling_user_input.js b/browser/base/content/test/permissions/browser_permissions_handling_user_input.js
new file mode 100644
index 0000000000..ca157c55ba
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_handling_user_input.js
@@ -0,0 +1,93 @@
+/* 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..47f4b4792d
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_permissions_postPrompt.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ORIGIN = "https://example.com";
+const PERMISSIONS_PAGE =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
+ "permissions.html";
+
+function testPostPrompt(task) {
+ let uri = Services.io.newURI(PERMISSIONS_PAGE);
+ return BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function(browser) {
+ let icon = document.getElementById("web-notifications-notification-icon");
+ ok(
+ !BrowserTestUtils.is_visible(icon),
+ "notifications icon is not visible at first"
+ );
+
+ await SpecialPowers.spawn(browser, [], task);
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(icon),
+ "notifications icon is visible"
+ );
+ ok(
+ !PopupNotifications.panel.hasAttribute("panelopen"),
+ "only the icon is showing, the panel is not open"
+ );
+
+ let popupshown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ icon.click();
+ await popupshown;
+
+ ok(true, "Notification permission prompt was shown");
+
+ let notification = PopupNotifications.panel.firstElementChild;
+ EventUtils.synthesizeMouseAtCenter(notification.button, {});
+
+ is(
+ PermissionTestUtils.testPermission(uri, "desktop-notification"),
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ "User can override the default deny by using the prompt"
+ );
+
+ PermissionTestUtils.remove(uri, "desktop-notification");
+ });
+}
+
+add_task(async function testNotificationPermission() {
+ Services.prefs.setBoolPref(
+ "dom.webnotifications.requireuserinteraction",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "permissions.desktop-notification.postPrompt.enabled",
+ true
+ );
+
+ Services.prefs.setIntPref(
+ "permissions.default.desktop-notification",
+ Ci.nsIPermissionManager.DENY_ACTION
+ );
+
+ // First test that all requests (even with user interaction) will cause a post-prompt
+ // if the global default is "deny".
+
+ await testPostPrompt(function() {
+ E10SUtils.wrapHandlingUserInput(content, true, function() {
+ content.document.getElementById("desktop-notification").click();
+ });
+ });
+
+ await testPostPrompt(function() {
+ E10SUtils.wrapHandlingUserInput(content, true, function() {
+ content.document.getElementById("push").click();
+ });
+ });
+
+ Services.prefs.clearUserPref("permissions.default.desktop-notification");
+
+ // Now test that requests without user interaction will post-prompt when the
+ // user interaction requirement is set.
+
+ await testPostPrompt(function() {
+ content.postMessage("push", "*");
+ });
+
+ await testPostPrompt(async function() {
+ let response = await content.Notification.requestPermission();
+ is(response, "default", "The request was automatically denied");
+ });
+
+ Services.prefs.clearUserPref("dom.webnotifications.requireuserinteraction");
+ Services.prefs.clearUserPref(
+ "permissions.desktop-notification.postPrompt.enabled"
+ );
+});
diff --git a/browser/base/content/test/permissions/browser_reservedkey.js b/browser/base/content/test/permissions/browser_reservedkey.js
new file mode 100644
index 0000000000..2036188584
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_reservedkey.js
@@ -0,0 +1,228 @@
+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");
+ // We need to have the attribute "oncommand" for the "command" listener to fire
+ key1.setAttribute("oncommand", "//");
+ 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");
+ // We need to have the attribute "oncommand" for the "command" listener to fire
+ key2.setAttribute("oncommand", "//");
+ 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");
+ // We need to have the attribute "oncommand" for the "command" listener to fire
+ key3.setAttribute("oncommand", "//");
+ 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);
+});
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..0767b97882
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions.js
@@ -0,0 +1,119 @@
+/* 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.import(
+ "resource://gre/modules/E10SUtils.jsm"
+ );
+
+ 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..6426f2fef3
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js
@@ -0,0 +1,110 @@
+/* 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.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/The request is not allowed/);
+
+const EXPIRE_TIME_MS = 100;
+const TIMEOUT_MS = 500;
+
+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 = gIdentityHandler._identityBox.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);
+ });
+ }
+});
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..cc965031a8
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js
@@ -0,0 +1,247 @@
+/* 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) {
+ let reloadButton = document.getElementById("reload-button");
+
+ 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;
+ await TestUtils.waitForCondition(() => {
+ return !reloadButton.disabled;
+ });
+
+ 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).
+ EventUtils.synthesizeMouseAtCenter(reloadButton, {});
+
+ 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).
+ EventUtils.synthesizeMouseAtCenter(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))
+ )
+ );
+ EventUtils.synthesizeMouseAtCenter(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..513b98481d
--- /dev/null
+++ b/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js
@@ -0,0 +1,105 @@
+/* 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);
+});
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..a425b4f5bb
--- /dev/null
+++ b/browser/base/content/test/permissions/head.js
@@ -0,0 +1,9 @@
+ChromeUtils.import("resource:///modules/SitePermissions.jsm", this);
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+SpecialPowers.addTaskImport(
+ "E10SUtils",
+ "resource://gre/modules/E10SUtils.jsm"
+);
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/.eslintrc.js b/browser/base/content/test/plugins/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/plugins/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/plugins/BlocklistTestProxy.jsm b/browser/base/content/test/plugins/BlocklistTestProxy.jsm
new file mode 100644
index 0000000000..cd13352f4a
--- /dev/null
+++ b/browser/base/content/test/plugins/BlocklistTestProxy.jsm
@@ -0,0 +1,88 @@
+var EXPORTED_SYMBOLS = ["BlocklistTestProxyChild"];
+
+var Cm = Components.manager;
+
+const kBlocklistServiceUUID = "{66354bc9-7ed1-4692-ae1d-8da97d6b205e}";
+const kBlocklistServiceContractID = "@mozilla.org/extensions/blocklist;1";
+
+let existingBlocklistFactory = null;
+try {
+ existingBlocklistFactory = Cm.getClassObject(
+ Cc[kBlocklistServiceContractID],
+ Ci.nsIFactory
+ );
+} catch (ex) {}
+
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+/*
+ * A lightweight blocklist proxy for testing purposes.
+ */
+var BlocklistProxy = {
+ _uuid: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIBlocklistService",
+ "nsITimerCallback",
+ ]),
+
+ init() {
+ if (!this._uuid) {
+ this._uuid = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID();
+ Cm.nsIComponentRegistrar.registerFactory(
+ this._uuid,
+ "",
+ "@mozilla.org/extensions/blocklist;1",
+ this
+ );
+ }
+ },
+
+ uninit() {
+ if (this._uuid) {
+ Cm.nsIComponentRegistrar.unregisterFactory(this._uuid, this);
+ if (existingBlocklistFactory) {
+ Cm.nsIComponentRegistrar.registerFactory(
+ Components.ID(kBlocklistServiceUUID),
+ "Blocklist Service",
+ "@mozilla.org/extensions/blocklist;1",
+ existingBlocklistFactory
+ );
+ }
+ this._uuid = null;
+ }
+ },
+
+ notify(aTimer) {},
+
+ async getAddonBlocklistState(aAddon, aAppVersion, aToolkitVersion) {
+ await new Promise(r => setTimeout(r, 150));
+ return 0; // STATE_NOT_BLOCKED
+ },
+
+ async getPluginBlocklistState(aPluginTag, aAppVersion, aToolkitVersion) {
+ await new Promise(r => setTimeout(r, 150));
+ return 0; // STATE_NOT_BLOCKED
+ },
+
+ async getPluginBlockURL(aPluginTag) {
+ await new Promise(r => setTimeout(r, 150));
+ return "";
+ },
+};
+
+class BlocklistTestProxyChild extends JSProcessActorChild {
+ constructor() {
+ super();
+ BlocklistProxy.init();
+ }
+
+ receiveMessage(message) {
+ if (message.name == "unload") {
+ BlocklistProxy.uninit();
+ }
+ }
+}
diff --git a/browser/base/content/test/plugins/browser.ini b/browser/base/content/test/plugins/browser.ini
new file mode 100644
index 0000000000..61898167f7
--- /dev/null
+++ b/browser/base/content/test/plugins/browser.ini
@@ -0,0 +1,25 @@
+[DEFAULT]
+prefs =
+ plugin.load_flash_only=false
+support-files =
+ BlocklistTestProxy.jsm
+ empty_file.html
+ head.js
+ plugin_bug797677.html
+ plugin_favorfallback.html
+ plugin_outsideScrollArea.html
+ plugin_simple_blank.swf
+ plugin_test.html
+ plugin_zoom.html
+
+[browser_bug797677.js]
+[browser_CTP_favorfallback.js]
+[browser_CTP_outsideScrollArea.js]
+tags = blocklist
+[browser_CTP_zoom.js]
+tags = blocklist
+[browser_enable_DRM_prompt.js]
+skip-if = (os == 'win' && processor == 'aarch64') # bug 1533164
+[browser_private_browsing_eme_persistent_state.js]
+[browser_globalplugin_crashinfobar.js]
+skip-if = !crashreporter
diff --git a/browser/base/content/test/plugins/browser_CTP_favorfallback.js b/browser/base/content/test/plugins/browser_CTP_favorfallback.js
new file mode 100644
index 0000000000..ba3af78f57
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_favorfallback.js
@@ -0,0 +1,104 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace(
+ "chrome://mochitests/content/",
+ "http://127.0.0.1:8888/"
+);
+var gPluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+
+add_task(async function() {
+ registerCleanupFunction(function() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.favorfallback.mode");
+ Services.prefs.clearUserPref("plugins.favorfallback.rules");
+ });
+});
+
+add_task(async function() {
+ Services.prefs.setCharPref("plugins.favorfallback.mode", "follow-ctp");
+});
+
+/* The expected behavior of each testcase is documented with its markup
+ * in plugin_favorfallback.html.
+ *
+ * - "name" is the name of the testcase in the test file.
+ * - "rule" is how the plugins.favorfallback.rules must be configured
+ * for this testcase.
+ */
+const testcases = [
+ {
+ name: "video",
+ rule: "video",
+ },
+
+ {
+ name: "nosrc",
+ rule: "nosrc",
+ },
+
+ {
+ name: "embed",
+ rule: "embed,true",
+ },
+
+ {
+ name: "adobelink",
+ rule: "adobelink,true",
+ },
+
+ {
+ name: "installinstructions",
+ rule: "installinstructions,true",
+ },
+];
+
+add_task(async function() {
+ for (let testcase of Object.values(testcases)) {
+ info(`Running testcase ${testcase.name}`);
+
+ Services.prefs.setCharPref("plugins.favorfallback.rules", testcase.rule);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `${gTestRoot}plugin_favorfallback.html?testcase=${testcase.name}`
+ );
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [testcase.name],
+ async function testPlugins(name) {
+ let testcaseDiv = content.document.getElementById(`testcase_${name}`);
+ let ctpPlugins = testcaseDiv.querySelectorAll(".expected_ctp");
+
+ for (let ctpPlugin of ctpPlugins) {
+ ok(
+ ctpPlugin instanceof Ci.nsIObjectLoadingContent,
+ "This is a plugin object"
+ );
+ is(
+ ctpPlugin.pluginFallbackType,
+ Ci.nsIObjectLoadingContent.PLUGIN_ALTERNATE,
+ "Plugins always use alternate content"
+ );
+ }
+
+ let fallbackPlugins = testcaseDiv.querySelectorAll(
+ ".expected_fallback"
+ );
+
+ for (let fallbackPlugin of fallbackPlugins) {
+ ok(
+ fallbackPlugin instanceof Ci.nsIObjectLoadingContent,
+ "This is a plugin object"
+ );
+ is(
+ fallbackPlugin.pluginFallbackType,
+ Ci.nsIObjectLoadingContent.PLUGIN_ALTERNATE,
+ "Plugin fallback content was used"
+ );
+ }
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js b/browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js
new file mode 100644
index 0000000000..ed68a27688
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js
@@ -0,0 +1,122 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace(
+ "chrome://mochitests/content/",
+ "http://127.0.0.1:8888/"
+);
+var gTestBrowser = null;
+var gPluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+
+add_task(async function() {
+ registerCleanupFunction(function() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(async function() {
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+});
+
+// Test that the plugin "blockall" overlay is always present but hidden,
+// regardless of whether the overlay is fully, partially, or not in the
+// viewport.
+
+// fully in viewport
+add_task(async function() {
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ gTestRoot + "plugin_outsideScrollArea.html"
+ );
+
+ await SpecialPowers.spawn(gTestBrowser, [], async function() {
+ let doc = content.document;
+ let p = doc.createElement("embed");
+
+ p.setAttribute("id", "test");
+ p.setAttribute("type", "application/x-shockwave-flash");
+ p.style.left = "0";
+ p.style.bottom = "200px";
+
+ doc.getElementById("container").appendChild(p);
+ });
+
+ // Work around for delayed PluginBindingAttached
+ await promiseUpdatePluginBindings(gTestBrowser);
+
+ await SpecialPowers.spawn(gTestBrowser, [], async function() {
+ let plugin = content.document.getElementById("test");
+ let overlay = plugin.openOrClosedShadowRoot.getElementById("main");
+ Assert.ok(overlay);
+ Assert.ok(!overlay.getAttribute("visible"));
+ Assert.ok(overlay.getAttribute("blockall") == "blockall");
+ });
+});
+
+// partially in viewport
+add_task(async function() {
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ gTestRoot + "plugin_outsideScrollArea.html"
+ );
+
+ await SpecialPowers.spawn(gTestBrowser, [], async function() {
+ let doc = content.document;
+ let p = doc.createElement("embed");
+
+ p.setAttribute("id", "test");
+ p.setAttribute("type", "application/x-shockwave-flash");
+ p.style.left = "0";
+ p.style.bottom = "-410px";
+
+ doc.getElementById("container").appendChild(p);
+ });
+
+ // Work around for delayed PluginBindingAttached
+ await promiseUpdatePluginBindings(gTestBrowser);
+
+ await SpecialPowers.spawn(gTestBrowser, [], async function() {
+ let plugin = content.document.getElementById("test");
+ let overlay = plugin.openOrClosedShadowRoot.getElementById("main");
+ Assert.ok(overlay);
+ Assert.ok(!overlay.getAttribute("visible"));
+ Assert.ok(overlay.getAttribute("blockall") == "blockall");
+ });
+});
+
+// not in viewport
+add_task(async function() {
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ gTestRoot + "plugin_outsideScrollArea.html"
+ );
+
+ await SpecialPowers.spawn(gTestBrowser, [], async function() {
+ let doc = content.document;
+ let p = doc.createElement("embed");
+
+ p.setAttribute("id", "test");
+ p.setAttribute("type", "application/x-shockwave-flash");
+ p.style.left = "-600px";
+ p.style.bottom = "0";
+
+ doc.getElementById("container").appendChild(p);
+ });
+
+ // Work around for delayed PluginBindingAttached
+ await promiseUpdatePluginBindings(gTestBrowser);
+
+ await SpecialPowers.spawn(gTestBrowser, [], async function() {
+ let plugin = content.document.getElementById("test");
+ let overlay = plugin.openOrClosedShadowRoot.getElementById("main");
+ Assert.ok(overlay);
+ Assert.ok(!overlay.getAttribute("visible"));
+ Assert.ok(overlay.getAttribute("blockall") == "blockall");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_zoom.js b/browser/base/content/test/plugins/browser_CTP_zoom.js
new file mode 100644
index 0000000000..eee2ec1837
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_zoom.js
@@ -0,0 +1,61 @@
+"use strict";
+
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace(
+ "chrome://mochitests/content/",
+ "http://127.0.0.1:8888/"
+);
+
+var gTestBrowser = null;
+
+add_task(async function() {
+ registerCleanupFunction(function() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ FullZoom.reset(); // must be called before closing the tab we zoomed!
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(async function() {
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ gTestRoot + "plugin_zoom.html"
+ );
+
+ // Work around for delayed PluginBindingAttached
+ await promiseUpdatePluginBindings(gTestBrowser);
+});
+
+// Enlarges the zoom level 4 times and tests that the overlay is
+// visible after each enlargement.
+add_task(async function() {
+ for (let count = 0; count < 4; count++) {
+ FullZoom.enlarge();
+
+ // Reload the page
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ gTestRoot + "plugin_zoom.html"
+ );
+ await promiseUpdatePluginBindings(gTestBrowser);
+ await SpecialPowers.spawn(gTestBrowser, [{ count }], async function(args) {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = plugin.openOrClosedShadowRoot.getElementById("main");
+ Assert.ok(
+ overlay &&
+ !overlay.classList.contains("visible") &&
+ overlay.getAttribute("blockall") == "blockall",
+ "Overlay should be present for zoom change count " + args.count
+ );
+ });
+ }
+});
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..4ba565a1a5
--- /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.pluginFallbackType,
+ Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED,
+ "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..be0c779657
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_enable_DRM_prompt.js
@@ -0,0 +1,228 @@
+const TEST_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty_file.html";
+
+/*
+ * Register cleanup function to reset prefs after other tasks have run.
+ */
+
+add_task(async function() {
+ // Note: SpecialPowers.pushPrefEnv has problems with the "Enable DRM"
+ // button on the notification box toggling the prefs. So manually
+ // set/unset the prefs the UI we're testing toggles.
+ let emeWasEnabled = Services.prefs.getBoolPref("media.eme.enabled", false);
+ let cdmWasEnabled = Services.prefs.getBoolPref(
+ "media.gmp-widevinecdm.enabled",
+ false
+ );
+
+ // Restore the preferences to their pre-test state on test finish.
+ registerCleanupFunction(function() {
+ // Unlock incase lock test threw and didn't unlock.
+ Services.prefs.unlockPref("media.eme.enabled");
+ Services.prefs.setBoolPref("media.eme.enabled", emeWasEnabled);
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", cdmWasEnabled);
+ });
+});
+
+/*
+ * Bug 1366167 - Tests that the "Enable DRM" prompt shows if EME is requested while EME is disabled.
+ */
+
+add_task(async function test_drm_prompt_shows_for_toplevel() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async function(browser) {
+ // Turn off EME and Widevine CDM.
+ Services.prefs.setBoolPref("media.eme.enabled", false);
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", false);
+
+ // Have content request access to Widevine, UI should drop down to
+ // prompt user to enable DRM.
+ let result = await SpecialPowers.spawn(browser, [], async function() {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ },
+ ];
+ await content.navigator.requestMediaKeySystemAccess(
+ "com.widevine.alpha",
+ config
+ );
+ } catch (ex) {
+ return { rejected: true };
+ }
+ return { rejected: false };
+ });
+ is(
+ result.rejected,
+ true,
+ "EME request should be denied because EME disabled."
+ );
+
+ // Verify the UI prompt showed.
+ let box = gBrowser.getNotificationBox(browser);
+ let notification = box.currentNotification;
+
+ ok(notification, "Notification should be visible");
+ is(
+ notification.getAttribute("value"),
+ "drmContentDisabled",
+ "Should be showing the right notification"
+ );
+
+ // Verify the "Enable DRM" button is there.
+ let buttons = notification.querySelectorAll(".notification-button");
+ is(buttons.length, 1, "Should have one button.");
+
+ // Prepare a Promise that should resolve when the "Enable DRM" button's
+ // page reload completes.
+ let refreshPromise = BrowserTestUtils.browserLoaded(browser);
+ buttons[0].click();
+
+ // Wait for the reload to complete.
+ await refreshPromise;
+
+ // Verify clicking the "Enable DRM" button enabled DRM.
+ let enabled = Services.prefs.getBoolPref("media.eme.enabled", true);
+ is(
+ enabled,
+ true,
+ "EME should be enabled after click on 'Enable DRM' button"
+ );
+ });
+});
+
+add_task(async function test_eme_locked() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async function(browser) {
+ // Turn off EME and Widevine CDM.
+ Services.prefs.setBoolPref("media.eme.enabled", false);
+ Services.prefs.lockPref("media.eme.enabled");
+
+ // Have content request access to Widevine, UI should drop down to
+ // prompt user to enable DRM.
+ let result = await SpecialPowers.spawn(browser, [], async function() {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ },
+ ];
+ await content.navigator.requestMediaKeySystemAccess(
+ "com.widevine.alpha",
+ config
+ );
+ } catch (ex) {
+ return { rejected: true };
+ }
+ return { rejected: false };
+ });
+ is(
+ result.rejected,
+ true,
+ "EME request should be denied because EME disabled."
+ );
+
+ // Verify the UI prompt did not show.
+ let box = gBrowser.getNotificationBox(browser);
+ let notification = box.currentNotification;
+
+ is(
+ notification,
+ null,
+ "Notification should not be displayed since pref is locked"
+ );
+
+ // Unlock the pref for any tests that follow.
+ Services.prefs.unlockPref("media.eme.enabled");
+ });
+});
+
+/*
+ * Bug 1642465 - Ensure cross origin frames requesting access prompt in the same way as same origin.
+ */
+
+add_task(async function test_drm_prompt_shows_for_cross_origin_iframe() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async function(browser) {
+ // Turn off EME and Widevine CDM.
+ Services.prefs.setBoolPref("media.eme.enabled", false);
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", false);
+
+ // Have content request access to Widevine, UI should drop down to
+ // prompt user to enable DRM.
+ const CROSS_ORIGIN_URL = TEST_URL.replace("example.com", "example.org");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [CROSS_ORIGIN_URL],
+ async function(crossOriginUrl) {
+ let frame = content.document.createElement("iframe");
+ frame.src = crossOriginUrl;
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ return content.SpecialPowers.spawn(frame, [], async function() {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [
+ { contentType: 'video/webm; codecs="vp9"' },
+ ],
+ },
+ ];
+ await content.navigator.requestMediaKeySystemAccess(
+ "com.widevine.alpha",
+ config
+ );
+ } catch (ex) {
+ return { rejected: true };
+ }
+ return { rejected: false };
+ });
+ }
+ );
+ is(
+ result.rejected,
+ true,
+ "EME request should be denied because EME disabled."
+ );
+
+ // Verify the UI prompt showed.
+ let box = gBrowser.getNotificationBox(browser);
+ let notification = box.currentNotification;
+
+ ok(notification, "Notification should be visible");
+ is(
+ notification.getAttribute("value"),
+ "drmContentDisabled",
+ "Should be showing the right notification"
+ );
+
+ // Verify the "Enable DRM" button is there.
+ let buttons = notification.querySelectorAll(".notification-button");
+ is(buttons.length, 1, "Should have one button.");
+
+ // Prepare a Promise that should resolve when the "Enable DRM" button's
+ // page reload completes.
+ let refreshPromise = BrowserTestUtils.browserLoaded(browser);
+ buttons[0].click();
+
+ // Wait for the reload to complete.
+ await refreshPromise;
+
+ // Verify clicking the "Enable DRM" button enabled DRM.
+ let enabled = Services.prefs.getBoolPref("media.eme.enabled", true);
+ is(
+ enabled,
+ true,
+ "EME should be enabled after click on 'Enable DRM' button"
+ );
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
new file mode 100644
index 0000000000..97cb9db618
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
@@ -0,0 +1,63 @@
+"use strict";
+
+let { PluginManager } = ChromeUtils.import(
+ "resource:///actors/PluginParent.jsm"
+);
+
+/**
+ * Test that the notification bar for crashed GMPs works.
+ */
+add_task(async function() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function(browser) {
+ // Ensure the parent has heard before the client.
+ // In practice, this is always true for GMP crashes (but not for NPAPI ones!)
+ let props = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag2
+ );
+ props.setPropertyAsUint32("pluginID", 1);
+ props.setPropertyAsACString("pluginName", "GlobalTestPlugin");
+ props.setPropertyAsACString("pluginDumpID", "1234");
+ Services.obs.notifyObservers(props, "gmp-plugin-crash");
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ const GMP_CRASH_EVENT = {
+ pluginID: 1,
+ pluginName: "GlobalTestPlugin",
+ submittedCrashReport: false,
+ bubbles: true,
+ cancelable: true,
+ gmpPlugin: true,
+ };
+
+ let crashEvent = new content.PluginCrashedEvent(
+ "PluginCrashed",
+ GMP_CRASH_EVENT
+ );
+ content.dispatchEvent(crashEvent);
+ });
+
+ let notification = await waitForNotificationBar(
+ "plugin-crashed",
+ browser
+ );
+
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ ok(notification, "Infobar was shown.");
+ is(
+ notification.priority,
+ notificationBox.PRIORITY_WARNING_MEDIUM,
+ "Correct priority."
+ );
+ is(
+ notification.messageText.textContent,
+ "The GlobalTestPlugin plugin has crashed.",
+ "Correct message."
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/plugins/browser_private_browsing_eme_persistent_state.js b/browser/base/content/test/plugins/browser_private_browsing_eme_persistent_state.js
new file mode 100644
index 0000000000..9a0b91119b
--- /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..d00bd4a446
--- /dev/null
+++ b/browser/base/content/test/plugins/head.js
@@ -0,0 +1,452 @@
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
+});
+
+// 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.loadURI(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 the chrome side nsIPluginTag for this plugin
+function getTestPlugin(aName) {
+ let pluginName = aName || "Test Plug-in";
+ let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ let tags = ph.getPluginTags();
+
+ // Find the test plugin
+ for (let i = 0; i < tags.length; i++) {
+ if (tags[i].name == pluginName) {
+ return tags[i];
+ }
+ }
+ ok(false, "Unable to find plugin");
+ return null;
+}
+
+// Set the 'enabledState' on the nsIPluginTag stored in the main or chrome
+// process.
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+ let name = pluginName || "Test Plug-in";
+ let plugin = getTestPlugin(name);
+ plugin.enabledState = newEnabledState;
+}
+
+// Get the 'enabledState' on the nsIPluginTag stored in the main or chrome
+// process.
+function getTestPluginEnabledState(pluginName) {
+ let name = pluginName || "Test Plug-in";
+ let plugin = getTestPlugin(name);
+ return plugin.enabledState;
+}
+
+// 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 {
+ pluginFallbackType: plugin.pluginFallbackType,
+ activated: plugin.activated,
+ hasRunningPlugin: plugin.hasRunningPlugin,
+ displayedType: plugin.displayedType,
+ };
+ });
+}
+
+// Return a promise and call the plugin's playPlugin() method.
+function promisePlayObject(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return SpecialPowers.spawn(browser, [aId], async function(contentId) {
+ content.document.getElementById(contentId).playPlugin();
+ });
+}
+
+function promiseCrashObject(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return SpecialPowers.spawn(browser, [aId], async function(contentId) {
+ let plugin = content.document.getElementById(contentId);
+ Cu.waiveXrays(plugin).crash();
+ });
+}
+
+// Return a promise and call the plugin's getObjectValue() method.
+function promiseObjectValueResult(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return SpecialPowers.spawn(browser, [aId], async function(contentId) {
+ let plugin = content.document.getElementById(contentId);
+ return Cu.waiveXrays(plugin).getObjectValue();
+ });
+}
+
+// Return a promise and reload the target plugin in the page
+function promiseReloadPlugin(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return SpecialPowers.spawn(browser, [aId], async function(contentId) {
+ let plugin = content.document.getElementById(contentId);
+ // eslint-disable-next-line no-self-assign
+ plugin.src = plugin.src;
+ });
+}
+
+// after a test is done using the plugin doorhanger, we should just clear
+// any permissions that may have crept in
+function clearAllPluginPermissions() {
+ for (let perm of Services.perms.all) {
+ if (perm.type.startsWith("plugin")) {
+ info(
+ "removing permission:" + perm.principal.origin + " " + perm.type + "\n"
+ );
+ Services.perms.removePermission(perm);
+ }
+ }
+}
+
+// Ported from AddonTestUtils.jsm
+let JSONBlocklistWrapper = {
+ /**
+ * Load the data from the specified files into the *real* blocklist providers.
+ * Loads using loadBlocklistRawData, which will treat this as an update.
+ *
+ * @param {nsIFile} dir
+ * The directory in which the files live.
+ * @param {string} prefix
+ * a prefix for the files which ought to be loaded.
+ * This method will suffix -extensions.json and -plugins.json
+ * to the prefix it is given, and attempt to load both.
+ * Insofar as either exists, their data will be dumped into
+ * the respective store, and the respective update handlers
+ * will be called.
+ */
+ async loadBlocklistData(url) {
+ const fullURL = `${url}-plugins.json`;
+ let jsonObj;
+ try {
+ jsonObj = await (await fetch(fullURL)).json();
+ } catch (ex) {
+ ok(false, ex);
+ }
+ info(`Loaded ${fullURL}`);
+
+ return this.loadBlocklistRawData({ plugins: jsonObj });
+ },
+
+ /**
+ * Load the following data into the *real* blocklist providers.
+ * While `overrideBlocklist` replaces the blocklist entirely with a mock
+ * that returns dummy data, this method instead loads data into the actual
+ * blocklist, fires update methods as would happen if this data came from
+ * an actual blocklist update, etc.
+ *
+ * @param {object} data
+ * An object that can optionally have `extensions` and/or `plugins`
+ * properties, each being an array of blocklist items.
+ * This code only uses plugin blocks, that can look something like:
+ *
+ * {
+ * "matchFilename": "libnptest\\.so|nptest\\.dll|Test\\.plugin",
+ * "versionRange": [
+ * {
+ * "severity": "0",
+ * "vulnerabilityStatus": "1"
+ * }
+ * ],
+ * "blockID": "p9999"
+ * }
+ *
+ */
+ async loadBlocklistRawData(data) {
+ const bsPass = ChromeUtils.import(
+ "resource://gre/modules/Blocklist.jsm",
+ null
+ );
+ const blocklistMapping = {
+ extensions: bsPass.ExtensionBlocklistRS,
+ plugins: bsPass.PluginBlocklistRS,
+ };
+
+ for (const [dataProp, blocklistObj] of Object.entries(blocklistMapping)) {
+ let newData = data[dataProp];
+ if (!newData) {
+ continue;
+ }
+ if (!Array.isArray(newData)) {
+ throw new Error(
+ "Expected an array of new items to put in the " +
+ dataProp +
+ " blocklist!"
+ );
+ }
+ for (let item of newData) {
+ if (!item.id) {
+ item.id = uuidGen.generateUUID().number.slice(1, -1);
+ }
+ if (!item.last_modified) {
+ item.last_modified = Date.now();
+ }
+ }
+ await blocklistObj.ensureInitialized();
+ let db = await blocklistObj._client.db;
+ await db.importChanges({}, 42, newData, { clear: true });
+ // We manually call _onUpdate... which is evil, but at the moment kinto doesn't have
+ // a better abstraction unless you want to mock your own http server to do the update.
+ await blocklistObj._onUpdate();
+ }
+ },
+};
+
+// An async helper that insures a new blocklist is loaded (in both
+// processes if applicable).
+async function asyncSetAndUpdateBlocklist(aURL, aBrowser) {
+ let doTestRemote = aBrowser ? aBrowser.isRemoteBrowser : false;
+ let localPromise = TestUtils.topicObserved("plugin-blocklist-updated");
+ info("*** loading blocklist: " + aURL);
+ await JSONBlocklistWrapper.loadBlocklistData(aURL);
+ info("*** waiting on local load");
+ await localPromise;
+ if (doTestRemote) {
+ info("*** waiting on remote load");
+ // Ensure content has been updated with the blocklist
+ await SpecialPowers.spawn(aBrowser, [], () => {});
+ }
+ info("*** blocklist loaded.");
+}
+
+// Insure there's a popup notification present. This test does not indicate
+// open state. aBrowser can be undefined.
+function promisePopupNotification(aName, aBrowser) {
+ return new Promise(resolve => {
+ waitForCondition(
+ () => PopupNotifications.getNotification(aName, aBrowser),
+ () => {
+ ok(
+ !!PopupNotifications.getNotification(aName, aBrowser),
+ aName + " notification appeared"
+ );
+
+ resolve();
+ },
+ "timeout waiting for popup notification " + aName
+ );
+ });
+}
+
+/**
+ * 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);
+ });
+}
+
+/**
+ * Due to layout being async, "PluginBindAttached" may trigger later. This
+ * returns a Promise that resolves once we've forced a layout flush, which
+ * triggers the PluginBindAttached event to fire. This trick only works if
+ * there is some sort of plugin in the page.
+ * @param browser
+ * The browser to force plugin bindings in.
+ * @return Promise
+ */
+function promiseUpdatePluginBindings(browser) {
+ return SpecialPowers.spawn(browser, [], async function() {
+ let doc = content.document;
+ let elems = doc.getElementsByTagName("embed");
+ if (!elems || elems.length < 1) {
+ elems = doc.getElementsByTagName("object");
+ }
+ if (elems && elems.length) {
+ elems[0].clientTop;
+ }
+ });
+}
diff --git a/browser/base/content/test/plugins/plugin_bug797677.html b/browser/base/content/test/plugins/plugin_bug797677.html
new file mode 100644
index 0000000000..1545f36475
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_bug797677.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body><embed id="plugin" type="9000"></embed></body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_favorfallback.html b/browser/base/content/test/plugins/plugin_favorfallback.html
new file mode 100644
index 0000000000..6eaf154994
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_favorfallback.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body>
+<style>
+.testcase {
+ display: none;
+}
+object {
+ width: 200px;
+ height: 200px;
+}
+</style>
+
+<!-- Tests that a <video> tag in the fallback content favors the fallback content -->
+<div id="testcase_video" class="testcase">
+ <object class="expected_ctp" type="application/x-shockwave-flash-test">
+ Unexpected fallback
+ </object>
+ <object class="expected_fallback" type="application/x-shockwave-flash-test">
+ <video></video>
+ Expected fallback
+ </object>
+</div>
+
+<!-- Tests that an object with no src specified (no data="") favors the fallback content -->
+<div id="testcase_nosrc" class="testcase">
+ <!-- We must use an existing and valid file here because otherwise the failed load
+ triggers the plugin's alternate content, indepedent of the favor-fallback code path -->
+ <object class="expected_ctp" type="application/x-shockwave-flash-test" data="plugin_simple_blank.swf">
+ Unexpected fallback
+ </object>
+ <object class="expected_fallback" type="application/x-shockwave-flash-test">
+ Expected fallback
+ </object>
+</div>
+
+<!-- Tests that an <embed> tag in the fallback content forces the plugin content,
+ when fallback is defaulting to true -->
+<div id="testcase_embed" class="testcase">
+ <object class="expected_ctp" type="application/x-shockwave-flash-test">
+ <embed></embed>
+ Unexpected fallback
+ </object>
+ <object class="expected_fallback" type="application/x-shockwave-flash-test">
+ Expected fallback
+ </object>
+</div>
+
+<!-- Tests that links to adobe.com inside the fallback content forces the plugin content,
+ when fallback is defaulting to true -->
+<div id="testcase_adobelink" class="testcase">
+ <object class="expected_ctp" type="application/x-shockwave-flash-test">
+ <a href="https://www.adobe.com">Go to adobe.com</a>
+ Unexpected fallback
+ </object>
+ <object class="expected_ctp" type="application/x-shockwave-flash-test">
+ <a href="https://adobe.com">Go to adobe.com</a>
+ Unexpected fallback
+ </object>
+ <object class="expected_fallback" type="application/x-shockwave-flash-test">
+ Expected fallback
+ </object>
+</div>
+
+<!-- Tests that instructions to download or install flash inside the fallback content
+ forces the plugin content, when fallback is defaulting to true -->
+<div id="testcase_installinstructions" class="testcase">
+ <object class="expected_ctp" type="application/x-shockwave-flash-test">
+ Install -- Unexpected fallback
+ </object>
+ <object class="expected_ctp" type="application/x-shockwave-flash-test">
+ Flash -- Unexpected fallback
+ </object>
+ <object class="expected_ctp" type="application/x-shockwave-flash-test">
+ Download -- Unexpected fallback
+ </object>
+ <object class="expected_fallback" type="application/x-shockwave-flash-test">
+ <!-- Tests that the words Install, Flash or Download do not trigger
+ this behavior if it's just inside a comment, and not part of
+ the text content -->
+ Expected Fallback
+ </object>
+ <object class="expected_fallback" type="application/x-shockwave-flash-test">
+ Expected fallback
+ </object>
+</div>
+
+<script>
+ let queryString = location.search;
+ let match = /^\?testcase=([a-z]+)$/.exec(queryString);
+ let testcase = match[1];
+ document.getElementById(`testcase_${testcase}`).style.display = "block";
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_outsideScrollArea.html b/browser/base/content/test/plugins/plugin_outsideScrollArea.html
new file mode 100644
index 0000000000..c6ef50d5db
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_outsideScrollArea.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<style type="text/css">
+#container {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ background: blue;
+}
+
+#test {
+ width: 400px;
+ height: 400px;
+ position: absolute;
+}
+</style>
+</head>
+<body>
+ <div id="container"></div>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_simple_blank.swf b/browser/base/content/test/plugins/plugin_simple_blank.swf
new file mode 100644
index 0000000000..b846387eb8
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_simple_blank.swf
Binary files differ
diff --git a/browser/base/content/test/plugins/plugin_test.html b/browser/base/content/test/plugins/plugin_test.html
new file mode 100644
index 0000000000..3d4f43e6a5
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 300px; height: 300px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_zoom.html b/browser/base/content/test/plugins/plugin_zoom.html
new file mode 100644
index 0000000000..f9e5986581
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_zoom.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<!-- The odd width and height are here to trigger bug 972237. -->
+<embed id="test" style="width: 99.789%; height: 99.123%" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/popupNotifications/.eslintrc.js b/browser/base/content/test/popupNotifications/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/popupNotifications/browser.ini b/browser/base/content/test/popupNotifications/browser.ini
new file mode 100644
index 0000000000..695115a517
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -0,0 +1,30 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_displayURI.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_2.js]
+skip-if = (os == "linux" && (debug || asan)) || (os == "linux" && bits == 64 && os_version == "18.04") # bug 1251135
+[browser_popupNotification_3.js]
+skip-if = (os == "linux" && (debug || asan)) || verify
+[browser_popupNotification_4.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_5.js]
+skip-if = true # bug 1332646
+[browser_popupNotification_accesskey.js]
+skip-if = (os == "linux" && (debug || asan)) || os == "mac"
+[browser_popupNotification_checkbox.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_selection_required.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_keyboard.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_learnmore.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_no_anchors.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_reshow_in_background.js]
+skip-if = (os == "linux" && (debug || asan))
diff --git a/browser/base/content/test/popupNotifications/browser_displayURI.js b/browser/base/content/test/popupNotifications/browser_displayURI.js
new file mode 100644
index 0000000000..4383322b57
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_displayURI.js
@@ -0,0 +1,156 @@
+/*
+ * Make sure that the correct origin is shown for permission prompts.
+ */
+
+async function check(contentTask, options = {}) {
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com/",
+ async function(browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ ok(
+ body.innerHTML.includes("example.com"),
+ "Check that at least the eTLD+1 is present in the markup"
+ );
+ }
+ );
+
+ let channel = NetUtil.newChannel({
+ uri: getRootDirectory(gTestPath),
+ loadUsingSystemPrincipal: true,
+ });
+ channel = channel.QueryInterface(Ci.nsIFileChannel);
+
+ await BrowserTestUtils.withNewTab(channel.file.path, async function(browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ if (
+ notification.id == "geolocation-notification" ||
+ notification.id == "xr-notification"
+ ) {
+ ok(
+ body.innerHTML.includes("local file"),
+ `file:// URIs should be displayed as local file.`
+ );
+ } else {
+ ok(
+ body.innerHTML.includes("Unknown origin"),
+ "file:// URIs should be displayed as unknown origin."
+ );
+ }
+ });
+
+ if (!options.skipOnExtension) {
+ // Test the scenario also on the extension page if not explicitly unsupported
+ // (e.g. an extension page can't be navigated on a blob URL).
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test Extension Name",
+ },
+ background() {
+ let { browser } = this;
+ browser.test.sendMessage(
+ "extension-tab-url",
+ browser.extension.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_task(async function setup() {
+ 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..38d0c9a8f4
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification.js
@@ -0,0 +1,393 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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() {
+ 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..416010a67c
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
@@ -0,0 +1,304 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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,
+ "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;
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Next load will remove the notification
+ this.complete = true;
+ 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,
+ "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;
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Next load will hide the notification
+ this.notification.options.timeout = Date.now() - 1;
+ this.complete = true;
+ 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,
+ "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;
+
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ 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..b5ab4e3377
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
@@ -0,0 +1,366 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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,
+ "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",
+ run() {
+ // Set the security delay to 100s
+ PopupNotifications.buttonDelay = 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",
+ run() {
+ // Set the security delay to 10ms
+ PopupNotifications.buttonDelay = 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"
+ );
+ PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
+ },
+ },
+ // reload removes notification
+ {
+ id: "Test#6",
+ async run() {
+ 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,
+ "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() {
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let originalTab = gBrowser.selectedTab;
+ let bgTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "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 =>
+ wgp.documentURI.spec.startsWith("http://example.org/")
+ ),
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() {
+ content.document
+ .getElementById("iframe")
+ .setAttribute("src", "http://example.org/");
+ }),
+ ]);
+
+ executeSoon(() => {
+ let notification = PopupNotifications.getNotification(
+ this.notifyObj.id,
+ this.notifyObj.browser
+ );
+ ok(
+ notification != null,
+ "Notification remained when subframe navigated"
+ );
+ this.notifyObj.options.eventCallback = undefined;
+
+ notification.remove();
+ });
+ },
+ onHidden() {},
+ },
+ // Popup Notifications should catch exceptions from callbacks
+ {
+ id: "Test#10",
+ run() {
+ this.testNotif1 = new BasicNotification(this.id);
+ this.testNotif1.message += " 1";
+ this.notification1 = showNotification(this.testNotif1);
+ this.testNotif1.options.eventCallback = function(eventName) {
+ info("notifyObj1.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 1!");
+ }
+ };
+
+ this.testNotif2 = new BasicNotification(this.id);
+ this.testNotif2.message += " 2";
+ this.testNotif2.id += "-2";
+ this.testNotif2.options.eventCallback = function(eventName) {
+ info("notifyObj2.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 2!");
+ }
+ };
+ this.notification2 = showNotification(this.testNotif2);
+ },
+ onShown(popup) {
+ is(popup.children.length, 2, "two notifications are shown");
+ dismissNotification(popup);
+ },
+ onHidden() {
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
new file mode 100644
index 0000000000..27375a172d
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
@@ -0,0 +1,289 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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,
+ "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,
+ "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.mainAction.disableHighlight = true;
+ 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..1159844551
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
@@ -0,0 +1,496 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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,
+ "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;
+
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ 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,
+ "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,
+ "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,
+ "http://example.com/"
+ );
+ let firstTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let shown = waitForNotificationPanel();
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.persistent = true;
+ this.notification = showNotification(notifyObj);
+ await shown;
+
+ ok(
+ notifyObj.shownCallbackTriggered,
+ "Should have triggered the shown event"
+ );
+ ok(
+ notifyObj.showingCallbackTriggered,
+ "Should have triggered the showing event"
+ );
+ // Reset to false so that we can ensure these are not fired a second time.
+ notifyObj.shownCallbackTriggered = false;
+ notifyObj.showingCallbackTriggered = false;
+ let timeShown = this.notification.timeShown;
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(firstTab);
+ let win = await promiseWin;
+
+ let anchor = win.document.getElementById("default-notification-icon");
+ win.PopupNotifications._reshowNotifications(anchor);
+ ok(
+ !win.PopupNotifications.panel.children.length,
+ "no notification displayed in new window"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ let id = PopupNotifications.panel.firstElementChild.getAttribute(
+ "popupid"
+ );
+ ok(
+ id.endsWith("Test#7"),
+ "Should have found the notification from Test7"
+ );
+ ok(
+ PopupNotifications.isPanelOpen,
+ "Should have kept the popup on the first window"
+ );
+ ok(
+ !notifyObj.dismissalCallbackTriggered,
+ "Should not have triggered a dismissed event"
+ );
+ ok(
+ !notifyObj.shownCallbackTriggered,
+ "Should not have triggered a second shown event"
+ );
+ ok(
+ !notifyObj.showingCallbackTriggered,
+ "Should not have triggered a second showing event"
+ );
+ ok(
+ this.notification.timeShown > timeShown,
+ "should have updated timeShown to restart the security delay"
+ );
+
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+
+ goNext();
+ },
+ },
+ // Test that only the first persistent notification is shown on update
+ {
+ id: "Test#8",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj1);
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that persistent notifications are shown stacked by anchor on update
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notifyObj3 = new BasicNotification(this.id);
+ this.notifyObj3.id += "_3";
+ this.notifyObj3.anchorID = "default-notification-icon";
+ this.notifyObj3.options.persistent = true;
+ this.notification3 = showNotification(this.notifyObj3);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 2, "two notifications displayed");
+ let [notification1, notification2] = notifications;
+ is(
+ notification1.id,
+ this.notifyObj1.id + "-notification",
+ "id 1 matches"
+ );
+ is(
+ notification2.id,
+ this.notifyObj3.id + "-notification",
+ "id 2 matches"
+ );
+
+ this.notification1.remove();
+ this.notification2.remove();
+ this.notification3.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that on closebutton click, only the persistent notification
+ // that contained the closebutton loses its persistent status.
+ {
+ id: "Test#10",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "geo-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notifyObj1.options.hideClose = false;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notifyObj2.options.hideClose = false;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notifyObj3 = new BasicNotification(this.id);
+ this.notifyObj3.id += "_3";
+ this.notifyObj3.anchorID = "geo-notification-icon";
+ this.notifyObj3.options.persistent = true;
+ this.notifyObj3.options.hideClose = false;
+ this.notification3 = showNotification(this.notifyObj3);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 3, "three notifications displayed");
+ EventUtils.synthesizeMouseAtCenter(notifications[1].closebutton, {});
+ },
+ onHidden(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 2, "two notifications displayed");
+
+ ok(this.notification1.options.persistent, "notification 1 is persistent");
+ ok(
+ !this.notification2.options.persistent,
+ "notification 2 is not persistent"
+ );
+ ok(this.notification3.options.persistent, "notification 3 is persistent");
+
+ this.notification1.remove();
+ this.notification2.remove();
+ this.notification3.remove();
+ },
+ },
+ // Test clicking the anchor icon.
+ // Clicking the anchor of an already visible persistent notification should
+ // focus the main action button, but not cause additional showing/shown event
+ // callback calls.
+ // Clicking the anchor of a dismissed notification should show it, even when
+ // the currently displayed notification is a persistent one.
+ {
+ id: "Test#11",
+ async run() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+
+ function clickAnchor(notifyObj) {
+ let anchor = document.getElementById(notifyObj.anchorID);
+ EventUtils.synthesizeMouseAtCenter(anchor, {});
+ }
+
+ let popup = PopupNotifications.panel;
+
+ let notifyObj1 = new BasicNotification(this.id);
+ notifyObj1.id += "_1";
+ notifyObj1.anchorID = "default-notification-icon";
+ notifyObj1.options.persistent = true;
+ let shown = waitForNotificationPanel();
+ let notification1 = showNotification(notifyObj1);
+ await shown;
+ checkPopup(popup, notifyObj1);
+ ok(
+ !notifyObj1.dismissalCallbackTriggered,
+ "Should not have dismissed the notification"
+ );
+ notifyObj1.shownCallbackTriggered = false;
+ notifyObj1.showingCallbackTriggered = false;
+
+ // Click the anchor. This should focus the closebutton
+ // (because it's the first focusable element), but not
+ // call event callbacks on the notification object.
+ clickAnchor(notifyObj1);
+ is(document.activeElement, popup.children[0].closebutton);
+ ok(
+ !notifyObj1.dismissalCallbackTriggered,
+ "Should not have dismissed the notification"
+ );
+ ok(
+ !notifyObj1.shownCallbackTriggered,
+ "Should have triggered the shown event again"
+ );
+ ok(
+ !notifyObj1.showingCallbackTriggered,
+ "Should have triggered the showing event again"
+ );
+
+ // Add another notification.
+ let notifyObj2 = new BasicNotification(this.id);
+ notifyObj2.id += "_2";
+ notifyObj2.anchorID = "geo-notification-icon";
+ notifyObj2.options.dismissed = true;
+ let notification2 = showNotification(notifyObj2);
+
+ // Click the anchor of the second notification, this should dismiss the
+ // first notification.
+ shown = waitForNotificationPanel();
+ clickAnchor(notifyObj2);
+ await shown;
+ checkPopup(popup, notifyObj2);
+ ok(
+ notifyObj1.dismissalCallbackTriggered,
+ "Should have dismissed the first notification"
+ );
+
+ // Click the anchor of the first notification, it should be shown again.
+ shown = waitForNotificationPanel();
+ clickAnchor(notifyObj1);
+ await shown;
+ checkPopup(popup, notifyObj1);
+ ok(
+ notifyObj2.dismissalCallbackTriggered,
+ "Should have dismissed the second notification"
+ );
+
+ // Cleanup.
+ notification1.remove();
+ notification2.remove();
+ goNext();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js
new file mode 100644
index 0000000000..4a68105e27
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+let buttonPressed = false;
+
+function commandTriggered() {
+ buttonPressed = true;
+}
+
+var tests = [
+ // This test ensures that the accesskey closes the popup.
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ window.addEventListener("command", commandTriggered, true);
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("VK_ALT", { type: "keydown" });
+ EventUtils.synthesizeKey("M", { altKey: true });
+ EventUtils.synthesizeKey("VK_ALT", { type: "keyup" });
+
+ // If bug xxx was present, then the popup would be in the
+ // process of being hidden right now.
+ isnot(popup.state, "hiding", "popup is not hiding");
+ },
+ onHidden(popup) {
+ window.removeEventListener("command", commandTriggered, true);
+ ok(buttonPressed, "button pressed");
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
new file mode 100644
index 0000000000..30339d4b66
--- /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_keyboard.js b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
new file mode 100644
index 0000000000..7517f78b51
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -0,0 +1,274 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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",
+ async run() {
+ await waitForWindowReadyForPopupNotifications(window);
+ 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..ed59bd33de
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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,
+ "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"
+ );
+
+ 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,
+ "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_selection_required.js b/browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js
new file mode 100644
index 0000000000..150d769e7c
--- /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 BrowserTestUtils.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..0d347f976c
--- /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, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tabB.linkedBrowser);
+
+ let tabC = BrowserTestUtils.addTab(gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tabC.linkedBrowser);
+
+ let seenEvents = [];
+
+ let options = {
+ dismissed: false,
+ eventCallback(popupEvent) {
+ seenEvents.push(popupEvent);
+ },
+ };
+
+ let notification = PopupNotifications.show(
+ tabC.linkedBrowser,
+ "test-notification",
+ "",
+ "plugins-notification-icon",
+ null,
+ null,
+ options
+ );
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ await BrowserTestUtils.switchTab(gBrowser, tabB);
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ notification.reshow();
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ let panelShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tabC);
+ await panelShown;
+
+ Assert.equal(seenEvents.length, 2, "Should have seen two events.");
+ Assert.equal(
+ seenEvents[0],
+ "showing",
+ "Should have said popup was showing."
+ );
+ Assert.equal(seenEvents[1], "shown", "Should have said popup was shown.");
+
+ let panelHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ PopupNotifications.remove(notification);
+ await panelHidden;
+
+ BrowserTestUtils.removeTab(tabB);
+ BrowserTestUtils.removeTab(tabC);
+ }
+);
diff --git a/browser/base/content/test/popupNotifications/head.js b/browser/base/content/test/popupNotifications/head.js
new file mode 100644
index 0000000000..4ae7657fe0
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -0,0 +1,376 @@
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+
+/**
+ * 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.jsm makes before it
+ // allows a notification to open.
+ await BrowserTestUtils.waitForCondition(
+ () => win.gBrowser.selectedBrowser.docShellIsActive,
+ "The browser should be active"
+ );
+ await BrowserTestUtils.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.loadURI(browser, url);
+ }
+
+ return BrowserTestUtils.browserLoaded(browser, false, url);
+}
+
+const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref(
+ "security.notification_enable_delay"
+);
+
+// Tests that call setup() should have a `tests` array defined for the actual
+// tests to be run.
+/* global tests */
+function setup() {
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/").then(
+ goNext
+ );
+ registerCleanupFunction(() => {
+ gBrowser.removeTab(gBrowser.selectedTab);
+ PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
+ });
+}
+
+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 = {
+ 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;
+ }
+ 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"
+ );
+ is(
+ notification.hasAttribute("buttonhighlight"),
+ !notifyObj.mainAction.disableHighlight,
+ "main action highlight matches"
+ );
+ }
+ if (notifyObj.secondaryActions && notifyObj.secondaryActions.length) {
+ let secondaryAction = notifyObj.secondaryActions[0];
+ is(
+ notification.getAttribute("secondarybuttonlabel"),
+ secondaryAction.label,
+ "secondary action label matches"
+ );
+ is(
+ notification.getAttribute("secondarybuttonaccesskey"),
+ secondaryAction.accessKey,
+ "secondary action accesskey matches"
+ );
+ }
+ // Additional secondary actions appear as menu items.
+ let actualExtraSecondaryActions = Array.prototype.filter.call(
+ notification.menupopup.childNodes,
+ child => child.nodeName == "menuitem"
+ );
+ let extraSecondaryActions = notifyObj.secondaryActions
+ ? notifyObj.secondaryActions.slice(1)
+ : [];
+ is(
+ actualExtraSecondaryActions.length,
+ extraSecondaryActions.length,
+ "number of extra secondary actions matches"
+ );
+ extraSecondaryActions.forEach(function(a, i) {
+ is(
+ actualExtraSecondaryActions[i].getAttribute("label"),
+ a.label,
+ "label for extra secondary action " + i + " matches"
+ );
+ is(
+ actualExtraSecondaryActions[i].getAttribute("accesskey"),
+ a.accessKey,
+ "accessKey for extra secondary action " + i + " matches"
+ );
+ });
+}
+
+XPCOMUtils.defineLazyGetter(this, "gActiveListeners", () => {
+ let listeners = new Map();
+ registerCleanupFunction(() => {
+ for (let [listener, eventName] of listeners) {
+ PopupNotifications.panel.removeEventListener(eventName, listener);
+ }
+ });
+ return listeners;
+});
+
+function onPopupEvent(eventName, callback, condition) {
+ let listener = event => {
+ if (
+ event.target != PopupNotifications.panel ||
+ (condition && !condition())
+ ) {
+ return;
+ }
+ PopupNotifications.panel.removeEventListener(eventName, listener);
+ gActiveListeners.delete(listener);
+ executeSoon(() => callback.call(PopupNotifications.panel));
+ };
+ gActiveListeners.set(listener, eventName);
+ PopupNotifications.panel.addEventListener(eventName, listener);
+}
+
+function waitForNotificationPanel() {
+ return new Promise(resolve => {
+ onPopupEvent("popupshown", function() {
+ resolve(this);
+ });
+ });
+}
+
+function waitForNotificationPanelHidden() {
+ return new Promise(resolve => {
+ onPopupEvent("popuphidden", function() {
+ resolve(this);
+ });
+ });
+}
+
+function triggerMainCommand(popup) {
+ let notifications = popup.childNodes;
+ ok(!!notifications.length, "at least one notification displayed");
+ let notification = notifications[0];
+ info("Triggering main command for notification " + notification.id);
+ EventUtils.synthesizeMouseAtCenter(notification.button, {});
+}
+
+function triggerSecondaryCommand(popup, index) {
+ let notifications = popup.childNodes;
+ ok(!!notifications.length, "at least one notification displayed");
+ let notification = notifications[0];
+ info("Triggering secondary command for notification " + notification.id);
+
+ if (index == 0) {
+ EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
+ return;
+ }
+
+ // Extra secondary actions appear in a menu.
+ notification.secondaryButton.nextElementSibling.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/.eslintrc.js b/browser/base/content/test/popups/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/popups/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/popups/browser.ini b/browser/base/content/test/popups/browser.ini
new file mode 100644
index 0000000000..aba7cf6695
--- /dev/null
+++ b/browser/base/content/test/popups/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files =
+ head.js
+[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') || (e10s && debug) # Frequent bug 1081925 and bug 1125520 failures
+[browser_popup_frames.js]
+support-files =
+ popup_blocker.html
+ popup_blocker_a.html
+ popup_blocker_b.html
+[browser_popup_blocker_identity_block.js]
+support-files =
+ popup_blocker2.html
+ popup_blocker_a.html
+[browser_popup_close_main_window.js]
+[browser_popup_blocker_frames.js]
+support-files =
+ popup_blocker.html
+ popup_blocker_a.html
+ popup_blocker_b.html
+[browser_popup_blocker_iframes.js]
+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.
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..6b65cee57a
--- /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 = [
+ [true, BUTTONS_MAY_VISIBLE],
+ [false, 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.setBoolPref("browser.tabs.drawInTitlebar", 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.drawInTitlebar");
+ 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..23cc01e46c
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+function clearAllPermissionsByPrefix(aPrefix) {
+ for (let perm of Services.perms.all) {
+ if (perm.type.startsWith(aPrefix)) {
+ Services.perms.removePermission(perm);
+ }
+ }
+}
+
+add_task(async function setup() {
+ // 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"
+ );
+
+ // Wait for the popup-blocked notification.
+ let notification;
+ await TestUtils.waitForCondition(
+ () =>
+ (notification = gBrowser
+ .getNotificationBox()
+ .getNotificationWithValue("popup-blocked"))
+ );
+
+ // Show the menu.
+ let popupShown = BrowserTestUtils.waitForEvent(window, "popupshown");
+ let popupFilled = waitForBlockedPopups(2);
+ notification.querySelector("button").doCommand();
+ 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);
+ }
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
+
+ // Press the button.
+ let allow = document.getElementById("blockedPopupAllowSite");
+ allow.doCommand();
+ await TestUtils.waitForCondition(
+ () =>
+ popupTabs.length == 2 &&
+ popupTabs.every(
+ aTab => aTab.linkedBrowser.currentURI.spec != "about:blank"
+ )
+ );
+
+ 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"
+ );
+
+ // Clean up.
+ gBrowser.removeTab(tab);
+ for (let popup of popupTabs) {
+ 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..bcde4b5a64
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker_frames.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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",
+ "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() {
+ await test_opening_blocked_popups("http://example.com/");
+});
+
+add_task(async function() {
+ 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..1ab9b1805e
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker_identity_block.js
@@ -0,0 +1,241 @@
+"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 { SitePermissions } = ChromeUtils.import(
+ "resource:///modules/SitePermissions.jsm"
+);
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const URL = baseURL + "popup_blocker2.html";
+const URI = Services.io.newURI(URL);
+const PRINCIPAL = Services.scriptSecurityManager.createContentPrincipal(
+ URI,
+ {}
+);
+
+function openIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ return promise;
+}
+
+function closeIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ gIdentityHandler._identityPopup.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 openIdentityPopup();
+ Assert.equal(document.getElementById("blocked-popup-indicator-item"), null);
+ await closeIdentityPopup();
+
+ // Blocked popup notification icon should be hidden in the identity block when no popups are blocked.
+ let icon = gIdentityHandler._identityBox.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-icon").click();
+ await openIdentityPopup();
+ await TestUtils.waitForCondition(
+ () => document.getElementById("blocked-popup-indicator-item") !== null
+ );
+
+ // Check that the default state is correctly set to "Block".
+ let menulist = document.getElementById("identity-popup-popup-menulist");
+ Assert.equal(menulist.value, "0");
+ Assert.equal(menulist.label, "Block");
+
+ await closeIdentityPopup();
+
+ // 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 openIdentityPopup();
+ 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 openIdentityPopup();
+ let menulist = document.getElementById("identity-popup-popup-menulist");
+ menulist.menupopup.openPopup(); // Open the allow/block menu
+ let menuitem = menulist.getElementsByTagName("menuitem")[0];
+ menuitem.click();
+ await closeIdentityPopup();
+
+ 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 openIdentityPopup();
+ menulist = document.getElementById("identity-popup-popup-menulist");
+ menulist.menupopup.openPopup(); // Open the allow/block menu
+ menuitem = menulist.getElementsByTagName("menuitem")[1];
+ menuitem.click();
+ await closeIdentityPopup();
+
+ // 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.jsm considers setting default permissions to be removal.
+ PermissionTestUtils.add(URI, "popup", Ci.nsIPermissionManager.DENY_ACTION);
+
+ await openIdentityPopup();
+ let menulist = document.getElementById("identity-popup-popup-menulist");
+ Assert.equal(menulist.value, "0");
+ Assert.equal(menulist.label, "Block");
+ await closeIdentityPopup();
+
+ PermissionTestUtils.add(URI, "popup", Services.perms.ALLOW_ACTION);
+
+ await openIdentityPopup();
+ menulist = document.getElementById("identity-popup-popup-menulist");
+ Assert.equal(menulist.value, "1");
+ Assert.equal(menulist.label, "Allow");
+ await closeIdentityPopup();
+
+ 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..c3919cd497
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker_iframes.js
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const testURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+);
+
+const examplecomURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+const w3cURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://w3c-test.org"
+);
+
+const examplenetURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.net"
+);
+
+const prefixexamplecomURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://prefixexample.com"
+);
+
+class TestCleanup {
+ constructor() {
+ this.tabs = [];
+ }
+
+ count() {
+ return this.tabs.length;
+ }
+
+ static setup() {
+ let cleaner = new TestCleanup();
+ this.onTabOpen = event => {
+ cleaner.tabs.push(event.target);
+ };
+ gBrowser.tabContainer.addEventListener("TabOpen", this.onTabOpen);
+ return cleaner;
+ }
+
+ clean() {
+ gBrowser.tabContainer.removeEventListener("TabOpen", this.onTabOpen);
+ for (let tab of this.tabs) {
+ gBrowser.removeTab(tab);
+ }
+ }
+}
+
+async function runTest(count, urls, permissions, delayedAllow) {
+ let cleaner = TestCleanup.setup();
+
+ // Enable the popup blocker.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+ });
+
+ await SpecialPowers.pushPermissions(permissions);
+
+ // Open the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ let contexts = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [...urls, !!delayedAllow],
+ async (url1, url2, url3, url4, delay) => {
+ let iframe1 = content.document.createElement("iframe");
+ let iframe2 = content.document.createElement("iframe");
+ iframe1.id = "iframe1";
+ iframe2.id = "iframe2";
+ iframe1.src = new URL(
+ `popup_blocker_frame.html?delayed=${delay}&base=${url3}`,
+ url1
+ );
+ iframe2.src = new URL(
+ `popup_blocker_frame.html?delayed=${delay}&base=${url4}`,
+ url2
+ );
+
+ let promises = [
+ new Promise(resolve => (iframe1.onload = resolve)),
+ new Promise(resolve => (iframe2.onload = resolve)),
+ ];
+
+ content.document.body.appendChild(iframe1);
+ content.document.body.appendChild(iframe2);
+
+ await Promise.all(promises);
+ return [iframe1.browsingContext, iframe2.browsingContext];
+ }
+ );
+
+ if (delayedAllow) {
+ await delayedAllow();
+ await SpecialPowers.spawn(tab.linkedBrowser, contexts, async function(
+ bc1,
+ bc2
+ ) {
+ bc1.window.postMessage("allow", "*");
+ bc2.window.postMessage("allow", "*");
+ });
+ }
+
+ await TestUtils.waitForCondition(
+ () => cleaner.count() == count,
+ `waiting for ${count} tabs, got ${cleaner.count()}`
+ );
+
+ ok(cleaner.count() == count, `should have ${count} tabs`);
+
+ await SpecialPowers.popPermissions();
+ cleaner.clean();
+}
+
+add_task(async function() {
+ let permission = {
+ type: "popup",
+ allow: true,
+ context: "",
+ };
+
+ let expected = [];
+
+ let tests = [
+ [examplecomURL, w3cURL, prefixexamplecomURL, examplenetURL],
+ [examplecomURL, examplecomURL, prefixexamplecomURL, examplenetURL],
+ [examplecomURL, examplecomURL, prefixexamplecomURL, prefixexamplecomURL],
+ [examplecomURL, w3cURL, prefixexamplecomURL, prefixexamplecomURL],
+ ];
+
+ permission.context = testURL;
+ expected = [5, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [permission]);
+ }
+
+ permission.context = examplecomURL;
+ expected = [3, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [permission]);
+ }
+
+ permission.context = prefixexamplecomURL;
+ expected = [3, 3, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [permission]);
+ }
+
+ async function allowPopup() {
+ await SpecialPowers.pushPermissions([permission]);
+ }
+
+ permission.context = testURL;
+ expected = [5, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [], allowPopup);
+ }
+
+ permission.context = examplecomURL;
+ expected = [3, 5, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [], allowPopup);
+ }
+
+ permission.context = prefixexamplecomURL;
+ expected = [3, 3, 3, 3];
+ for (let test in tests) {
+ await runTest(expected[test], tests[test], [], allowPopup);
+ }
+});
diff --git a/browser/base/content/test/popups/browser_popup_close_main_window.js b/browser/base/content/test/popups/browser_popup_close_main_window.js
new file mode 100644
index 0000000000..7698684a95
--- /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..37b306b0eb
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_frames.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/. */
+
+const baseURL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "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() {
+ await test_opening_blocked_popups("http://example.com/");
+});
+
+add_task(async function() {
+ await test_opening_blocked_popups("http://w3c-test.org/");
+});
diff --git a/browser/base/content/test/popups/head.js b/browser/base/content/test/popups/head.js
new file mode 100644
index 0000000000..47e43e904c
--- /dev/null
+++ b/browser/base/content/test/popups/head.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function waitForBlockedPopups(numberOfPopups) {
+ let menupopup = document.getElementById("blockedPopupOptions");
+ await BrowserTestUtils.waitForCondition(() => {
+ let popups = menupopup.querySelectorAll("[popupReportIndex]");
+ return popups.length == numberOfPopups;
+ }, `Waiting for ${numberOfPopups} popups`);
+}
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/protectionsUI/.eslintrc.js b/browser/base/content/test/protectionsUI/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/protectionsUI/benignPage.html b/browser/base/content/test/protectionsUI/benignPage.html
new file mode 100644
index 0000000000..0be1cbc1c7
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/benignPage.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <!--TODO: We used to have an iframe here, to double-check that benign-->
+ <!--iframes may be included in pages. However, the cookie restrictions-->
+ <!--project introduced a change that declared blockable content to be-->
+ <!--found on any page that embeds iframes, rendering this unusable for-->
+ <!--our purposes. That's not ideal and we intend to restore this iframe.-->
+ <!--(See bug 1511303 for a more detailed technical explanation.)-->
+ <!--<iframe src="http://not-tracking.example.com/"></iframe>-->
+ </body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/browser.ini b/browser/base/content/test/protectionsUI/browser.ini
new file mode 100644
index 0000000000..fb863d440a
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser.ini
@@ -0,0 +1,41 @@
+[DEFAULT]
+tags = trackingprotection
+support-files =
+ head.js
+ benignPage.html
+ containerPage.html
+ cookiePage.html
+ cookieSetterPage.html
+ cookieServer.sjs
+ embeddedPage.html
+ trackingAPI.js
+ trackingPage.html
+
+[browser_protectionsUI.js]
+[browser_protectionsUI_3.js]
+[browser_protectionsUI_animation.js]
+[browser_protectionsUI_animation_2.js]
+[browser_protectionsUI_background_tabs.js]
+[browser_protectionsUI_categories.js]
+[browser_protectionsUI_cookies_subview.js]
+[browser_protectionsUI_cryptominers.js]
+[browser_protectionsUI_fetch.js]
+support-files =
+ file_protectionsUI_fetch.html
+ file_protectionsUI_fetch.js
+ file_protectionsUI_fetch.js^headers^
+[browser_protectionsUI_fingerprinters.js]
+[browser_protectionsUI_milestones.js]
+[browser_protectionsUI_open_preferences.js]
+[browser_protectionsUI_pbmode_exceptions.js]
+[browser_protectionsUI_report_breakage.js]
+skip-if = debug || asan # Bug 1546797
+[browser_protectionsUI_socialtracking.js]
+[browser_protectionsUI_shield_visibility.js]
+support-files =
+ sandboxed.html
+ sandboxed.html^headers^
+[browser_protectionsUI_state.js]
+[browser_protectionsUI_state_reset.js]
+[browser_protectionsUI_telemetry.js]
+[browser_protectionsUI_trackers_subview.js]
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..b708491e54
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI.js
@@ -0,0 +1,740 @@
+/* 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 =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ContentBlockingAllowList",
+ "resource://gre/modules/ContentBlockingAllowList.jsm"
+);
+
+ChromeUtils.import(
+ "resource://testing-common/CustomizableUITestUtils.jsm",
+ this
+);
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set the auto hide timing to 100ms for blocking the test less.
+ ["browser.protections_panel.toast.timeout", 100],
+ // Hide protections cards so as not to trigger more async messaging
+ // when landing on the page.
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["privacy.trackingprotection.enabled", true],
+ ],
+ });
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.telemetry.clearEvents();
+
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.clearEvents();
+ });
+});
+
+add_task(async function testToggleSwitch() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TRACKING_PAGE
+ );
+
+ await openProtectionsPanel();
+
+ await TestUtils.waitForCondition(() => {
+ return gProtectionsHandler._protectionsPopup.hasAttribute("blocking");
+ });
+
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ let buttonEvents = events.filter(
+ e =>
+ e[1] == "security.ui.protectionspopup" &&
+ e[2] == "open" &&
+ e[3] == "protections_popup"
+ );
+ is(buttonEvents.length, 1, "recorded telemetry for opening the popup");
+
+ // Check the visibility of the "Site not working?" link.
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be visible."
+ );
+
+ // The 'Site Fixed?' link should be hidden.
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be hidden."
+ );
+
+ // Navigate through the 'Site Not Working?' flow and back to the main view,
+ // checking for telemetry on the way.
+ let siteNotWorkingView = document.getElementById(
+ "protections-popup-siteNotWorkingView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ siteNotWorkingView,
+ "ViewShown"
+ );
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink.click();
+ await viewShown;
+
+ checkClickTelemetry("sitenotworking_link");
+
+ let sendReportButton = document.getElementById(
+ "protections-popup-siteNotWorkingView-sendReport"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ sendReportButton.click();
+ await viewShown;
+
+ checkClickTelemetry("send_report_link");
+
+ viewShown = BrowserTestUtils.waitForEvent(siteNotWorkingView, "ViewShown");
+ sendReportView.querySelector(".subviewbutton-back").click();
+ await viewShown;
+
+ let mainView = document.getElementById("protections-popup-mainView");
+
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ siteNotWorkingView.querySelector(".subviewbutton-back").click();
+ await viewShown;
+
+ ok(
+ gProtectionsHandler._protectionsPopupTPSwitch.hasAttribute("enabled"),
+ "TP Switch should be enabled"
+ );
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ // The 'Site not working?' link should be hidden after clicking the TP switch.
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be hidden after TP switch turns to off."
+ );
+ // Same for the 'Site Fixed?' link
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be hidden."
+ );
+
+ await popuphiddenPromise;
+ checkClickTelemetry("etp_toggle_off");
+
+ // We need to wait toast's popup shown and popup hidden events. It won't fire
+ // the popup shown event if we open the protections panel while the toast is
+ // opening.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+
+ await browserLoadedPromise;
+
+ // Wait until the toast is shown and hidden.
+ await popupShownPromise;
+ await popuphiddenPromise;
+
+ await openProtectionsPanel();
+ ok(
+ !gProtectionsHandler._protectionsPopupTPSwitch.hasAttribute("enabled"),
+ "TP Switch should be disabled"
+ );
+
+ // The 'Site not working?' link should be hidden if the TP is off.
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ "The 'Site not working?' link should be hidden if TP is off."
+ );
+
+ // The 'Site Fixed?' link should be shown if TP is off.
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be visible."
+ );
+
+ // Check telemetry for 'Site Fixed?' link.
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink.click();
+ await viewShown;
+
+ checkClickTelemetry("sitenotworking_link", "sitefixed");
+
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ sendReportView.querySelector(".subviewbutton-back").click();
+ await viewShown;
+
+ // Click the TP switch again and check the visibility of the 'Site not
+ // Working?'. It should be hidden after toggling the TP switch.
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageLink
+ ),
+ `The 'Site not working?' link should be still hidden after toggling TP
+ switch to on from off.`
+ );
+ ok(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._protectionsPopupTPSwitchBreakageFixedLink
+ ),
+ "The 'Site Fixed?' link should be hidden."
+ );
+
+ await browserLoadedPromise;
+ checkClickTelemetry("etp_toggle_on");
+
+ ContentBlockingAllowList.remove(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for the protection settings button.
+ */
+add_task(async function testSettingsButton() {
+ // Open a tab and its protection panel.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+ gProtectionsHandler._protectionsPopupSettingsButton.click();
+
+ // The protection popup should be hidden after clicking settings button.
+ await popuphiddenPromise;
+ // Wait until the about:preferences has been opened correctly.
+ let newTab = await newTabPromise;
+
+ ok(true, "about:preferences has been opened successfully");
+ checkClickTelemetry("settings");
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring Tracking Protection label is shown correctly
+ */
+add_task(async function testTrackingProtectionLabel() {
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let trackingProtectionLabel = document.getElementById(
+ "protections-popup-footer-protection-type-label"
+ );
+
+ is(
+ trackingProtectionLabel.textContent,
+ "Custom",
+ "The label is correctly set to Custom."
+ );
+ await closeProtectionsPanel();
+
+ Services.prefs.setStringPref("browser.contentblocking.category", "standard");
+ await openProtectionsPanel();
+
+ is(
+ trackingProtectionLabel.textContent,
+ "Standard",
+ "The label is correctly set to Standard."
+ );
+ await closeProtectionsPanel();
+
+ Services.prefs.setStringPref("browser.contentblocking.category", "strict");
+ await openProtectionsPanel();
+ is(
+ trackingProtectionLabel.textContent,
+ "Strict",
+ "The label is correctly set to Strict."
+ );
+
+ await closeProtectionsPanel();
+ Services.prefs.setStringPref("browser.contentblocking.category", "custom");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for the 'Show Full Report' button in the footer section.
+ */
+add_task(async function testShowFullReportButton() {
+ // Open a tab and its protection panel.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let newTabPromise = waitForAboutProtectionsTab();
+ let showFullReportButton = document.getElementById(
+ "protections-popup-show-report-button"
+ );
+
+ showFullReportButton.click();
+
+ // The protection popup should be hidden after clicking the link.
+ await popuphiddenPromise;
+ // Wait until the 'about:protections' has been opened correctly.
+ let newTab = await newTabPromise;
+
+ ok(true, "about:protections has been opened successfully");
+
+ checkClickTelemetry("full_report");
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring the mini panel is working correctly
+ */
+add_task(async function testMiniPanel() {
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ // Open the mini panel.
+ await openProtectionsPanel(true);
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+
+ // Check that only the header is displayed.
+ let mainView = document.getElementById("protections-popup-mainView");
+ for (let item of mainView.childNodes) {
+ if (item.id !== "protections-popup-mainView-panel-header-section") {
+ ok(
+ !BrowserTestUtils.is_visible(item),
+ `The section '${item.id}' is hidden in the toast.`
+ );
+ } else {
+ ok(
+ BrowserTestUtils.is_visible(item),
+ "The panel header is displayed as the content of the toast."
+ );
+ }
+ }
+
+ // Wait until the auto hide is happening.
+ await popuphiddenPromise;
+
+ ok(true, "The mini panel hides automatically.");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for the toggle switch flow
+ */
+add_task(async function testToggleSwitchFlow() {
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Click the TP switch, from On -> Off.
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ // Check that the icon state has been changed.
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "The tracking protection icon state has been changed to disabled."
+ );
+
+ // The panel should be closed and the mini panel will show up after refresh.
+ await popuphiddenPromise;
+ await browserLoadedPromise;
+ await popupShownPromise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+ "The protections popup should have the 'toast' attribute."
+ );
+
+ // Click on the mini panel and making sure the protection popup shows up.
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ document.getElementById("protections-popup-mainView-panel-header").click();
+ await popuphiddenPromise;
+ await popupShownPromise;
+
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+ "The 'toast' attribute should be cleared on the protections popup."
+ );
+
+ // Click the TP switch again, from Off -> On.
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popupshown"
+ );
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+ // Check that the icon state has been changed.
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "The tracking protection icon state has been changed to enabled."
+ );
+
+ // Protections popup hidden -> Page refresh -> Mini panel shows up.
+ await popuphiddenPromise;
+ popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ await browserLoadedPromise;
+ await popupShownPromise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+ "The protections popup should have the 'toast' attribute."
+ );
+
+ // Wait until the auto hide is happening.
+ await popuphiddenPromise;
+
+ // Clean up the TP state.
+ ContentBlockingAllowList.remove(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring the tracking protection icon will show a correct
+ * icon according to the TP enabling state.
+ */
+add_task(async function testTrackingProtectionIcon() {
+ // Open a tab and its protection panel.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ let TPIcon = document.getElementById("tracking-protection-icon");
+ // Check the icon url. It will show a shield icon if TP is enabled.
+ is(
+ gBrowser.ownerGlobal
+ .getComputedStyle(TPIcon)
+ .getPropertyValue("list-style-image"),
+ `url("chrome://browser/skin/tracking-protection.svg")`,
+ "The tracking protection icon shows a shield icon."
+ );
+
+ // Disable the tracking protection.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ "https://example.com/"
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await browserLoadedPromise;
+
+ // Check that the tracking protection icon should show a strike-through shield
+ // icon after page is reloaded.
+ is(
+ gBrowser.ownerGlobal
+ .getComputedStyle(TPIcon)
+ .getPropertyValue("list-style-image"),
+ `url("chrome://browser/skin/tracking-protection-disabled.svg")`,
+ "The tracking protection icon shows a strike through shield icon."
+ );
+
+ // Clean up the TP state.
+ ContentBlockingAllowList.remove(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for ensuring the number of blocked trackers is displayed properly.
+ */
+add_task(async function testNumberOfBlockedTrackers() {
+ // First, clear the tracking database.
+ await TrackingDBService.clearAll();
+
+ // Open a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ await openProtectionsPanel();
+
+ let trackerCounterBox = document.getElementById(
+ "protections-popup-trackers-blocked-counter-box"
+ );
+ let trackerCounterDesc = document.getElementById(
+ "protections-popup-trackers-blocked-counter-description"
+ );
+
+ // Check that whether the counter is not shown if the number of blocked
+ // trackers is zero.
+ ok(
+ BrowserTestUtils.is_hidden(trackerCounterBox),
+ "The blocked tracker counter is hidden if there is no blocked tracker."
+ );
+
+ await closeProtectionsPanel();
+
+ // Add one tracker into the database and check that the tracker counter is
+ // properly shown.
+ await addTrackerDataIntoDB(1);
+
+ // A promise for waiting the `showing` attributes has been set to the counter
+ // box. This means the database access is finished.
+ let counterShownPromise = BrowserTestUtils.waitForAttribute(
+ "showing",
+ trackerCounterBox
+ );
+
+ await openProtectionsPanel();
+ await counterShownPromise;
+
+ // Check that the number of blocked trackers is shown.
+ ok(
+ BrowserTestUtils.is_visible(trackerCounterBox),
+ "The blocked tracker counter is shown if there is one blocked tracker."
+ );
+ is(
+ trackerCounterDesc.textContent,
+ "1 Blocked",
+ "The blocked tracker counter is correct."
+ );
+
+ await closeProtectionsPanel();
+ await TrackingDBService.clearAll();
+
+ // Add trackers into the database and check that the tracker counter is
+ // properly shown as well as whether the pre-fetch is triggered by the
+ // keyboard navigation.
+ await addTrackerDataIntoDB(10);
+
+ // We cannot wait for the change of "showing" attribute here since this
+ // attribute will only be set if the previous counter is zero. Instead, we
+ // wait for the change of the text content of the counter.
+ let updateCounterPromise = new Promise(resolve => {
+ let mut = new MutationObserver(mutations => {
+ resolve();
+ mut.disconnect();
+ });
+
+ mut.observe(trackerCounterDesc, {
+ childList: true,
+ });
+ });
+
+ await openProtectionsPanelWithKeyNav();
+ await updateCounterPromise;
+
+ // Check that the number of blocked trackers is shown.
+ ok(
+ BrowserTestUtils.is_visible(trackerCounterBox),
+ "The blocked tracker counter is shown if there are more than one blocked tracker."
+ );
+ is(
+ trackerCounterDesc.textContent,
+ "10 Blocked",
+ "The blocked tracker counter is correct."
+ );
+
+ await closeProtectionsPanel();
+ await TrackingDBService.clearAll();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSubViewTelemetry() {
+ let items = [
+ ["protections-popup-category-tracking-protection", "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) {
+ 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-footer > button").click();
+ let prefsTab = await prefsTabPromise;
+ BrowserTestUtils.removeTab(prefsTab);
+ checkClickTelemetry("subview_settings", telemetryId);
+ });
+ }
+});
+
+/**
+ * A test to make sure the TP state won't apply incorrectly if we quickly switch
+ * tab after toggling the TP switch.
+ */
+add_task(async function testQuickSwitchTabAfterTogglingTPSwitch() {
+ const FIRST_TEST_SITE = "https://example.com/";
+ const SECOND_TEST_SITE = "https://example.org/";
+
+ // First, clear the tracking database.
+ await TrackingDBService.clearAll();
+
+ // Open two tabs with different origins.
+ let tabOne = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FIRST_TEST_SITE
+ );
+ let tabTwo = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SECOND_TEST_SITE
+ );
+
+ // Open the protection panel of the second tab.
+ await openProtectionsPanel();
+
+ // A promise to check the reload happens on the second tab.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tabTwo.linkedBrowser,
+ false,
+ SECOND_TEST_SITE
+ );
+
+ // Toggle the TP state and switch tab without waiting it to be finished.
+ gProtectionsHandler._protectionsPopupTPSwitch.click();
+ gBrowser.selectedTab = tabOne;
+
+ // Wait for the second tab to be reloaded.
+ await browserLoadedPromise;
+
+ // Check that the first tab is still with ETP enabled.
+ ok(
+ !ContentBlockingAllowList.includes(gBrowser.selectedBrowser),
+ "The ETP state of the first tab is still enabled."
+ );
+
+ // Check the ETP is disabled on the second origin.
+ ok(
+ ContentBlockingAllowList.includes(tabTwo.linkedBrowser),
+ "The ETP state of the second tab has been changed to disabled."
+ );
+
+ // Clean up the state of the allow list for the second tab.
+ ContentBlockingAllowList.remove(tabTwo.linkedBrowser);
+
+ BrowserTestUtils.removeTab(tabOne);
+ BrowserTestUtils.removeTab(tabTwo);
+
+ // Finally, clear the tracking database.
+ await TrackingDBService.clearAll();
+});
+
+// Test that the "Privacy Protections" button in the app menu loads about:protections
+// and has appropriate telemetry
+add_task(async function testProtectionsButton() {
+ let gCUITestUtils = new CustomizableUITestUtils(window);
+
+ await BrowserTestUtils.withNewTab(gBrowser, async function(browser) {
+ await gCUITestUtils.openMainMenu();
+
+ let loaded = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:protections",
+ "Should open about:protections"
+ );
+ document.getElementById("appMenu-protection-report-button").click();
+ await loaded;
+
+ // When the graph is built it means any messaging has finished,
+ // we can close the tab.
+ await SpecialPowers.spawn(browser, [], async function() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let bars = content.document.querySelectorAll(".graph-bar");
+ return bars.length;
+ }, "The graph has been built");
+ });
+ });
+ checkClickTelemetry("open_full_report", undefined, "app_menu");
+});
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..7621a0c6be
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_3.js
@@ -0,0 +1,54 @@
+/*
+ * 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
+ * See also Bug 1178985.
+ */
+
+const PREF = "privacy.trackingprotection.enabled";
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(PREF);
+ Services.prefs.clearUserPref(PB_PREF);
+});
+
+add_task(async function testNormalBrowsing() {
+ let TrackingProtection = gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ Services.prefs.setBoolPref(PREF, true);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=true)");
+
+ Services.prefs.setBoolPref(PREF, false);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled (ENABLED=false,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(!TrackingProtection.enabled, "TP is disabled (ENABLED=false,PB=true)");
+});
+
+add_task(async function testPrivateBrowsing() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let TrackingProtection = privateWin.gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ Services.prefs.setBoolPref(PREF, true);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=true)");
+
+ Services.prefs.setBoolPref(PREF, false);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled (ENABLED=false,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=false,PB=true)");
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_animation.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_animation.js
new file mode 100644
index 0000000000..2d11bd5b25
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_animation.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const BENIGN_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const TP_PREF = "privacy.trackingprotection.enabled";
+const PREFER_REDUCED_MOTION_PREF = "ui.prefersReducedMotion";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+
+// Test that the shield icon animation can be controlled by the cosmetic
+// animations pref and that one of the icons is visible in each case.
+add_task(async function testShieldAnimation() {
+ await UrlClassifierTestUtils.addTestTrackers();
+ Services.prefs.setBoolPref(TP_PREF, true);
+ Services.prefs.setBoolPref(DTSCBN_PREF, true);
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ let animationIcon = document.getElementById(
+ "tracking-protection-icon-animatable-image"
+ );
+ let noAnimationIcon = document.getElementById("tracking-protection-icon");
+
+ Services.prefs.setIntPref(PREFER_REDUCED_MOTION_PREF, 0);
+ await Promise.all([
+ promiseTabLoadEvent(tab, TRACKING_PAGE),
+ waitForContentBlockingEvent(2, tab.linkedBrowser.ownerGlobal),
+ ]);
+ ok(
+ BrowserTestUtils.is_hidden(noAnimationIcon),
+ "the default icon is hidden when animations are enabled"
+ );
+ ok(
+ BrowserTestUtils.is_visible(animationIcon),
+ "the animated icon is shown when animations are enabled"
+ );
+
+ await promiseTabLoadEvent(tab, BENIGN_PAGE);
+ ok(BrowserTestUtils.is_hidden(animationIcon), "the animated icon is hidden");
+ ok(
+ BrowserTestUtils.is_visible(noAnimationIcon),
+ "the default icon is visible"
+ );
+
+ Services.prefs.setIntPref(PREFER_REDUCED_MOTION_PREF, 1);
+ await Promise.all([
+ promiseTabLoadEvent(tab, TRACKING_PAGE),
+ waitForContentBlockingEvent(2, tab.linkedBrowser.ownerGlobal),
+ ]);
+
+ // We will only show the last frame of the animation when the animation is
+ // disable. So, the animated icon would be shown but the default icon
+ // wouldn't.
+ ok(
+ BrowserTestUtils.is_hidden(noAnimationIcon),
+ "the default icon is hidden when animations are disabled"
+ );
+ ok(
+ BrowserTestUtils.is_visible(animationIcon),
+ "the animated icon is shown when animations are disabled"
+ );
+
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref(PREFER_REDUCED_MOTION_PREF);
+ Services.prefs.clearUserPref(TP_PREF);
+ Services.prefs.clearUserPref(DTSCBN_PREF);
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_animation_2.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_animation_2.js
new file mode 100644
index 0000000000..517655fcc9
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_animation_2.js
@@ -0,0 +1,264 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/*
+ * Test that the Content Blocking icon is properly animated 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 NCB_PREF = "network.cookie.cookieBehavior";
+const BENIGN_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const TRACKING_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const COOKIE_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+
+requestLongerTimeout(2);
+
+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);
+});
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 0]],
+ });
+});
+
+async function testTrackingProtectionAnimation(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");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ info("Load a test page containing tracking elements");
+ let trackingTab = await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ TRACKING_PAGE
+ );
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+ ok(gProtectionsHandler.iconBox.hasAttribute("animate"), "iconBox animating");
+ await BrowserTestUtils.waitForEvent(
+ gProtectionsHandler.animatedIcon,
+ "animationend"
+ );
+
+ info("Load a test page containing tracking cookies");
+ let trackingCookiesTab = await BrowserTestUtils.openNewForegroundTab(
+ tabbrowser,
+ COOKIE_PAGE
+ );
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+ ok(gProtectionsHandler.iconBox.hasAttribute("animate"), "iconBox animating");
+ await BrowserTestUtils.waitForEvent(
+ gProtectionsHandler.animatedIcon,
+ "animationend"
+ );
+
+ 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");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ info("Switch from benign -> tracking tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ tabbrowser.selectedTab = trackingTab;
+ await securityChanged;
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ info("Switch from tracking -> tracking cookies tab");
+ securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
+ tabbrowser.selectedTab = trackingCookiesTab;
+ await securityChanged;
+
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "iconBox active");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ 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");
+ ok(gProtectionsHandler.iconBox.hasAttribute("animate"), "iconBox animating");
+ await BrowserTestUtils.waitForEvent(
+ gProtectionsHandler.animatedIcon,
+ "animationend"
+ );
+
+ 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");
+ ok(gProtectionsHandler.iconBox.hasAttribute("animate"), "iconBox animating");
+ await BrowserTestUtils.waitForEvent(
+ gProtectionsHandler.animatedIcon,
+ "animationend"
+ );
+
+ 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");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ 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");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ 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");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ 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");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("animate"),
+ "iconBox not animating"
+ );
+
+ while (tabbrowser.tabs.length > 1) {
+ tabbrowser.removeCurrentTab();
+ }
+}
+
+add_task(async function testNormalBrowsing() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let gProtectionsHandler = gBrowser.ownerGlobal.gProtectionsHandler;
+ ok(
+ gProtectionsHandler,
+ "gProtectionsHandler is attached to the browser window"
+ );
+ let TrackingProtection = gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+ let ThirdPartyCookies = gBrowser.ownerGlobal.ThirdPartyCookies;
+ 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 testTrackingProtectionAnimation(gBrowser);
+});
+
+add_task(async function testPrivateBrowsing() {
+ 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.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+ let ThirdPartyCookies = tabbrowser.ownerGlobal.ThirdPartyCookies;
+ 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 testTrackingProtectionAnimation(tabbrowser);
+
+ 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..055c75aae8
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_background_tabs.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const BENIGN_PAGE =
+ "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..d71ed2fadf
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_categories.js
@@ -0,0 +1,291 @@
+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";
+
+ChromeUtils.import(
+ "resource://testing-common/CustomizableUITestUtils.jsm",
+ this
+);
+
+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("http://www.example.com", async function() {
+ // Ensure the category nodes exist.
+ await openProtectionsPanel();
+ await closeProtectionsPanel();
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+ let categoryLabel = document.getElementById(
+ "protections-popup-cookies-category-label"
+ );
+
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ await TestUtils.waitForCondition(
+ () => !categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(!categoryItem.classList.contains("blocked"));
+
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_REJECT);
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingAll2.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString("contentBlocking.cookies.blockingAll2.label")
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blocking3rdParty2.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blocking3rdParty2.label"
+ )
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ )
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+ await TestUtils.waitForCondition(
+ () => categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(categoryItem.classList.contains("blocked"));
+ await TestUtils.waitForCondition(
+ () =>
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ ),
+ "The category label has updated correctly"
+ );
+ ok(
+ categoryLabel.textContent ==
+ gNavigatorBundle.getString(
+ "contentBlocking.cookies.blockingTrackers3.label"
+ )
+ );
+
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN
+ );
+ await TestUtils.waitForCondition(
+ () => !categoryItem.classList.contains("blocked"),
+ "The category label has updated correctly"
+ );
+ ok(!categoryItem.classList.contains("blocked"));
+ });
+});
+
+let categoryEnabledPrefs = [TP_PREF, STC_PREF, TPC_PREF, CM_PREF, FP_PREF];
+
+let detectedStateFlags = [
+ Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+ Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT,
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED,
+ Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT,
+ Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT,
+];
+
+async function waitForClass(item, className, shouldBePresent = true) {
+ await TestUtils.waitForCondition(() => {
+ return item.classList.contains(className) == shouldBePresent;
+ }, `Target class ${className} should be ${shouldBePresent ? "present" : "not present"} on item ${item.id}`);
+
+ ok(
+ item.classList.contains(className) == shouldBePresent,
+ `item.classList.contains(${className}) is ${shouldBePresent} for ${item.id}`
+ );
+}
+
+add_task(async function testCategorySections() {
+ Services.prefs.setBoolPref(ST_PREF, true);
+
+ for (let pref of categoryEnabledPrefs) {
+ if (pref == TPC_PREF) {
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ } else {
+ Services.prefs.setBoolPref(pref, false);
+ }
+ }
+
+ await BrowserTestUtils.withNewTab("http://www.example.com", async function() {
+ // Ensure the category nodes exist.
+ await openProtectionsPanel();
+ await closeProtectionsPanel();
+
+ let categoryItems = [
+ "protections-popup-category-tracking-protection",
+ "protections-popup-category-socialblock",
+ "protections-popup-category-cookies",
+ "protections-popup-category-cryptominers",
+ "protections-popup-category-fingerprinters",
+ ].map(id => document.getElementById(id));
+
+ for (let item of categoryItems) {
+ await waitForClass(item, "notFound");
+ await waitForClass(item, "blocked", false);
+ }
+
+ // For every item, we enable the category and spoof a content blocking event,
+ // and check that .notFound goes away and .blocked is set. Then we disable the
+ // category and checks that .blocked goes away, and .notFound is still unset.
+ let contentBlockingState = 0;
+ for (let i = 0; i < categoryItems.length; i++) {
+ let itemToTest = categoryItems[i];
+ let enabledPref = categoryEnabledPrefs[i];
+ contentBlockingState |= detectedStateFlags[i];
+ if (enabledPref == TPC_PREF) {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT
+ );
+ } else {
+ Services.prefs.setBoolPref(enabledPref, true);
+ }
+ gProtectionsHandler.onContentBlockingEvent(contentBlockingState);
+ gProtectionsHandler.updatePanelForBlockingEvent(contentBlockingState);
+ await waitForClass(itemToTest, "notFound", false);
+ await waitForClass(itemToTest, "blocked", true);
+ if (enabledPref == TPC_PREF) {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+ } else {
+ Services.prefs.setBoolPref(enabledPref, false);
+ }
+ await waitForClass(itemToTest, "notFound", false);
+ await waitForClass(itemToTest, "blocked", false);
+ }
+ });
+});
+
+/**
+ * Check that when we open the popup in a new window, the initial state is correct
+ * wrt the pref.
+ */
+add_task(async function testCategorySectionInitial() {
+ let categoryItems = [
+ "protections-popup-category-tracking-protection",
+ "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_cookies_subview.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js
new file mode 100644
index 0000000000..9f9d49fbfd
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js
@@ -0,0 +1,511 @@
+/* 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 =
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+const CONTAINER_PAGE =
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/containerPage.html";
+
+const TPC_PREF = "network.cookie.cookieBehavior";
+
+add_task(async function setup() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+/*
+ * Accepts an array containing 6 elements that identify the testcase:
+ * [0] - boolean indicating whether trackers are blocked.
+ * [1] - boolean indicating whether third party cookies are blocked.
+ * [2] - boolean indicating whether first party cookies are blocked.
+ * [3] - integer indicating number of expected content blocking events.
+ * [4] - integer indicating number of expected subview list headers.
+ * [5] - integer indicating number of expected cookie list items.
+ * [6] - integer indicating number of expected cookie list items
+ * after loading a cookie-setting third party URL in an iframe
+ * [7] - integer indicating number of expected cookie list items
+ * after loading a cookie-setting first party URL in an iframe
+ */
+async function assertSitesListed(testCase) {
+ let sitesListedTestCases = [
+ [true, false, false, 4, 1, 1, 1, 1],
+ [true, true, false, 5, 1, 1, 2, 2],
+ [true, true, true, 6, 2, 2, 3, 3],
+ [false, false, false, 3, 1, 1, 1, 1],
+ ];
+ let [
+ trackersBlocked,
+ thirdPartyBlocked,
+ firstPartyBlocked,
+ contentBlockingEventCount,
+ listHeaderCount,
+ cookieItemsCount1,
+ cookieItemsCount2,
+ cookieItemsCount3,
+ ] = sitesListedTestCases[testCase];
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([
+ promise,
+ waitForContentBlockingEvent(contentBlockingEventCount),
+ ]);
+ let browser = tab.linkedBrowser;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listHeaders = cookiesView.querySelectorAll(
+ ".protections-popup-cookiesView-list-header"
+ );
+ is(
+ listHeaders.length,
+ listHeaderCount,
+ `We have ${listHeaderCount} list headers.`
+ );
+ if (listHeaderCount == 1) {
+ ok(
+ !BrowserTestUtils.is_visible(listHeaders[0]),
+ "Only one header, should be hidden"
+ );
+ } else {
+ for (let header of listHeaders) {
+ ok(
+ BrowserTestUtils.is_visible(header),
+ "Multiple list headers - all should be visible."
+ );
+ }
+ }
+
+ let emptyLabels = cookiesView.querySelectorAll(
+ ".protections-popup-empty-label"
+ );
+ is(emptyLabels.length, 0, `We have no empty labels`);
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(
+ listItems.length,
+ cookieItemsCount1,
+ `We have ${cookieItemsCount1} cookies in the list`
+ );
+
+ if (trackersBlocked) {
+ let trackerTestItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ if (label.value == "http://trackertest.org") {
+ trackerTestItem = item;
+ break;
+ }
+ }
+ ok(trackerTestItem, "Has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(trackerTestItem), "List item is visible");
+ }
+
+ if (firstPartyBlocked) {
+ let notTrackingExampleItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ if (label.value == "http://not-tracking.example.com") {
+ notTrackingExampleItem = item;
+ break;
+ }
+ }
+ ok(notTrackingExampleItem, "Has an item for not-tracking.example.com");
+ ok(
+ BrowserTestUtils.is_visible(notTrackingExampleItem),
+ "List item is visible"
+ );
+ }
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = cookiesView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ let change = waitForContentBlockingEvent();
+ let timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
+
+ await SpecialPowers.spawn(browser, [], function() {
+ content.postMessage("third-party-cookie", "*");
+ });
+
+ let result = await Promise.race([change, timeoutPromise]);
+ is(result, undefined, "No contentBlockingEvent events should be received");
+
+ viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ emptyLabels = cookiesView.querySelectorAll(".protections-popup-empty-label");
+ is(emptyLabels.length, 0, `We have no empty labels`);
+
+ listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(
+ listItems.length,
+ cookieItemsCount2,
+ `We have ${cookieItemsCount2} cookies in the list`
+ );
+
+ if (thirdPartyBlocked) {
+ let test1ExampleItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ if (label.value == "https://test1.example.org") {
+ test1ExampleItem = item;
+ break;
+ }
+ }
+ ok(test1ExampleItem, "Has an item for test1.example.org");
+ ok(BrowserTestUtils.is_visible(test1ExampleItem), "List item is visible");
+ }
+
+ if (trackersBlocked || thirdPartyBlocked || firstPartyBlocked) {
+ let trackerTestItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ if (label.value == "http://trackertest.org") {
+ trackerTestItem = item;
+ break;
+ }
+ }
+ ok(trackerTestItem, "List item should exist for http://trackertest.org");
+ ok(BrowserTestUtils.is_visible(trackerTestItem), "List item is visible");
+ }
+
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ change = waitForSecurityChange();
+ timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
+
+ await SpecialPowers.spawn(browser, [], function() {
+ content.postMessage("first-party-cookie", "*");
+ });
+
+ result = await Promise.race([change, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ emptyLabels = cookiesView.querySelectorAll(".protections-popup-empty-label");
+ is(emptyLabels.length, 0, "We have no empty labels");
+
+ listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(
+ listItems.length,
+ cookieItemsCount3,
+ `We have ${cookieItemsCount3} cookies in the list`
+ );
+
+ if (firstPartyBlocked) {
+ let notTrackingExampleItem;
+ for (let item of listItems) {
+ let label = item.querySelector(".protections-popup-list-host-label");
+ if (label.value == "http://not-tracking.example.com") {
+ notTrackingExampleItem = item;
+ break;
+ }
+ }
+ ok(notTrackingExampleItem, "Has an item for not-tracking.example.com");
+ ok(
+ BrowserTestUtils.is_visible(notTrackingExampleItem),
+ "List item is visible"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testCookiesSubView() {
+ info("Testing cookies subview with reject tracking cookies.");
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ let testCaseIndex = 0;
+ await assertSitesListed(testCaseIndex++);
+ info("Testing cookies subview with reject third party cookies.");
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+ await assertSitesListed(testCaseIndex++);
+ info("Testing cookies subview with reject all cookies.");
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_REJECT);
+ await assertSitesListed(testCaseIndex++);
+ info("Testing cookies subview with accept all cookies.");
+ Services.prefs.setIntPref(TPC_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ await assertSitesListed(testCaseIndex++);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testCookiesSubViewAllowed() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://trackertest.org/"
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "cookie",
+ Services.perms.ALLOW_ACTION
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(3)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 cookie in the list");
+
+ let listItem = listItems[0];
+ let label = listItem.querySelector(".protections-popup-list-host-label");
+ is(label.value, "http://trackertest.org", "has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "list item is visible");
+ ok(
+ listItem.classList.contains("allowed"),
+ "indicates whether the cookie was blocked or allowed"
+ );
+
+ let button = listItem.querySelector(
+ ".identity-popup-permission-remove-button"
+ );
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Permission remove button is visible"
+ );
+ button.click();
+ is(
+ Services.perms.testExactPermissionFromPrincipal(principal, "cookie"),
+ Services.perms.UNKNOWN_ACTION,
+ "Button click should remove cookie pref."
+ );
+ ok(!listItem.classList.contains("allowed"), "Has removed the allowed class");
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testCookiesSubViewAllowedHeuristic() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://not-tracking.example.com/"
+ );
+
+ // Pretend that the tracker has already been interacted with
+ let trackerPrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://trackertest.org/"
+ );
+ Services.perms.addFromPrincipal(
+ trackerPrincipal,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(5)]);
+ let browser = tab.linkedBrowser;
+
+ let popup;
+ let windowCreated = TestUtils.topicObserved(
+ "chrome-document-global-created",
+ (subject, data) => {
+ popup = subject;
+ return true;
+ }
+ );
+ let permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => {
+ return (
+ subject &&
+ subject.QueryInterface(Ci.nsIPermission).type ==
+ "3rdPartyStorage^http://trackertest.org" &&
+ subject.principal.origin == principal.origin &&
+ data == "added"
+ );
+ });
+
+ await SpecialPowers.spawn(browser, [], function() {
+ content.postMessage("window-open", "*");
+ });
+ await Promise.all([windowCreated, permChanged]);
+
+ await new Promise(resolve => waitForFocus(resolve, popup));
+ await new Promise(resolve => waitForFocus(resolve, window));
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 cookie in the list");
+
+ let listItem = listItems[0];
+ let label = listItem.querySelector(".protections-popup-list-host-label");
+ is(label.value, "http://trackertest.org", "has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "list item is visible");
+ ok(
+ listItem.classList.contains("allowed"),
+ "indicates whether the cookie was blocked or allowed"
+ );
+
+ let button = listItem.querySelector(
+ ".identity-popup-permission-remove-button"
+ );
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Permission remove button is visible"
+ );
+ button.click();
+ is(
+ Services.perms.testExactPermissionFromPrincipal(
+ principal,
+ "3rdPartyStorage^http://trackertest.org"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Button click should remove the storage pref."
+ );
+ ok(!listItem.classList.contains("allowed"), "Has removed the allowed class");
+
+ await SpecialPowers.spawn(browser, [], function() {
+ content.postMessage("window-close", "*");
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testCookiesSubViewBlockedDoublyNested() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: CONTAINER_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(3)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let listItems = cookiesView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 cookie in the list");
+
+ let listItem = listItems[0];
+ let label = listItem.querySelector(".protections-popup-list-host-label");
+ is(label.value, "http://trackertest.org", "has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "list item is visible");
+ ok(
+ !listItem.classList.contains("allowed"),
+ "indicates whether the cookie was blocked or allowed"
+ );
+
+ let button = listItem.querySelector(
+ ".identity-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..0ab558a050
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cryptominers.js
@@ -0,0 +1,298 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ "http://example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const CM_PROTECTION_PREF = "privacy.trackingprotection.cryptomining.enabled";
+let cmHistogram;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "urlclassifier.features.cryptomining.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ [
+ "urlclassifier.features.cryptomining.annotate.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", false],
+ ["privacy.trackingprotection.fingerprinting.enabled", false],
+ ["urlclassifier.features.fingerprinting.annotate.blacklistHosts", ""],
+ ],
+ });
+ cmHistogram = Services.telemetry.getHistogramById(
+ "CRYPTOMINERS_BLOCKED_COUNT"
+ );
+ registerCleanupFunction(() => {
+ cmHistogram.clear();
+ });
+});
+
+async function testIdentityState(hasException) {
+ cmHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ await openProtectionsPanel();
+
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "cryptominers are not detected"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible regardless the exception"
+ );
+
+ promise = waitForContentBlockingEvent();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("cryptomining", "*");
+ });
+
+ await promise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ hasException,
+ "Shows an exception when appropriate"
+ );
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ let loads = hasException ? 3 : 1;
+ testTelemetry(loads, 1, hasException);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testSubview(hasException) {
+ cmHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("cryptomining", "*");
+ });
+ await promise;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cryptominers"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ // We have to wait until the ContentBlockingLog gets updated in the content.
+ // Unfortunately, we need to use the setTimeout here since we don't have an
+ // easy to know whether the log is updated in the content. This should be
+ // removed after the log been removed in the content (Bug 1599046).
+ await new Promise(resolve => {
+ setTimeout(resolve, 500);
+ });
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let subview = document.getElementById("protections-popup-cryptominersView");
+ let viewShown = BrowserTestUtils.waitForEvent(subview, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ let listItems = subview.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 item in the list");
+ let listItem = listItems[0];
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.querySelector("label").value,
+ "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);
+});
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..56db385ee0
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js
@@ -0,0 +1,38 @@
+const URL =
+ "http://mochi.test:8888/browser/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html";
+
+add_task(async function test_fetch() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.trackingprotection.enabled", true]],
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser, url: URL }, async function(
+ newTabBrowser
+ ) {
+ let contentBlockingEvent = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(newTabBrowser, [], async function() {
+ await content.wrappedJSObject
+ .test_fetch()
+ .then(response => Assert.ok(false, "should have denied the request"))
+ .catch(e => Assert.ok(true, `Caught exception: ${e}`));
+ });
+ await contentBlockingEvent;
+
+ let gProtectionsHandler = newTabBrowser.ownerGlobal.gProtectionsHandler;
+ ok(gProtectionsHandler, "got CB object");
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "has detected content blocking"
+ );
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("active"),
+ "icon box is active"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ gNavigatorBundle.getString("trackingProtection.icon.activeTooltip2"),
+ "correct tooltip"
+ );
+ });
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js
new file mode 100644
index 0000000000..accf84ccd5
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_fingerprinters.js
@@ -0,0 +1,297 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ "http://example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const FP_PROTECTION_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+let fpHistogram;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "urlclassifier.features.fingerprinting.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ [
+ "urlclassifier.features.fingerprinting.annotate.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", false],
+ ["privacy.trackingprotection.cryptomining.enabled", false],
+ ["urlclassifier.features.cryptomining.annotate.blacklistHosts", ""],
+ ["urlclassifier.features.cryptomining.annotate.blacklistTables", ""],
+ ],
+ });
+ fpHistogram = Services.telemetry.getHistogramById(
+ "FINGERPRINTERS_BLOCKED_COUNT"
+ );
+ registerCleanupFunction(() => {
+ fpHistogram.clear();
+ });
+});
+
+async function testIdentityState(hasException) {
+ fpHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ await openProtectionsPanel();
+
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "fingerprinters are not detected"
+ );
+ ok(
+ !BrowserTestUtils.is_hidden(gProtectionsHandler.iconBox),
+ "icon box is visible regardless the exception"
+ );
+
+ promise = waitForContentBlockingEvent();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("fingerprinting", "*");
+ });
+
+ await promise;
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ hasException,
+ "Shows an exception when appropriate"
+ );
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ let loads = hasException ? 3 : 1;
+ testTelemetry(loads, 1, hasException);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testCategoryItem() {
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, false);
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ await openProtectionsPanel();
+ let categoryItem = document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, true);
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, false);
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ await closeProtectionsPanel();
+
+ promise = waitForContentBlockingEvent();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("fingerprinting", "*");
+ });
+
+ await promise;
+
+ await openProtectionsPanel();
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, true);
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, false);
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ await closeProtectionsPanel();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testSubview(hasException) {
+ fpHistogram.clear();
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("fingerprinting", "*");
+ });
+ await promise;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-fingerprinters"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ // We have to wait until the ContentBlockingLog gets updated in the content.
+ // Unfortunately, we need to use the setTimeout here since we don't have an
+ // easy to know whether the log is updated in the content. This should be
+ // removed after the log been removed in the content (Bug 1599046).
+ await new Promise(resolve => {
+ setTimeout(resolve, 500);
+ });
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let subview = document.getElementById("protections-popup-fingerprintersView");
+ let viewShown = BrowserTestUtils.waitForEvent(subview, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ let listItems = subview.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 item in the list");
+ let listItem = listItems[0];
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.querySelector("label").value,
+ "https://fingerprinting.example.com",
+ "Has the correct host"
+ );
+ is(
+ listItem.classList.contains("allowed"),
+ hasException,
+ "Indicates the fingerprinter was blocked or allowed"
+ );
+
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = subview.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ let loads = hasException ? 3 : 1;
+ testTelemetry(loads, 1, hasException);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+function testTelemetry(pagesVisited, pagesWithBlockableContent, hasException) {
+ let results = fpHistogram.snapshot();
+ Assert.equal(
+ results.values[0],
+ pagesVisited,
+ "The correct number of page loads have been recorded"
+ );
+ let expectedValue = hasException ? 2 : 1;
+ Assert.equal(
+ results.values[expectedValue],
+ pagesWithBlockableContent,
+ "The correct number of fingerprinters have been recorded as blocked or allowed."
+ );
+}
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(FP_PROTECTION_PREF, true);
+
+ await testIdentityState(false);
+ await testIdentityState(true);
+
+ await testSubview(false);
+ await testSubview(true);
+
+ await testCategoryItem();
+
+ Services.prefs.clearUserPref(FP_PROTECTION_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js
new file mode 100644
index 0000000000..0748e9752d
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_milestones.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Hide protections cards so as not to trigger more async messaging
+ // when landing on the page.
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["browser.contentblocking.cfr-milestone.update-interval", 0],
+ ],
+ });
+});
+
+add_task(async function doTest() {
+ // This also ensures that the DB tables have been initialized.
+ await TrackingDBService.clearAll();
+
+ let milestones = JSON.parse(
+ Services.prefs.getStringPref(
+ "browser.contentblocking.cfr-milestone.milestones"
+ )
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ for (let milestone of milestones) {
+ Services.telemetry.clearEvents();
+ // Trigger the milestone feature.
+ Services.prefs.setIntPref(
+ "browser.contentblocking.cfr-milestone.milestone-achieved",
+ milestone
+ );
+ await TestUtils.waitForCondition(
+ () => gProtectionsHandler._milestoneTextSet
+ );
+ // We set the shown-time pref to pretend that the CFR has been
+ // shown, so that we can test the panel.
+ // TODO: Full integration test for robustness.
+ Services.prefs.setStringPref(
+ "browser.contentblocking.cfr-milestone.milestone-shown-time",
+ Date.now().toString()
+ );
+ await openProtectionsPanel();
+
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupMilestonesText
+ ),
+ "Milestones section should be visible in the panel."
+ );
+
+ await closeProtectionsPanel();
+ await openProtectionsPanel();
+
+ ok(
+ BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupMilestonesText
+ ),
+ "Milestones section should still be visible in the panel."
+ );
+
+ let newTabPromise = waitForAboutProtectionsTab();
+ await EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("protections-popup-milestones-content"),
+ {}
+ );
+ let protectionsTab = await newTabPromise;
+
+ ok(true, "about:protections has been opened as expected.");
+
+ BrowserTestUtils.removeTab(protectionsTab);
+
+ await openProtectionsPanel();
+
+ ok(
+ !BrowserTestUtils.is_visible(
+ gProtectionsHandler._protectionsPopupMilestonesText
+ ),
+ "Milestones section should no longer be visible in the panel."
+ );
+
+ checkClickTelemetry("milestone_message");
+
+ await closeProtectionsPanel();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await TrackingDBService.clearAll();
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js
new file mode 100644
index 0000000000..992d5538b9
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_open_preferences.js
@@ -0,0 +1,153 @@
+/* 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 =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const COOKIE_PAGE =
+ "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_task(async function setup() {
+ 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-tracking-protection"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let trackersView = document.getElementById("protections-popup-trackersView");
+ let viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Trackers view was shown");
+
+ let preferencesButton = document.getElementById(
+ "protections-popup-trackersView-settings-button"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(preferencesButton),
+ "The preferences button is shown."
+ );
+
+ let shown = waitAndAssertPreferencesShown("trackingprotection");
+ preferencesButton.click();
+ await shown;
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+// Tests that pressing the preferences button in the cookies subview
+// links to about:preferences
+add_task(async function testOpenPreferencesFromCookiesSubview() {
+ Services.prefs.setIntPref(
+ TPC_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: COOKIE_PAGE,
+ gBrowser,
+ });
+
+ // Wait for 2 content blocking events - one for the load and one for the tracker.
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent(2)]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-cookies"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let cookiesView = document.getElementById("protections-popup-cookiesView");
+ let viewShown = BrowserTestUtils.waitForEvent(cookiesView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Cookies view was shown");
+
+ let preferencesButton = document.getElementById(
+ "protections-popup-cookiesView-settings-button"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(preferencesButton),
+ "The preferences button is shown."
+ );
+
+ let shown = waitAndAssertPreferencesShown("trackingprotection");
+ preferencesButton.click();
+ await shown;
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js
new file mode 100644
index 0000000000..e3191a0f80
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_pbmode_exceptions.js
@@ -0,0 +1,163 @@
+/* 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 =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+var TrackingProtection = null;
+var gProtectionsHandler = null;
+var browser = null;
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(TP_PB_PREF);
+ Services.prefs.clearUserPref(DTSCBN_PREF);
+ gProtectionsHandler = TrackingProtection = browser = null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+function hidden(sel) {
+ let win = browser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ let display = win.getComputedStyle(el).getPropertyValue("display", null);
+ return display === "none";
+}
+
+function protectionsPopupState() {
+ let win = browser.ownerGlobal;
+ return win.document.getElementById("protections-popup")?.state || "closed";
+}
+
+function clickButton(sel) {
+ let win = browser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ el.doCommand();
+}
+
+function testTrackingPage() {
+ info("Tracking content must be blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(!gProtectionsHandler.hasException, "content shows no exception");
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ ok(gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "icon box shows no exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ gNavigatorBundle.getString("trackingProtection.icon.activeTooltip2"),
+ "correct tooltip"
+ );
+}
+
+function testTrackingPageUnblocked() {
+ info("Tracking content must be allowlisted and not blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(gProtectionsHandler.hasException, "content shows exception");
+
+ ok(!gProtectionsHandler.iconBox.hasAttribute("active"), "shield is active");
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "shield shows exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ gNavigatorBundle.getString("trackingProtection.icon.disabledTooltip2"),
+ "correct tooltip"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+}
+
+add_task(async function testExceptionAddition() {
+ await 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.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() {
+ 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.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..e325bab4ed
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_report_breakage.js
@@ -0,0 +1,400 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const BENIGN_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const COOKIE_PAGE =
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+
+const CM_PREF = "privacy.trackingprotection.cryptomining.enabled";
+const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+const TP_PREF = "privacy.trackingprotection.enabled";
+const CB_PREF = "network.cookie.cookieBehavior";
+
+const PREF_REPORT_BREAKAGE_URL = "browser.contentblocking.reportBreakage.url";
+
+let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+let { CommonUtils } = ChromeUtils.import("resource://services-common/utils.js");
+let { Preferences } = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm"
+);
+
+add_task(async function setup() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ // Clear prefs that are touched in this test again for sanity.
+ Services.prefs.clearUserPref(TP_PREF);
+ Services.prefs.clearUserPref(CB_PREF);
+ Services.prefs.clearUserPref(FP_PREF);
+ Services.prefs.clearUserPref(CM_PREF);
+ Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_URL);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "urlclassifier.features.fingerprinting.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ [
+ "urlclassifier.features.fingerprinting.annotate.blacklistHosts",
+ "fingerprinting.example.com",
+ ],
+ ["privacy.trackingprotection.cryptomining.enabled", true],
+ [
+ "urlclassifier.features.cryptomining.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ [
+ "urlclassifier.features.cryptomining.annotate.blacklistHosts",
+ "cryptomining.example.com",
+ ],
+ ],
+ });
+});
+
+add_task(async function testReportBreakageCancel() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function() {
+ await openProtectionsPanel();
+ await TestUtils.waitForCondition(() =>
+ gProtectionsHandler._protectionsPopup.hasAttribute("blocking")
+ );
+
+ let siteNotWorkingButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-link"
+ );
+ ok(
+ BrowserTestUtils.is_visible(siteNotWorkingButton),
+ "site not working button is visible"
+ );
+ let siteNotWorkingView = document.getElementById(
+ "protections-popup-siteNotWorkingView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ siteNotWorkingView,
+ "ViewShown"
+ );
+ siteNotWorkingButton.click();
+ await viewShown;
+
+ let sendReportButton = document.getElementById(
+ "protections-popup-siteNotWorkingView-sendReport"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ sendReportButton.click();
+ await viewShown;
+
+ ok(true, "Report breakage view was shown");
+
+ viewShown = BrowserTestUtils.waitForEvent(siteNotWorkingView, "ViewShown");
+ let cancelButton = document.getElementById(
+ "protections-popup-sendReportView-cancel"
+ );
+ cancelButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testReportBreakageSiteException() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+
+ await BrowserTestUtils.withNewTab(url, async browser => {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false);
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+
+ await openProtectionsPanel();
+
+ let siteFixedButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-fixed-link"
+ );
+ ok(
+ BrowserTestUtils.is_visible(siteFixedButton),
+ "site fixed button is visible"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ siteFixedButton.click();
+ await viewShown;
+
+ ok(true, "Report breakage view was shown");
+
+ await testReportBreakageSubmit(
+ TRACKING_PAGE,
+ "trackingprotection",
+ false,
+ true
+ );
+
+ // Pass false for shouldReload - there's no need since the tab is going away.
+ gProtectionsHandler.enableForCurrentPage(false);
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testNoTracking() {
+ await BrowserTestUtils.withNewTab(BENIGN_PAGE, async function() {
+ await openProtectionsPanel();
+
+ let siteNotWorkingButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-link"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(siteNotWorkingButton),
+ "site not working button is not visible"
+ );
+ });
+});
+
+add_task(async function testReportBreakageError() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function() {
+ await openAndTestReportBreakage(TRACKING_PAGE, "trackingprotection", true);
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testTP() {
+ Services.prefs.setBoolPref(TP_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function() {
+ await openAndTestReportBreakage(TRACKING_PAGE, "trackingprotection");
+ });
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
+
+add_task(async function testCR() {
+ Services.prefs.setIntPref(
+ CB_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ // Make sure that we correctly strip the query.
+ let url = COOKIE_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function() {
+ await openAndTestReportBreakage(COOKIE_PAGE, "cookierestrictions");
+ });
+
+ Services.prefs.clearUserPref(CB_PREF);
+});
+
+add_task(async function testFP() {
+ Services.prefs.setIntPref(CB_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ Services.prefs.setBoolPref(FP_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function(browser) {
+ let promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(browser, [], function() {
+ content.postMessage("fingerprinting", "*");
+ });
+ await promise;
+
+ await openAndTestReportBreakage(TRACKING_PAGE, "fingerprinting", true);
+ });
+
+ Services.prefs.clearUserPref(FP_PREF);
+ Services.prefs.clearUserPref(CB_PREF);
+});
+
+add_task(async function testCM() {
+ Services.prefs.setIntPref(CB_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT);
+ Services.prefs.setBoolPref(CM_PREF, true);
+ // Make sure that we correctly strip the query.
+ let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+ await BrowserTestUtils.withNewTab(url, async function(browser) {
+ let promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(browser, [], function() {
+ content.postMessage("cryptomining", "*");
+ });
+ await promise;
+
+ await openAndTestReportBreakage(TRACKING_PAGE, "cryptomining", true);
+ });
+
+ Services.prefs.clearUserPref(CM_PREF);
+ Services.prefs.clearUserPref(CB_PREF);
+});
+
+async function openAndTestReportBreakage(url, tags, error = false) {
+ await openProtectionsPanel();
+
+ let siteNotWorkingButton = document.getElementById(
+ "protections-popup-tp-switch-breakage-link"
+ );
+ ok(
+ BrowserTestUtils.is_visible(siteNotWorkingButton),
+ "site not working button is visible"
+ );
+ let siteNotWorkingView = document.getElementById(
+ "protections-popup-siteNotWorkingView"
+ );
+ let viewShown = BrowserTestUtils.waitForEvent(
+ siteNotWorkingView,
+ "ViewShown"
+ );
+ siteNotWorkingButton.click();
+ await viewShown;
+
+ let sendReportButton = document.getElementById(
+ "protections-popup-siteNotWorkingView-sendReport"
+ );
+ let sendReportView = document.getElementById(
+ "protections-popup-sendReportView"
+ );
+ viewShown = BrowserTestUtils.waitForEvent(sendReportView, "ViewShown");
+ sendReportButton.click();
+ await viewShown;
+
+ ok(true, "Report breakage view was shown");
+
+ await testReportBreakageSubmit(url, tags, error, false);
+}
+
+// This function assumes that the breakage report view is ready.
+async function testReportBreakageSubmit(url, tags, error, hasException) {
+ // Setup a mock server for receiving breakage reports.
+ let server = new HttpServer();
+ server.start(-1);
+ let i = server.identity;
+ let path =
+ i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort + "/";
+
+ Services.prefs.setStringPref(PREF_REPORT_BREAKAGE_URL, path);
+
+ let comments = document.getElementById(
+ "protections-popup-sendReportView-collection-comments"
+ );
+ is(comments.value, "", "Comments textarea should initially be empty");
+
+ let submitButton = document.getElementById(
+ "protections-popup-sendReportView-submit"
+ );
+ let reportURL = document.getElementById(
+ "protections-popup-sendReportView-collection-url"
+ ).value;
+
+ is(reportURL, url, "Shows the correct URL in the report UI.");
+
+ // Make sure that sending the report closes the identity popup.
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+
+ // Check that we're receiving a good report.
+ await new Promise(resolve => {
+ server.registerPathHandler("/", async (request, response) => {
+ is(request.method, "POST", "request was a post");
+
+ // Extract and "parse" the form data in the request body.
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let boundary = request
+ .getHeader("Content-Type")
+ .match(/boundary=-+([^-]*)/i)[1];
+ let regex = new RegExp("-+" + boundary + "-*\\s+");
+ let sections = body.split(regex);
+
+ let prefs = [
+ "privacy.trackingprotection.enabled",
+ "privacy.trackingprotection.pbmode.enabled",
+ "urlclassifier.trackingTable",
+ "network.http.referer.defaultPolicy",
+ "network.http.referer.defaultPolicy.pbmode",
+ "network.cookie.cookieBehavior",
+ "network.cookie.lifetimePolicy",
+ "privacy.annotate_channels.strict_list.enabled",
+ "privacy.restrict3rdpartystorage.expiration",
+ "privacy.trackingprotection.fingerprinting.enabled",
+ "privacy.trackingprotection.cryptomining.enabled",
+ ];
+ let prefsBody = "";
+
+ for (let pref of prefs) {
+ prefsBody += `${pref}: ${Preferences.get(pref)}\r\n`;
+ }
+
+ Assert.deepEqual(
+ sections,
+ [
+ "",
+ `Content-Disposition: form-data; name=\"title\"\r\n\r\n${
+ Services.io.newURI(reportURL).host
+ }\r\n`,
+ 'Content-Disposition: form-data; name="body"\r\n\r\n' +
+ `Full URL: ${reportURL + "?"}\r\n` +
+ `userAgent: ${navigator.userAgent}\r\n\r\n` +
+ "**Preferences**\r\n" +
+ `${prefsBody}\r\n` +
+ `hasException: ${hasException}\r\n\r\n` +
+ "**Comments**\r\n" +
+ "This is a comment\r\n",
+ 'Content-Disposition: form-data; name="labels"\r\n\r\n' +
+ `${hasException ? "" : tags}\r\n`,
+ "",
+ ],
+ "Should send the correct form data"
+ );
+
+ if (error) {
+ response.setStatusLine(request.httpVersion, 500, "Request failed");
+ } else {
+ response.setStatusLine(request.httpVersion, 201, "Entry created");
+ }
+
+ resolve();
+ });
+
+ comments.value = "This is a comment";
+ submitButton.click();
+ });
+
+ let errorMessage = document.getElementById(
+ "protections-popup-sendReportView-report-error"
+ );
+ if (error) {
+ await BrowserTestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(errorMessage)
+ );
+ is(
+ comments.value,
+ "This is a comment",
+ "Comment not cleared in case of an error"
+ );
+ gProtectionsHandler._protectionsPopup.hidePopup();
+ } else {
+ ok(BrowserTestUtils.is_hidden(errorMessage), "Error message not shown");
+ }
+
+ await popuphidden;
+
+ // Stop the server.
+ await new Promise(r => server.stop(r));
+
+ Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_URL);
+}
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js
new file mode 100644
index 0000000000..be3cc46118
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_shield_visibility.js
@@ -0,0 +1,123 @@
+/* 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",
+ 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 setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // By default, proxies don't apply to 127.0.0.1. We need them to for this test, though:
+ ["network.proxy.allow_hijacking_localhost", true],
+ ],
+ });
+
+ for (let testData of TEST_CASES) {
+ info(`Testing for ${testData.type}`);
+ let testURL = testData.testURL;
+
+ // Overwrite the url if it is testing the file url.
+ if (testData.type === "file") {
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append(testURL);
+ dir.normalize();
+ testURL = Services.io.newFileURI(dir).spec;
+ }
+
+ let pageLoaded;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, testURL);
+ let browser = gBrowser.selectedBrowser;
+ if (testData.type === "certificateError") {
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ } else {
+ pageLoaded = BrowserTestUtils.browserLoaded(browser, true);
+ }
+ },
+ false
+ );
+ await pageLoaded;
+
+ is(
+ BrowserTestUtils.is_hidden(
+ gProtectionsHandler._trackingProtectionIconContainer
+ ),
+ testData.hidden,
+ "tracking protection icon container is correctly displayed"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js
new file mode 100644
index 0000000000..65580ac0cb
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_socialtracking.js
@@ -0,0 +1,315 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE =
+ "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_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ST_BLOCK_COOKIES_PREF, true],
+ [
+ "urlclassifier.features.socialtracking.blacklistHosts",
+ "social-tracking.example.org",
+ ],
+ [
+ "urlclassifier.features.socialtracking.annotate.blacklistHosts",
+ "social-tracking.example.org",
+ ],
+ // Whitelist trackertest.org loaded by default in trackingPage.html
+ ["urlclassifier.trackingSkipURLs", "trackertest.org"],
+ ["urlclassifier.trackingAnnotationSkipURLs", "trackertest.org"],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+});
+
+async function testIdentityState(hasException) {
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ await openProtectionsPanel();
+ let categoryItem = document.getElementById(
+ "protections-popup-category-socialblock"
+ );
+
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "socialtrackings are not detected"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible regardless the exception"
+ );
+ await closeProtectionsPanel();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("socialtracking", "*");
+ });
+ await openProtectionsPanel();
+
+ await TestUtils.waitForCondition(() => {
+ return !categoryItem.classList.contains("notFound");
+ });
+
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are detected"
+ );
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "social trackers are detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ hasException,
+ "Shows an exception when appropriate"
+ );
+ await closeProtectionsPanel();
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testSubview(hasException) {
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ }
+
+ promise = waitForContentBlockingEvent();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("socialtracking", "*");
+ });
+ await promise;
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-socialblock"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "STP category item is visible");
+ ok(
+ categoryItem.classList.contains("blocked"),
+ "STP category item is blocked"
+ );
+
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ // We have to wait until the ContentBlockingLog gets updated in the content.
+ // Unfortunately, we need to use the setTimeout here since we don't have an
+ // easy to know whether the log is updated in the content. This should be
+ // removed after the log been removed in the content (Bug 1599046).
+ await new Promise(resolve => {
+ setTimeout(resolve, 500);
+ });
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let subview = document.getElementById("protections-popup-socialblockView");
+ let viewShown = BrowserTestUtils.waitForEvent(subview, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ let listItems = subview.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 item in the list");
+ let listItem = listItems[0];
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.querySelector("label").value,
+ "https://social-tracking.example.org",
+ "Has the correct host"
+ );
+
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = subview.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ if (hasException) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await loaded;
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testCategoryItem(blockLoads) {
+ if (blockLoads) {
+ Services.prefs.setBoolPref(ST_PROTECTION_PREF, true);
+ }
+
+ Services.prefs.setBoolPref(ST_BLOCK_COOKIES_PREF, false);
+
+ let promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ await openProtectionsPanel();
+
+ let categoryItem = document.getElementById(
+ "protections-popup-category-socialblock"
+ );
+
+ let noTrackersDetectedDesc = document.getElementById(
+ "protections-popup-no-trackers-found-description"
+ );
+
+ ok(categoryItem.hasAttribute("uidisabled"), "Category should be uidisabled");
+
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(!BrowserTestUtils.is_visible(categoryItem), "Item should be hidden");
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("socialtracking", "*");
+ });
+
+ ok(
+ !categoryItem.classList.contains("blocked"),
+ "Category not marked as blocked"
+ );
+ ok(!BrowserTestUtils.is_visible(categoryItem), "Item should be hidden");
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+ ok(
+ BrowserTestUtils.is_visible(noTrackersDetectedDesc),
+ "No Trackers detected should be shown"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.setBoolPref(ST_BLOCK_COOKIES_PREF, true);
+
+ promise = BrowserTestUtils.openNewForegroundTab({
+ url: TRACKING_PAGE,
+ gBrowser,
+ });
+ [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
+
+ await openProtectionsPanel();
+
+ ok(!categoryItem.hasAttribute("uidisabled"), "Item shouldn't be uidisabled");
+
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ categoryItem.classList.contains("notFound"),
+ "Category marked as not found"
+ );
+ // At this point we should still be showing "No Trackers Detected"
+ ok(!BrowserTestUtils.is_visible(categoryItem), "Item should not be visible");
+ ok(
+ BrowserTestUtils.is_visible(noTrackersDetectedDesc),
+ "No Trackers detected should be shown"
+ );
+ ok(
+ !gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("socialtracking", "*");
+ });
+
+ await TestUtils.waitForCondition(() => {
+ return !categoryItem.classList.contains("notFound");
+ });
+
+ ok(categoryItem.classList.contains("blocked"), "Category marked as blocked");
+ ok(
+ !categoryItem.classList.contains("notFound"),
+ "Category not marked as not found"
+ );
+ ok(BrowserTestUtils.is_visible(categoryItem), "Item should be visible");
+ ok(
+ !BrowserTestUtils.is_visible(noTrackersDetectedDesc),
+ "No Trackers detected should be hidden"
+ );
+ ok(
+ gProtectionsHandler._protectionsPopup.hasAttribute("detected"),
+ "trackers are not detected"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(ST_PROTECTION_PREF);
+}
+
+add_task(async function testIdentityUI() {
+ requestLongerTimeout(2);
+
+ await testIdentityState(false);
+ await testIdentityState(true);
+
+ await testSubview(false);
+ await testSubview(true);
+
+ await testCategoryItem(false);
+ await testCategoryItem(true);
+});
diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_state.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_state.js
new file mode 100644
index 0000000000..d6a96e9c9e
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_state.js
@@ -0,0 +1,381 @@
+/*
+ * 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 TPC_PREF = "network.cookie.cookieBehavior";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+const BENIGN_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const TRACKING_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const COOKIE_PAGE =
+ "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookiePage.html";
+var gProtectionsHandler = null;
+var TrackingProtection = null;
+var ThirdPartyCookies = null;
+var tabbrowser = null;
+var gTrackingPageURL = TRACKING_PAGE;
+
+const sBrandBundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+);
+const sNoTrackerIconTooltip = gNavigatorBundle.getFormattedString(
+ "trackingProtection.icon.noTrackersDetectedTooltip",
+ [sBrandBundle.GetStringFromName("brandShortName")]
+);
+const sActiveIconTooltip = gNavigatorBundle.getString(
+ "trackingProtection.icon.activeTooltip2"
+);
+const sDisabledIconTooltip = gNavigatorBundle.getString(
+ "trackingProtection.icon.disabledTooltip2"
+);
+
+registerCleanupFunction(function() {
+ TrackingProtection = gProtectionsHandler = ThirdPartyCookies = tabbrowser = null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.prefs.clearUserPref(TP_PREF);
+ Services.prefs.clearUserPref(TP_PB_PREF);
+ Services.prefs.clearUserPref(TPC_PREF);
+ Services.prefs.clearUserPref(DTSCBN_PREF);
+});
+
+function notFound(id) {
+ let doc = tabbrowser.ownerGlobal.document;
+ return doc.getElementById(id).classList.contains("notFound");
+}
+
+async function testBenignPage() {
+ info("Non-tracking content must not be blocked");
+ ok(!gProtectionsHandler.anyDetected, "no trackers are detected");
+ ok(!gProtectionsHandler.hasException, "content shows no exception");
+
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "icon box shows no exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ sNoTrackerIconTooltip,
+ "correct tooltip"
+ );
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+
+ let win = tabbrowser.ownerGlobal;
+ await openProtectionsPanel(false, win);
+ ok(
+ notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is not found"
+ );
+ ok(
+ notFound("protections-popup-category-tracking-protection"),
+ "Trackers category is not found"
+ );
+ await closeProtectionsPanel(win);
+}
+
+async function testBenignPageWithException() {
+ info("Non-tracking content must not be blocked");
+ ok(!gProtectionsHandler.anyDetected, "no trackers are detected");
+ ok(gProtectionsHandler.hasException, "content shows exception");
+
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "shield shows exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ sDisabledIconTooltip,
+ "correct tooltip"
+ );
+
+ ok(
+ !BrowserTestUtils.is_hidden(gProtectionsHandler.iconBox),
+ "icon box is not hidden"
+ );
+
+ let win = tabbrowser.ownerGlobal;
+ await openProtectionsPanel(false, win);
+ ok(
+ notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is not found"
+ );
+ ok(
+ notFound("protections-popup-category-tracking-protection"),
+ "Trackers category is not found"
+ );
+ await closeProtectionsPanel(win);
+}
+
+function areTrackersBlocked(isPrivateBrowsing) {
+ let blockedByTP = Services.prefs.getBoolPref(
+ isPrivateBrowsing ? TP_PB_PREF : TP_PREF
+ );
+ let blockedByTPC = [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ].includes(Services.prefs.getIntPref(TPC_PREF));
+ return blockedByTP || blockedByTPC;
+}
+
+async function testTrackingPage(window) {
+ info("Tracking content must be blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(!gProtectionsHandler.hasException, "content shows no exception");
+
+ let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+ let blockedByTP = areTrackersBlocked(isWindowPrivate);
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is always visible"
+ );
+ is(
+ gProtectionsHandler.iconBox.hasAttribute("active"),
+ blockedByTP,
+ "shield is" + (blockedByTP ? "" : " not") + " active"
+ );
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "icon box shows no exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ blockedByTP ? sActiveIconTooltip : sNoTrackerIconTooltip,
+ "correct tooltip"
+ );
+
+ await openProtectionsPanel(false, window);
+ ok(
+ !notFound("protections-popup-category-tracking-protection"),
+ "Trackers category is detected"
+ );
+ if (gTrackingPageURL == COOKIE_PAGE) {
+ ok(
+ !notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is detected"
+ );
+ } else {
+ ok(
+ notFound("protections-popup-category-cookies"),
+ "Cookie restrictions category is not found"
+ );
+ }
+ await closeProtectionsPanel(window);
+}
+
+async function testTrackingPageUnblocked(blockedByTP, window) {
+ info("Tracking content must be in the exception list and not blocked");
+ ok(gProtectionsHandler.anyDetected, "trackers are detected");
+ ok(gProtectionsHandler.hasException, "content shows exception");
+
+ ok(
+ !gProtectionsHandler.iconBox.hasAttribute("active"),
+ "shield is not active"
+ );
+ ok(
+ gProtectionsHandler.iconBox.hasAttribute("hasException"),
+ "shield shows exception"
+ );
+ is(
+ gProtectionsHandler._trackingProtectionIconTooltipLabel.textContent,
+ sDisabledIconTooltip,
+ "correct tooltip"
+ );
+
+ ok(
+ BrowserTestUtils.is_visible(gProtectionsHandler.iconBox),
+ "icon box is visible"
+ );
+
+ await openProtectionsPanel(false, window);
+ ok(
+ !notFound("protections-popup-category-tracking-protection"),
+ "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 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.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() {
+ 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.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+ is(
+ TrackingProtection.enabled,
+ Services.prefs.getBoolPref(TP_PB_PREF),
+ "TP.enabled is based on the pb pref value"
+ );
+
+ await testContentBlocking(tab);
+
+ Services.prefs.setBoolPref(TP_PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ await testContentBlocking(tab);
+
+ privateWin.close();
+
+ Services.prefs.clearUserPref(TPC_PREF);
+});
+
+add_task(async function testThirdPartyCookies() {
+ await 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.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..d110149adb
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_state_reset.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TRACKING_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+const BENIGN_PAGE =
+ "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_telemetry.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js
new file mode 100644
index 0000000000..e7e85d1fbf
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_telemetry.js
@@ -0,0 +1,86 @@
+/*
+ * Test telemetry for Tracking Protection
+ */
+
+const PREF = "privacy.trackingprotection.enabled";
+const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications";
+const BENIGN_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html";
+const TRACKING_PAGE =
+ "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_task(async function setup() {
+ await UrlClassifierTestUtils.addTestTrackers();
+ Services.prefs.setBoolPref(DTSCBN_PREF, true);
+
+ let TrackingProtection = gBrowser.ownerGlobal.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..a229b4a1ec
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_trackers_subview.js
@@ -0,0 +1,128 @@
+/* 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.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const TRACKING_PAGE =
+ "http://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+
+add_task(async function setup() {
+ 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-tracking-protection"
+ );
+
+ // Explicitly waiting for the category item becoming visible.
+ await TestUtils.waitForCondition(() => {
+ return BrowserTestUtils.is_visible(categoryItem);
+ });
+
+ ok(BrowserTestUtils.is_visible(categoryItem), "TP category item is visible");
+ let trackersView = document.getElementById("protections-popup-trackersView");
+ let viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Trackers view was shown");
+
+ let listItems = trackersView.querySelectorAll(".protections-popup-list-item");
+ is(listItems.length, 1, "We have 1 tracker in the list");
+
+ let mainView = document.getElementById("protections-popup-mainView");
+ viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ let backButton = trackersView.querySelector(".subviewbutton-back");
+ backButton.click();
+ await viewShown;
+
+ ok(true, "Main view was shown");
+
+ let change = waitForSecurityChange(1);
+ let timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.postMessage("more-tracking", "*");
+ });
+
+ let result = await Promise.race([change, timeoutPromise]);
+ is(result, undefined, "No securityChange events should be received");
+
+ viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
+ categoryItem.click();
+ await viewShown;
+
+ ok(true, "Trackers view was shown");
+
+ listItems = Array.from(
+ trackersView.querySelectorAll(".protections-popup-list-item")
+ );
+ is(listItems.length, 2, "We have 2 trackers in the list");
+
+ let listItem = listItems.find(
+ item => item.querySelector("label").value == "http://trackertest.org"
+ );
+ ok(listItem, "Has an item for trackertest.org");
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.classList.contains("allowed"),
+ !blocked,
+ "Indicates whether the tracker was blocked or allowed"
+ );
+
+ listItem = listItems.find(
+ item => item.querySelector("label").value == "https://itisatracker.org"
+ );
+ ok(listItem, "Has an item for itisatracker.org");
+ ok(BrowserTestUtils.is_visible(listItem), "List item is visible");
+ is(
+ listItem.classList.contains("allowed"),
+ !blocked,
+ "Indicates whether the tracker was blocked or allowed"
+ );
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testTrackersSubView() {
+ info("Testing trackers subview with TP disabled.");
+ Services.prefs.setBoolPref(TP_PREF, false);
+ await assertSitesListed(false);
+ info("Testing trackers subview with TP enabled.");
+ Services.prefs.setBoolPref(TP_PREF, true);
+ await assertSitesListed(true);
+ info("Testing trackers subview with TP enabled and a CB exception.");
+ let uri = Services.io.newURI("https://tracking.example.org");
+ PermissionTestUtils.add(
+ uri,
+ "trackingprotection",
+ Services.perms.ALLOW_ACTION
+ );
+ await assertSitesListed(false);
+ info("Testing trackers subview with TP enabled and a CB exception removed.");
+ PermissionTestUtils.remove(uri, "trackingprotection");
+ await assertSitesListed(true);
+
+ Services.prefs.clearUserPref(TP_PREF);
+});
diff --git a/browser/base/content/test/protectionsUI/containerPage.html b/browser/base/content/test/protectionsUI/containerPage.html
new file mode 100644
index 0000000000..f68f7325c1
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/containerPage.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <iframe src="http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/embeddedPage.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/cookiePage.html b/browser/base/content/test/protectionsUI/cookiePage.html
new file mode 100644
index 0000000000..e7ef2aafa1
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/cookiePage.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <script src="trackingAPI.js" type="text/javascript"></script>
+ </head>
+ <body>
+ <iframe src="http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/cookieServer.sjs b/browser/base/content/test/protectionsUI/cookieServer.sjs
new file mode 100644
index 0000000000..91e30de0c1
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/cookieServer.sjs
@@ -0,0 +1,20 @@
+/* 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/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..ee5f998046
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.html
@@ -0,0 +1,16 @@
+<!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() {
+ let url = "http://trackertest.org/browser/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js";
+ return fetch(url);
+ }
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js
new file mode 100644
index 0000000000..f7ac687cfc
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js
@@ -0,0 +1,2 @@
+/* Some code goes here! */
+void 0;
diff --git a/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/file_protectionsUI_fetch.js^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/browser/base/content/test/protectionsUI/head.js b/browser/base/content/test/protectionsUI/head.js
new file mode 100644
index 0000000000..ef6b805510
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/head.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { Sqlite } = ChromeUtils.import("resource://gre/modules/Sqlite.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+XPCOMUtils.defineLazyGetter(this, "TRACK_DB_PATH", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "protections.sqlite");
+});
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ContentBlockingAllowList",
+ "resource://gre/modules/ContentBlockingAllowList.jsm"
+);
+
+var { UrlClassifierTestUtils } = ChromeUtils.import(
+ "resource://testing-common/UrlClassifierTestUtils.jsm"
+);
+
+async function openProtectionsPanel(toast, win = window) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ win,
+ "popupshown",
+ true,
+ e => e.target.id == "protections-popup"
+ );
+ let shieldIconContainer = win.document.getElementById(
+ "tracking-protection-icon-container"
+ );
+
+ // Move out than move over the shield icon to trigger the hover event in
+ // order to fetch tracker count.
+ EventUtils.synthesizeMouseAtCenter(
+ win.gURLBar.textbox,
+ {
+ type: "mousemove",
+ },
+ win
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ shieldIconContainer,
+ {
+ type: "mousemove",
+ },
+ win
+ );
+
+ if (!toast) {
+ EventUtils.synthesizeMouseAtCenter(shieldIconContainer, {}, win);
+ } else {
+ win.gProtectionsHandler.showProtectionsPopup({ toast });
+ }
+
+ await popupShownPromise;
+}
+
+async function openProtectionsPanelWithKeyNav() {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ e => e.target.id == "protections-popup"
+ );
+
+ gURLBar.focus();
+
+ // This will trigger the focus event for the shield icon for pre-fetching
+ // the tracker count.
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_Enter", {});
+
+ await popupShownPromise;
+}
+
+async function closeProtectionsPanel(win = window) {
+ let protectionsPopup = win.document.getElementById("protections-popup");
+ if (!protectionsPopup) {
+ return;
+ }
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(
+ protectionsPopup,
+ "popuphidden"
+ );
+
+ PanelMultiView.hidePopup(protectionsPopup);
+ await popuphiddenPromise;
+}
+
+function checkClickTelemetry(objectName, value, source = "protectionspopup") {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ let buttonEvents = events.filter(
+ e =>
+ e[1] == `security.ui.${source}` &&
+ e[2] == "click" &&
+ e[3] == objectName &&
+ e[4] === value
+ );
+ is(buttonEvents.length, 1, `recorded ${objectName} telemetry event`);
+}
+
+async function addTrackerDataIntoDB(count) {
+ const insertSQL =
+ "INSERT INTO events (type, count, timestamp)" +
+ "VALUES (:type, :count, date(:timestamp));";
+
+ let db = await Sqlite.openConnection({ path: TRACK_DB_PATH });
+ let date = new Date().toISOString();
+
+ await db.execute(insertSQL, {
+ type: TrackingDBService.TRACKERS_ID,
+ count,
+ timestamp: date,
+ });
+
+ await db.close();
+}
+
+async function waitForAboutProtectionsTab() {
+ let tab = await BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:protections",
+ true
+ );
+
+ // When the graph is built it means the messaging has finished,
+ // we can close the tab.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let bars = content.document.querySelectorAll(".graph-bar");
+ return bars.length;
+ }, "The graph has been built");
+ });
+
+ return tab;
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url) {
+ BrowserTestUtils.loadURI(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..cc15eacf56
--- /dev/null
+++ b/browser/base/content/test/protectionsUI/trackingAPI.js
@@ -0,0 +1,70 @@
+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":
+ createIframe("http://cryptomining.example.com/");
+ break;
+ case "fingerprinting":
+ createIframe("https://fingerprinting.example.com/");
+ break;
+ case "more-tracking":
+ createIframe("https://itisatracker.org/");
+ 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(
+ "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(
+ "http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs?type=image-no-cookie"
+ );
+ break;
+ case "window-open":
+ window.win = window.open(
+ "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/.eslintrc.js b/browser/base/content/test/referrer/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/referrer/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/referrer/browser.ini b/browser/base/content/test/referrer/browser.ini
new file mode 100644
index 0000000000..23636a8c72
--- /dev/null
+++ b/browser/base/content/test/referrer/browser.ini
@@ -0,0 +1,25 @@
+[DEFAULT]
+support-files =
+ file_referrer_policyserver.sjs
+ file_referrer_policyserver_attr.sjs
+ file_referrer_testserver.sjs
+ head.js
+
+[browser_referrer_middle_click.js]
+[browser_referrer_middle_click_in_container.js]
+[browser_referrer_open_link_in_private.js]
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_open_link_in_tab.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_window.js]
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_open_link_in_window_in_container.js]
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_simple_click.js]
+[browser_referrer_click_pinned_tab.js]
+[browser_referrer_open_link_in_container_tab.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_container_tab2.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_container_tab3.js]
+skip-if = os == 'linux' # Bug 1144816
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..c13d3b6cff
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_click_pinned_tab.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = [
+ {
+ fromScheme: "http://",
+ toScheme: "http://",
+ cross: true,
+ result: "http://test1.example.com/browser", // full referrer
+ },
+ {
+ fromScheme: "https://",
+ toScheme: "http://",
+ cross: true,
+ result: "", // no referrer when downgrade
+ },
+ {
+ fromScheme: "https://",
+ toScheme: "http://",
+ policy: "origin",
+ cross: true,
+ result: "https://test1.example.com/", // origin, even on downgrade
+ },
+ {
+ fromScheme: "https://",
+ 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
+ },
+ {
+ 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..f4d3cbc7ca
--- /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..69a9f6a393
--- /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..c8768d0d90
--- /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 }
+ );
+
+ menupopup.openPopup();
+ });
+}
+
+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..853e532739
--- /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..63235a9fcc
--- /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 }
+ );
+
+ menupopup.openPopup();
+ });
+}
+
+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..347263a7af
--- /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..da3215c5dd
--- /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..9bdd50d809
--- /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..3852ad9983
--- /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..b35f3bb160
--- /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..963e0bb77a
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver.sjs
@@ -0,0 +1,39 @@
+/**
+ * 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)
+{
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let scheme = query.get("scheme");
+ let policy = query.get("policy");
+ let rel = query.get("rel");
+ let cross = query.get("cross");
+
+ let host = cross ? "example.com" : "test1.example.com";
+ let linkUrl = scheme + host +
+ "/browser/browser/base/content/test/referrer/" +
+ "file_referrer_testserver.sjs";
+ let metaReferrerTag =
+ policy ? `<meta name='referrer' content='${policy}'>` : "";
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ ${metaReferrerTag}
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <a id='testlink' href='${linkUrl}' ${rel ? ` rel='${rel}'` : ""}>
+ referrer test link</a>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs b/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs
new file mode 100644
index 0000000000..e5591f8fb3
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs
@@ -0,0 +1,39 @@
+/**
+ * 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)
+{
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let scheme = query.get("scheme");
+ let policy = query.get("policy");
+ let rel = query.get("rel");
+ let cross = query.get("cross");
+
+ let host = cross ? "example.com" : "test1.example.com";
+ let linkUrl = scheme + host +
+ "/browser/browser/base/content/test/referrer/" +
+ "file_referrer_testserver.sjs";
+
+ let referrerPolicy =
+ policy ? `referrerpolicy="${policy}"` : "";
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <a id='testlink' href='${linkUrl}' ${referrerPolicy} ${rel ? ` rel='${rel}'` : ""}>
+ referrer test link</a>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_testserver.sjs b/browser/base/content/test/referrer/file_referrer_testserver.sjs
new file mode 100644
index 0000000000..0cfc53b2c3
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_testserver.sjs
@@ -0,0 +1,31 @@
+/**
+ * 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..1f3bf8ea07
--- /dev/null
+++ b/browser/base/content/test/referrer/head.js
@@ -0,0 +1,319 @@
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "BrowserTestUtils",
+ "resource://testing-common/BrowserTestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ContentTask",
+ "resource://testing-common/ContentTask.jsm"
+);
+
+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.
+ {
+ fromScheme: "http://",
+ toScheme: "http://",
+ result: "http://test1.example.com/browser", // full referrer
+ },
+ {
+ fromScheme: "https://",
+ 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://",
+ toScheme: "http://",
+ policy: "origin",
+ result: "https://test1.example.com/", // origin, even on downgrade
+ },
+ {
+ fromScheme: "https://",
+ 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
+ },
+ {
+ 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/.eslintrc.js b/browser/base/content/test/sanitize/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/sanitize/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/sanitize/browser.ini b/browser/base/content/test/sanitize/browser.ini
new file mode 100644
index 0000000000..235b53c121
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+support-files=
+ head.js
+ dummy.js
+ dummy_page.html
+prefs=
+ browser.cache.offline.enable=true
+ browser.cache.offline.storage.enable=true
+
+[browser_purgehistory_clears_sh.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_cookiePermission.js]
+[browser_cookiePermission_aboutURL.js]
+[browser_cookiePermission_containers.js]
+[browser_cookiePermission_subDomains.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..cd971c6b23
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js
@@ -0,0 +1,106 @@
+const { Sanitizer } = ChromeUtils.import("resource:///modules/Sanitizer.jsm");
+const { SiteDataTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SiteDataTestUtils.jsm"
+);
+
+function checkDataForAboutURL() {
+ return new Promise(resolve => {
+ let data = true;
+ let uri = Services.io.newURI("about:newtab");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
+ request.onupgradeneeded = function(e) {
+ data = false;
+ };
+ request.onsuccess = function(e) {
+ resolve(data);
+ };
+ });
+}
+
+add_task(async function deleteStorageInAboutURL() {
+ info("Test about:newtab");
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_SESSION],
+ ["browser.sanitizer.loglevel", "All"],
+ ],
+ });
+
+ // Let's create a tab with some data.
+ await SiteDataTestUtils.addToIndexedDB("about:newtab", "foo", "bar", {});
+
+ ok(await checkDataForAboutURL(), "We have data for about:newtab");
+
+ // Cleaning up.
+ await Sanitizer.runSanitizeOnShutdown();
+
+ ok(await checkDataForAboutURL(), "about:newtab data is not deleted.");
+
+ // Clean up.
+ await Sanitizer.sanitize(["cookies", "offlineApps"]);
+
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:newtab"
+ );
+ await new Promise(aResolve => {
+ let req = Services.qms.clearStoragesForPrincipal(principal);
+ req.callback = () => {
+ aResolve();
+ };
+ });
+});
+
+add_task(async function deleteStorageOnlyCustomPermissionInAboutURL() {
+ info("Test about:newtab + permissions");
+
+ // Let's clean up all the data.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_NORMALLY],
+ ["browser.sanitizer.loglevel", "All"],
+ ],
+ });
+
+ // Custom permission without considering OriginAttributes
+ let uri = Services.io.newURI("about:newtab");
+ PermissionTestUtils.add(uri, "cookie", Ci.nsICookiePermission.ACCESS_SESSION);
+
+ // Let's create a tab with some data.
+ await SiteDataTestUtils.addToIndexedDB("about:newtab", "foo", "bar", {});
+
+ ok(await checkDataForAboutURL(), "We have data for about:newtab");
+
+ // Cleaning up.
+ await Sanitizer.runSanitizeOnShutdown();
+
+ ok(await checkDataForAboutURL(), "about:newtab data is not deleted.");
+
+ // Clean up.
+ await Sanitizer.sanitize(["cookies", "offlineApps"]);
+
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:newtab"
+ );
+ await new Promise(aResolve => {
+ let req = Services.qms.clearStoragesForPrincipal(principal);
+ req.callback = () => {
+ aResolve();
+ };
+ });
+
+ PermissionTestUtils.remove(uri, "cookie");
+});
diff --git a/browser/base/content/test/sanitize/browser_cookiePermission_containers.js b/browser/base/content/test/sanitize/browser_cookiePermission_containers.js
new file mode 100644
index 0000000000..236c0913e8
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_containers.js
@@ -0,0 +1 @@
+runAllCookiePermissionTests({ name: "container", oa: { userContextId: 1 } });
diff --git a/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js
new file mode 100644
index 0000000000..e6b4eb5b2e
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_cookiePermission_subDomains.js
@@ -0,0 +1,226 @@
+const { Sanitizer } = ChromeUtils.import("resource:///modules/Sanitizer.jsm");
+const { SiteDataTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SiteDataTestUtils.jsm"
+);
+
+// 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);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_NORMALLY],
+ ["browser.sanitizer.loglevel", "All"],
+ ],
+ });
+
+ // Domains and data
+ let originA = "https://www.mozilla.org";
+ PermissionTestUtils.add(
+ originA,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ SiteDataTestUtils.addToCookies(originA);
+ await SiteDataTestUtils.addToIndexedDB(originA);
+
+ let originB = "https://mozilla.org";
+ PermissionTestUtils.add(
+ originB,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ SiteDataTestUtils.addToCookies(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);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_SESSION],
+ ["browser.sanitizer.loglevel", "All"],
+ ],
+ });
+
+ // Domains and data
+ let originA = "https://sub.mozilla.org";
+ PermissionTestUtils.add(
+ originA,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ SiteDataTestUtils.addToCookies(originA);
+ await SiteDataTestUtils.addToIndexedDB(originA);
+
+ let originB = "https://www.mozilla.org";
+
+ SiteDataTestUtils.addToCookies(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);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.lifetimePolicy", Ci.nsICookieService.ACCEPT_SESSION],
+ ["browser.sanitizer.loglevel", "All"],
+ ],
+ });
+
+ // Domains and data
+ let originA = "https://sub.mozilla.org";
+ PermissionTestUtils.add(
+ originA,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+ SiteDataTestUtils.addToCookies(originA);
+ await SiteDataTestUtils.addToIndexedDB(originA);
+
+ let originB = "https://mozilla.org";
+ SiteDataTestUtils.addToCookies(originB);
+ await SiteDataTestUtils.addToIndexedDB(originB);
+
+ let originC = "https://www.mozilla.org";
+ SiteDataTestUtils.addToCookies(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");
+});
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..d90aa4a180
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const url =
+ "http://example.org/browser/browser/base/content/test/sanitize/dummy_page.html";
+
+add_task(async function purgeHistoryTest() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function purgeHistoryTestInner(browser) {
+ let backButton = browser.ownerDocument.getElementById("Browser:Back");
+ let forwardButton = browser.ownerDocument.getElementById(
+ "Browser:Forward"
+ );
+
+ ok(
+ !browser.webNavigation.canGoBack,
+ "Initial value for webNavigation.canGoBack"
+ );
+ ok(
+ !browser.webNavigation.canGoForward,
+ "Initial value for webNavigation.canGoBack"
+ );
+ ok(backButton.hasAttribute("disabled"), "Back button is disabled");
+ ok(forwardButton.hasAttribute("disabled"), "Forward button is disabled");
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ let startHistory = content.history.length;
+ content.history.pushState({}, "");
+ content.history.pushState({}, "");
+ content.history.back();
+ await new Promise(function(r) {
+ content.onpopstate = r;
+ });
+ let newHistory = content.history.length;
+ Assert.equal(startHistory, 1, "Initial SHistory size");
+ Assert.equal(newHistory, 3, "New SHistory size");
+ });
+
+ ok(
+ browser.webNavigation.canGoBack,
+ "New value for webNavigation.canGoBack"
+ );
+ ok(
+ browser.webNavigation.canGoForward,
+ "New value for webNavigation.canGoForward"
+ );
+ ok(!backButton.hasAttribute("disabled"), "Back button was enabled");
+ ok(!forwardButton.hasAttribute("disabled"), "Forward button was enabled");
+
+ await Sanitizer.sanitize(["history"]);
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ Assert.equal(content.history.length, 1, "SHistory correctly cleared");
+ });
+
+ ok(
+ !browser.webNavigation.canGoBack,
+ "webNavigation.canGoBack correctly cleared"
+ );
+ ok(
+ !browser.webNavigation.canGoForward,
+ "webNavigation.canGoForward correctly cleared"
+ );
+ ok(backButton.hasAttribute("disabled"), "Back button was disabled");
+ ok(forwardButton.hasAttribute("disabled"), "Forward button was disabled");
+ }
+ );
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-formhistory.js b/browser/base/content/test/sanitize/browser_sanitize-formhistory.js
new file mode 100644
index 0000000000..d7768542de
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-formhistory.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() {
+ // 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 new Promise((resolve, reject) => {
+ FormHistory.update(
+ { op: "remove" },
+ {
+ handleError(error) {
+ reject(error);
+ },
+ handleCompletion(reason) {
+ if (!reason) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ }
+ );
+ });
+
+ // Sanitize now so we can test the baseline point.
+ await Sanitizer.sanitize(["formdata"]);
+ await gFindBarPromise;
+ ok(!gFindBar.hasTransactions, "pre-test baseline for sanitizer");
+
+ gFindBar.getElement("findbar-textbox").value = "m";
+ ok(gFindBar.hasTransactions, "formdata can be cleared after input");
+
+ await Sanitizer.sanitize(["formdata"]);
+ is(
+ gFindBar.getElement("findbar-textbox").value,
+ "",
+ "findBar textbox should be empty after sanitize"
+ );
+ ok(!gFindBar.hasTransactions, "No transactions after sanitize");
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-history.js b/browser/base/content/test/sanitize/browser_sanitize-history.js
new file mode 100644
index 0000000000..5ec722af5b
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-history.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that sanitizing history will clear storage access permissions
+// for sites without cookies or site data.
+add_task(async function sanitizeStorageAccessPermissions() {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+
+ await SiteDataTestUtils.addToIndexedDB("https://sub.example.org");
+ await SiteDataTestUtils.addToCookies("https://example.com");
+
+ PermissionTestUtils.add(
+ "https://example.org",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://example.com",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ "http://mochi.test",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Add some time in between taking the snapshot of the timestamp
+ // to avoid flakyness.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 100));
+ let timestamp = Date.now();
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 100));
+
+ PermissionTestUtils.add(
+ "http://example.net",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await Sanitizer.sanitize(["history"], { range: [timestamp, Date.now()] });
+
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://example.net",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://mochi.test",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.com",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.org",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await Sanitizer.sanitize(["history"]);
+
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://mochi.test",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://example.net",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.com",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.org",
+ "storageAccessAPI"
+ ),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await Sanitizer.sanitize(["history", "siteSettings"]);
+
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "http://mochi.test",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.com",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.testExactPermission(
+ "https://example.org",
+ "storageAccessAPI"
+ ),
+ Services.perms.UNKNOWN_ACTION
+ );
+});
diff --git a/browser/base/content/test/sanitize/browser_sanitize-offlineData.js b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
new file mode 100644
index 0000000000..94082b0b19
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
@@ -0,0 +1,208 @@
+// Bug 380852 - Delete permission manager entries in Clear Recent History
+
+const { Sanitizer } = ChromeUtils.import("resource:///modules/Sanitizer.jsm");
+const { SiteDataTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SiteDataTestUtils.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "sas",
+ "@mozilla.org/storage/activity-service;1",
+ "nsIStorageActivityService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "swm",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager"
+);
+
+const oneHour = 3600000000;
+const fiveHours = oneHour * 5;
+
+const itemsToClear = ["cookies", "offlineApps"];
+
+function hasIndexedDB(origin) {
+ return new Promise(resolve => {
+ let hasData = true;
+ let uri = Services.io.newURI(origin);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
+ request.onupgradeneeded = function(e) {
+ hasData = false;
+ };
+ request.onsuccess = function(e) {
+ resolve(hasData);
+ };
+ });
+}
+
+function waitForUnregister(host) {
+ return new Promise(resolve => {
+ let listener = {
+ onUnregister: registration => {
+ if (registration.principal.host != host) {
+ return;
+ }
+ swm.removeListener(listener);
+ resolve(registration);
+ },
+ };
+ swm.addListener(listener);
+ });
+}
+
+async function createData(host) {
+ let origin = "https://" + host;
+ let dummySWURL =
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ "dummy.js";
+
+ await SiteDataTestUtils.addToIndexedDB(origin);
+ await SiteDataTestUtils.addServiceWorker(dummySWURL);
+}
+
+function moveOriginInTime(principals, endDate, host) {
+ for (let i = 0; i < principals.length; ++i) {
+ let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
+ if (principal.host == host) {
+ sas.moveOriginInTime(principal, endDate - fiveHours);
+ return true;
+ }
+ }
+ return false;
+}
+
+add_task(async function testWithRange() {
+ // We have intermittent occurrences of NS_ERROR_ABORT being
+ // thrown at closing database instances when using Santizer.sanitize().
+ // This does not seem to impact cleanup, since our tests run fine anyway.
+ PromiseTestUtils.allowMatchingRejectionsGlobally(/NS_ERROR_ABORT/);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ // The service may have picked up activity from prior tests in this run.
+ // Clear it.
+ sas.testOnlyReset();
+
+ let endDate = Date.now() * 1000;
+ let principals = sas.getActiveOrigins(endDate - oneHour, endDate);
+ is(principals.length, 0, "starting from clear activity state");
+
+ info("sanitize: " + itemsToClear.join(", "));
+ await Sanitizer.sanitize(itemsToClear, { ignoreTimespan: false });
+
+ await createData("example.org");
+ await createData("example.com");
+
+ endDate = Date.now() * 1000;
+ principals = sas.getActiveOrigins(endDate - oneHour, endDate);
+ ok(!!principals, "We have an active origin.");
+ ok(principals.length >= 2, "We have an active origin.");
+
+ let found = 0;
+ for (let i = 0; i < principals.length; ++i) {
+ let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
+ if (principal.host == "example.org" || principal.host == "example.com") {
+ found++;
+ }
+ }
+
+ is(found, 2, "Our origins are active.");
+
+ ok(
+ await 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 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 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 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 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 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 Sanitizer.sanitize(itemsToClear);
+});
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..5209c9f0e3
--- /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("http://example.com", false);
+ // Sanity check
+ is(
+ Services.logins.getLoginSavingEnabled("http://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("http://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..6190d3cf54
--- /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(
+ "http://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..77e2dd4efc
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitize-timespans.js
@@ -0,0 +1,1194 @@
+requestLongerTimeout(2);
+
+const { PlacesTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+// 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();
+});
+
+function countEntries(name, message, check) {
+ return new Promise((resolve, reject) => {
+ var obj = {};
+ if (name !== null) {
+ obj.fieldname = name;
+ }
+
+ let count;
+ FormHistory.count(obj, {
+ handleResult: result => (count = result),
+ handleError(error) {
+ reject(error);
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion(reason) {
+ if (!reason) {
+ check(count, message);
+ resolve();
+ }
+ },
+ });
+ });
+}
+
+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("http://10minutes.com")),
+ "Pretend visit to 10minutes.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://1hour.com"),
+ "Pretend visit to 1hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://1hour10minutes.com"),
+ "Pretend visit to 1hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://2hour.com"),
+ "Pretend visit to 2hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (minutesSinceMidnight > 10) {
+ ok(
+ await PlacesUtils.history.hasVisits("http://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("http://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("http://1hour.com")),
+ "Pretend visit to 1hour.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://1hour10minutes.com"),
+ "Pretend visit to 1hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://2hour.com"),
+ "Pretend visit to 2hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (hoursSinceMidnight > 1) {
+ ok(
+ await PlacesUtils.history.hasVisits("http://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("http://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("http://1hour10minutes.com")),
+ "Pretend visit to 1hour10minutes.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://2hour.com"),
+ "Pretend visit to 2hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (minutesSinceMidnight > 70) {
+ ok(
+ await PlacesUtils.history.hasVisits("http://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("http://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("http://2hour.com")),
+ "Pretend visit to 2hour.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://2hour10minutes.com"),
+ "Pretend visit to 2hour10minutes.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (hoursSinceMidnight > 2) {
+ ok(
+ await PlacesUtils.history.hasVisits("http://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("http://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("http://2hour10minutes.com")),
+ "Pretend visit to 2hour10minutes.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour.com"),
+ "Pretend visit to 4hour.com should should still exist"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (minutesSinceMidnight > 130) {
+ ok(
+ await PlacesUtils.history.hasVisits("http://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("http://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("http://4hour.com")),
+ "Pretend visit to 4hour.com should now be deleted"
+ );
+ ok(
+ await PlacesUtils.history.hasVisits("http://4hour10minutes.com"),
+ "Pretend visit to 4hour10minutes.com should should still exist"
+ );
+ if (hoursSinceMidnight > 4) {
+ ok(
+ await PlacesUtils.history.hasVisits("http://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("http://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("http://4hour10minutes.com")),
+ "Pretend visit to 4hour10minutes.com should now be deleted"
+ );
+ if (minutesSinceMidnight > 250) {
+ ok(
+ await PlacesUtils.history.hasVisits("http://today.com"),
+ "Pretend visit to today.com should still exist"
+ );
+ }
+ ok(
+ await PlacesUtils.history.hasVisits("http://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);
+ await Sanitizer.sanitize(null, { ignoreTimespan: false });
+
+ 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("http://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("http://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("http://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(
+ "http://10minutes.com/",
+ "10 minutes ago",
+ now_uSec - 10 * kUsecPerMin
+ );
+ addPlace(
+ "http://1hour.com/",
+ "Less than 1 hour ago",
+ now_uSec - 45 * kUsecPerMin
+ );
+ addPlace(
+ "http://1hour10minutes.com/",
+ "1 hour 10 minutes ago",
+ now_uSec - 70 * kUsecPerMin
+ );
+ addPlace(
+ "http://2hour.com/",
+ "Less than 2 hours ago",
+ now_uSec - 90 * kUsecPerMin
+ );
+ addPlace(
+ "http://2hour10minutes.com/",
+ "2 hours 10 minutes ago",
+ now_uSec - 130 * kUsecPerMin
+ );
+ addPlace(
+ "http://4hour.com/",
+ "Less than 4 hours ago",
+ now_uSec - 180 * kUsecPerMin
+ );
+ addPlace(
+ "http://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("http://today.com/", "Today", today.getTime() * 1000);
+
+ let lastYear = new Date();
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
+ addPlace(
+ "http://before-today.com/",
+ "Before Today",
+ lastYear.getTime() * 1000
+ );
+ await PlacesTestUtils.addVisits(places);
+}
+
+async function setupFormHistory() {
+ function searchEntries(terms, params) {
+ return new Promise((resolve, reject) => {
+ let results = [];
+ FormHistory.search(terms, params, {
+ handleResult: result => results.push(result),
+ handleError(error) {
+ reject(error);
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion(reason) {
+ resolve(results);
+ },
+ });
+ });
+ }
+
+ function update(changes) {
+ return new Promise((resolve, reject) => {
+ FormHistory.update(changes, {
+ handleError(error) {
+ reject(error);
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion(reason) {
+ resolve();
+ },
+ });
+ });
+ }
+
+ // Make sure we've got a clean DB to start with, then add the entries we'll be testing.
+ await 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 update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 45 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "1hour" });
+ await update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 70 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "1hour10minutes" });
+ await update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 90 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "2hour" });
+ await update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 130 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "2hour10minutes" });
+ await update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 180 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "4hour" });
+ await update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 250 * kUsecPerMin;
+ results = await searchEntries(["guid"], { fieldname: "4hour10minutes" });
+ await 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 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 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..6ce8c1abe1
--- /dev/null
+++ b/browser/base/content/test/sanitize/browser_sanitizeDialog.js
@@ -0,0 +1,997 @@
+/* -*- 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.defineModuleGetter(
+ this,
+ "Timer",
+ "resource://gre/modules/Timer.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+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_task(async function init() {
+ requestLongerTimeout(3);
+ await blankSlate();
+ registerCleanupFunction(async function() {
+ await blankSlate();
+ await PlacesTestUtils.promiseAsyncUpdates();
+ });
+});
+
+/**
+ * Initializes the dialog to its default state.
+ */
+add_task(async function default_state() {
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ // Select "Last Hour"
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.acceptDialog();
+ };
+ wh.open();
+ await wh.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("http://" + i + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) });
+ uris.push(pURI);
+ }
+ await PlacesTestUtils.addVisits(places);
+
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.checkPrefCheckbox("history", false);
+ this.cancelDialog();
+ };
+ wh.onunload = async function() {
+ await promiseHistoryClearedState(uris, false);
+ await blankSlate();
+ await promiseHistoryClearedState(uris, true);
+ };
+ wh.open();
+ await wh.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("http://" + 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("http://" + (61 + i) + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i) });
+ olderURIs.push(pURI);
+ }
+ let promiseSanitized = promiseSanitizationComplete();
+
+ await PlacesTestUtils.addVisits(places);
+
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.checkPrefCheckbox("history", true);
+ this.acceptDialog();
+ };
+ wh.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);
+ };
+ wh.open();
+ await wh.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("http://" + i + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) });
+ uris.push(pURI);
+ }
+
+ await PlacesTestUtils.addVisits(places);
+ let wh = new WindowHelper();
+ wh.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();
+ };
+ wh.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);
+ is(exists, 0, "form entry " + entry + " should no longer exist");
+ }
+
+ // OK, done, cleanup after ourselves.
+ await blankSlate();
+ await promiseHistoryClearedState(uris, true);
+ await ensureDownloadsClearedState(downloadIDs, true);
+ };
+ wh.open();
+ await wh.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("http://" + aValue + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) });
+ uris.push(pURI);
+ });
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ await PlacesTestUtils.addVisits(places);
+ let wh = new WindowHelper();
+ wh.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();
+ };
+ wh.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);
+ };
+ wh.open();
+ await wh.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("http://" + aValue + "-minutes-ago.com/");
+ places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) });
+ uris.push(pURI);
+ });
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ await PlacesTestUtils.addVisits(places);
+ let wh = new WindowHelper();
+ wh.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();
+ };
+ wh.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);
+ };
+ wh.open();
+ await wh.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("http://" + 10 + "-minutes-ago.com/");
+ await PlacesTestUtils.addVisits({
+ uri: pURI,
+ visitDate: visitTimeForMinutesAgo(10),
+ });
+ let uris = [pURI];
+
+ let wh = new WindowHelper();
+ wh.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();
+ };
+ wh.onunload = async function() {
+ await promiseSanitized;
+
+ await promiseHistoryClearedState(uris, true);
+
+ let exists = await formNameExists(formEntries[0]);
+ is(
+ Boolean(exists),
+ false,
+ "form entry " + formEntries[0] + " should no longer exist"
+ );
+ };
+ wh.open();
+ await wh.promiseClosed;
+});
+
+add_task(async function test_no_formdata_history_to_clear() {
+ let promiseSanitized = promiseSanitizationComplete();
+ let wh = new WindowHelper();
+ wh.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();
+ };
+ wh.open();
+ await wh.promiseClosed;
+ await promiseSanitized;
+});
+
+add_task(async function test_form_entries() {
+ let formEntry = await promiseAddFormEntryWithMinutesAgo(10);
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ let wh = new WindowHelper();
+ wh.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();
+ };
+ wh.onunload = async function() {
+ await promiseSanitized;
+ let exists = await formNameExists(formEntry);
+ is(
+ Boolean(exists),
+ false,
+ "form entry " + formEntry + " should no longer exist"
+ );
+ };
+ wh.open();
+ await wh.promiseClosed;
+});
+
+// Test for offline cache deletion
+add_task(async function test_offline_cache() {
+ // Prepare stuff, we will work with www.example.com
+ var URL = "http://www.example.com";
+ var URI = makeURI(URL);
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ URI,
+ {}
+ );
+
+ // Give www.example.com privileges to store offline data
+ Services.perms.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN
+ );
+
+ // Store something to the offline cache
+ var appcacheserv = Cc[
+ "@mozilla.org/network/application-cache-service;1"
+ ].getService(Ci.nsIApplicationCacheService);
+ var appcachegroupid = appcacheserv.buildGroupIDForInfo(
+ makeURI(URL + "/manifest"),
+ Services.loadContextInfo.default
+ );
+ var appcache = appcacheserv.createApplicationCache(appcachegroupid);
+ var storage = Services.cache2.appCacheStorage(
+ Services.loadContextInfo.default,
+ appcache
+ );
+
+ // Open the dialog
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ // Clear only offlineApps
+ this.uncheckAllCheckboxes();
+ this.checkPrefCheckbox("offlineApps", true);
+ this.acceptDialog();
+ };
+ wh.onunload = function() {
+ // Check if the cache has been deleted
+ var size = -1;
+ var visitor = {
+ onCacheStorageInfo(aEntryCount, aConsumption, aCapacity, aDiskDirectory) {
+ size = aConsumption;
+ },
+ };
+ storage.asyncVisitStorage(visitor, false);
+ // Offline cache visit happens synchronously, since it's forwarded to the old code
+ is(size, 0, "offline application cache entries evicted");
+ };
+
+ var cacheListener = {
+ onCacheEntryCheck() {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+ onCacheEntryAvailable(entry, isnew, unused, status) {
+ is(status, Cr.NS_OK);
+ var stream = entry.openOutputStream(0, -1);
+ var content = "content";
+ stream.write(content, content.length);
+ stream.close();
+ entry.close();
+ wh.open();
+ },
+ };
+
+ storage.asyncOpenURI(
+ makeURI(URL),
+ "",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ cacheListener
+ );
+ await wh.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 = "http://www.example.com";
+ var URI = makeURI(URL);
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ URI,
+ {}
+ );
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ // Open the dialog
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ // Clear only offlineApps
+ this.uncheckAllCheckboxes();
+ this.checkPrefCheckbox("siteSettings", true);
+ this.acceptDialog();
+ };
+ wh.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"
+ );
+ };
+ wh.open();
+ await wh.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 aWin
+ * The dialog's nsIDOMWindow
+ */
+function WindowHelper(aWin) {
+ this.win = aWin;
+ this.promiseClosed = new Promise(resolve => {
+ this._resolveClosed = resolve;
+ });
+}
+
+WindowHelper.prototype = {
+ /**
+ * "Presses" the dialog's OK button.
+ */
+ acceptDialog() {
+ let dialog = this.win.document.querySelector("dialog");
+ is(
+ dialog.getButton("accept").disabled,
+ false,
+ "Dialog's OK button should not be disabled"
+ );
+ dialog.acceptDialog();
+ },
+
+ /**
+ * "Presses" the dialog's Cancel button.
+ */
+ cancelDialog() {
+ this.win.document.querySelector("dialog").cancelDialog();
+ },
+
+ /**
+ * (Un)checks a history scope checkbox (browser & download history,
+ * form history, etc.).
+ *
+ * @param aPrefName
+ * The final portion of the checkbox's privacy.cpd.* preference name
+ * @param aCheckState
+ * True if the checkbox should be checked, false otherwise
+ */
+ checkPrefCheckbox(aPrefName, aCheckState) {
+ var pref = "privacy.cpd." + aPrefName;
+ var cb = this.win.document.querySelectorAll(
+ "checkbox[preference='" + pref + "']"
+ );
+ is(cb.length, 1, "found checkbox for " + pref + " preference");
+ if (cb[0].checked != aCheckState) {
+ cb[0].click();
+ }
+ },
+
+ /**
+ * Makes sure all the checkboxes are checked.
+ */
+ _checkAllCheckboxesCustom(check) {
+ var cb = this.win.document.querySelectorAll("checkbox[preference]");
+ ok(cb.length > 1, "found checkboxes for preferences");
+ for (var i = 0; i < cb.length; ++i) {
+ var pref = this.win.Preferences.get(cb[i].getAttribute("preference"));
+ if (!!pref.value ^ check) {
+ cb[i].click();
+ }
+ }
+ },
+
+ checkAllCheckboxes() {
+ this._checkAllCheckboxesCustom(true);
+ },
+
+ uncheckAllCheckboxes() {
+ this._checkAllCheckboxesCustom(false);
+ },
+
+ /**
+ * @return The dialog's duration dropdown
+ */
+ getDurationDropdown() {
+ return this.win.document.getElementById("sanitizeDurationChoice");
+ },
+
+ /**
+ * @return The clear-everything warning box
+ */
+ getWarningPanel() {
+ return this.win.document.getElementById("sanitizeEverythingWarningBox");
+ },
+
+ /**
+ * @return True if the "Everything" warning panel is visible (as opposed to
+ * the tree)
+ */
+ isWarningPanelVisible() {
+ return !this.getWarningPanel().hidden;
+ },
+
+ /**
+ * Opens the clear recent history dialog. Before calling this, set
+ * this.onload to a function to execute onload. It should close the dialog
+ * when done so that the tests may continue. Set this.onunload to a function
+ * to execute onunload. this.onunload is optional. If it returns true, the
+ * caller is expected to call promiseAsyncUpdates at some point; if false is
+ * returned, promiseAsyncUpdates is called automatically.
+ */
+ open() {
+ let wh = this;
+
+ function windowObserver(win, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ var loaded = false;
+
+ win.addEventListener(
+ "load",
+ function onload(event) {
+ if (win.name !== "SanitizeDialog") {
+ return;
+ }
+
+ wh.win = win;
+ loaded = true;
+ executeSoon(() => wh.onload());
+ },
+ { once: true }
+ );
+
+ win.addEventListener("unload", function onunload(event) {
+ if (win.name !== "SanitizeDialog") {
+ win.removeEventListener("unload", onunload);
+ return;
+ }
+
+ // Why is unload fired before load?
+ if (!loaded) {
+ return;
+ }
+
+ win.removeEventListener("unload", onunload);
+ wh.win = win;
+
+ // Some exceptions that reach here don't reach the test harness, but
+ // ok()/is() do...
+ (async function() {
+ if (wh.onunload) {
+ await wh.onunload();
+ }
+ await PlacesTestUtils.promiseAsyncUpdates();
+ wh._resolveClosed();
+ })();
+ });
+ }
+ Services.ww.registerNotification(windowObserver);
+
+ let browserWin = null;
+ if (Services.appinfo.OS !== "Darwin") {
+ // Retrieve the browser window so we can specify it as the parent
+ // of the dialog to simulate the way the user opens the dialog
+ // on Windows and Linux.
+ browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ }
+
+ Services.ww.openWindow(
+ browserWin,
+ "chrome://browser/content/sanitize.xhtml",
+ "SanitizeDialog",
+ "chrome,titlebar,dialog,centerscreen,modal",
+ null
+ );
+ },
+
+ /**
+ * 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 new Promise((resolve, reject) =>
+ FormHistory.update(
+ { op: "add", fieldname: name, value: "dummy", firstUsed: timestamp },
+ {
+ handleError(error) {
+ reject();
+ throw new Error("Error occurred updating form history: " + error);
+ },
+ handleCompletion(reason) {
+ resolve(name);
+ },
+ }
+ )
+ );
+}
+
+/**
+ * Checks if a form entry exists.
+ */
+function formNameExists(name) {
+ return new Promise((resolve, reject) => {
+ let count = 0;
+ FormHistory.count(
+ { fieldname: name },
+ {
+ handleResult: result => (count = result),
+ handleError(error) {
+ reject(error);
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion(reason) {
+ if (!reason) {
+ resolve(count);
+ }
+ },
+ }
+ );
+ });
+}
+
+/**
+ * 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 new Promise((resolve, reject) => {
+ FormHistory.update(
+ { op: "remove" },
+ {
+ handleCompletion(reason) {
+ if (!reason) {
+ resolve();
+ }
+ },
+ handleError(error) {
+ reject(error);
+ throw new Error("Error occurred updating form history: " + error);
+ },
+ }
+ );
+ });
+
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Ensures that the given pref is the expected value.
+ *
+ * @param aPrefName
+ * The pref's sub-branch under the privacy branch
+ * @param aExpectedVal
+ * The pref's expected value
+ * @param aMsg
+ * Passed to is()
+ */
+function boolPrefIs(aPrefName, aExpectedVal, aMsg) {
+ is(Services.prefs.getBoolPref("privacy." + aPrefName), aExpectedVal, aMsg);
+}
+
+/**
+ * Checks to see if the download with the specified path exists.
+ *
+ * @param aPath
+ * The path of the download to check
+ * @return True if the download exists, false otherwise
+ */
+async function downloadExists(aPath) {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let listArray = await publicList.getAll();
+ return listArray.some(i => i.target.path == aPath);
+}
+
+/**
+ * Ensures that the specified downloads are either cleared or not.
+ *
+ * @param aDownloadIDs
+ * Array of download database IDs
+ * @param aShouldBeCleared
+ * True if each download should be cleared, false otherwise
+ */
+async function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) {
+ let niceStr = aShouldBeCleared ? "no longer" : "still";
+ for (let id of aDownloadIDs) {
+ is(
+ await downloadExists(id),
+ !aShouldBeCleared,
+ "download " + id + " should " + niceStr + " exist"
+ );
+ }
+}
+
+/**
+ * Ensures that the given pref is the expected value.
+ *
+ * @param aPrefName
+ * The pref's sub-branch under the privacy branch
+ * @param aExpectedVal
+ * The pref's expected value
+ * @param aMsg
+ * Passed to is()
+ */
+function intPrefIs(aPrefName, aExpectedVal, aMsg) {
+ is(Services.prefs.getIntPref("privacy." + aPrefName), aExpectedVal, aMsg);
+}
+
+/**
+ * Creates a visit time.
+ *
+ * @param aMinutesAgo
+ * The visit will be visited this many minutes ago
+ */
+function visitTimeForMinutesAgo(aMinutesAgo) {
+ return now_uSec - aMinutesAgo * kUsecPerMin;
+}
diff --git a/browser/base/content/test/sanitize/dummy.js b/browser/base/content/test/sanitize/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/base/content/test/sanitize/dummy.js
diff --git a/browser/base/content/test/sanitize/dummy_page.html b/browser/base/content/test/sanitize/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/base/content/test/sanitize/dummy_page.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/browser/base/content/test/sanitize/head.js b/browser/base/content/test/sanitize/head.js
new file mode 100644
index 0000000000..df564a5ea6
--- /dev/null
+++ b/browser/base/content/test/sanitize/head.js
@@ -0,0 +1,331 @@
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.jsm",
+ FormHistory: "resource://gre/modules/FormHistory.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ Sanitizer: "resource:///modules/Sanitizer.jsm",
+ SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.jsm",
+ PermissionTestUtils: "resource://testing-common/PermissionTestUtils.jsm",
+});
+
+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: [
+ ["network.cookie.lifetimePolicy", opt.lifetimePolicy],
+ ["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({
+ lifetimePolicy: Ci.nsICookieService.ACCEPT_SESSION,
+ 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({
+ lifetimePolicy: Ci.nsICookieService.ACCEPT_SESSION,
+ 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({
+ lifetimePolicy: Ci.nsICookieService.ACCEPT_SESSION,
+ 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({
+ lifetimePolicy: Ci.nsICookieService.ACCEPT_SESSION,
+ 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({
+ lifetimePolicy: Ci.nsICookieService.ACCEPT_NORMALLY,
+ 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({
+ lifetimePolicy: Ci.nsICookieService.ACCEPT_NORMALLY,
+ 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({
+ lifetimePolicy: Ci.nsICookieService.ACCEPT_SESSION,
+ createData: methods.createData,
+ checkData: methods.checkData,
+ originAttributes: originAttributes.oa,
+ cookiePermission: 123, // invalid cookie permission
+ expectedForOrg: false,
+ expectedForCom: false,
+ fullHost: true,
+ });
+ });
+ });
+}
diff --git a/browser/base/content/test/sidebar/.eslintrc.js b/browser/base/content/test/sidebar/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/sidebar/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/sidebar/browser.ini b/browser/base/content/test/sidebar/browser.ini
new file mode 100644
index 0000000000..3e076fadd4
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+
+[browser_sidebar_adopt.js]
+[browser_sidebar_keys.js]
+[browser_sidebar_move.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..e61e9a1972
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_adopt.js
@@ -0,0 +1,67 @@
+/* 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_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_keys.js b/browser/base/content/test/sidebar/browser_sidebar_keys.js
new file mode 100644
index 0000000000..2c9817d5b0
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_keys.js
@@ -0,0 +1,24 @@
+/* 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");
+});
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..49d705895b
--- /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.MozBoxOrdinalGroup];
+ });
+}
+
+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_switcher.js b/browser/base/content/test/sidebar/browser_sidebar_switcher.js
new file mode 100644
index 0000000000..dce5821394
--- /dev/null
+++ b/browser/base/content/test/sidebar/browser_sidebar_switcher.js
@@ -0,0 +1,64 @@
+registerCleanupFunction(() => {
+ SidebarUI.hide();
+});
+
+function showSwitcherPanelPromise() {
+ return new Promise(resolve => {
+ SidebarUI._switcherPanel.addEventListener(
+ "popupshown",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ SidebarUI.showSwitcherPanel();
+ });
+}
+
+function clickSwitcherButton(querySelector) {
+ let sidebarPopup = document.querySelector("#sidebarMenu-popup");
+ let switcherPromise = Promise.all([
+ BrowserTestUtils.waitForEvent(window, "SidebarFocused"),
+ BrowserTestUtils.waitForEvent(sidebarPopup, "popuphidden"),
+ ]);
+ document.querySelector(querySelector).click();
+ return switcherPromise;
+}
+
+add_task(async function() {
+ // If a sidebar is already open, close it.
+ if (!document.getElementById("sidebar-box").hidden) {
+ ok(
+ false,
+ "Unexpected sidebar found - a previous test failed to cleanup correctly"
+ );
+ SidebarUI.hide();
+ }
+
+ let sidebar = document.querySelector("#sidebar-box");
+ await SidebarUI.show("viewBookmarksSidebar");
+
+ await showSwitcherPanelPromise();
+ await clickSwitcherButton("#sidebar-switcher-history");
+ is(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewHistorySidebar",
+ "History sidebar loaded"
+ );
+
+ await showSwitcherPanelPromise();
+ await clickSwitcherButton("#sidebar-switcher-tabs");
+ is(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewTabsSidebar",
+ "Tabs sidebar loaded"
+ );
+
+ await showSwitcherPanelPromise();
+ await clickSwitcherButton("#sidebar-switcher-bookmarks");
+ is(
+ sidebar.getAttribute("sidebarcommand"),
+ "viewBookmarksSidebar",
+ "Bookmarks sidebar loaded"
+ );
+});
diff --git a/browser/base/content/test/siteIdentity/.eslintrc.js b/browser/base/content/test/siteIdentity/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/siteIdentity/browser.ini b/browser/base/content/test/siteIdentity/browser.ini
new file mode 100644
index 0000000000..9a4dd730cf
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser.ini
@@ -0,0 +1,125 @@
+[DEFAULT]
+support-files =
+ head.js
+ dummy_page.html
+ !/image/test/mochitest/blue.png
+
+[browser_geolocation_indicator.js]
+[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_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_getSecurityInfo.js]
+support-files =
+ dummy_iframe_page.html
+[browser_identity_UI.js]
+[browser_identityBlock_flicker.js]
+[browser_identityBlock_focus.js]
+support-files = ../permissions/permissions.html
+[browser_identityIcon_img_url.js]
+support-files =
+ file_mixedPassiveContent.html
+ file_csp_block_all_mixedcontent.html
+[browser_identityPopup_clearSiteData.js]
+skip-if = (os == "linux" && bits == 64) # Bug 1577395
+[browser_identityPopup_custom_roots.js]
+[browser_identityPopup_focus.js]
+[browser_identityPopup_HttpsOnlyMode.js]
+[browser_mcb_redirect.js]
+tags = mcb
+support-files =
+ test_mcb_redirect.html
+ test_mcb_redirect_image.html
+ test_mcb_double_redirect_image.html
+ test_mcb_redirect.js
+ test_mcb_redirect.sjs
+[browser_mixed_content_cert_override.js]
+skip-if = verify
+tags = mcb
+support-files =
+ test-mixedcontent-securityerrors.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_mixedContentFramesOnHttp.js]
+tags = mcb
+support-files =
+ file_mixedContentFramesOnHttp.html
+ file_mixedPassiveContent.html
+[browser_mixedContentFromOnunload.js]
+tags = mcb
+support-files =
+ file_mixedContentFromOnunload.html
+ file_mixedContentFromOnunload_test1.html
+ file_mixedContentFromOnunload_test2.html
+[browser_no_mcb_on_http_site.js]
+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_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_check_identity_state.js]
+[browser_iframe_navigation.js]
+support-files =
+ iframe_navigation.html
+[browser_navigation_failures.js]
+[browser_secure_transport_insecure_scheme.js]
+[browser_ignore_same_page_navigation.js]
+[browser_mixed_content_with_navigation.js]
+tags = mcb
+support-files =
+ file_mixedPassiveContent.html
+ file_bug1045809_1.html
+[browser_tab_sharing_state.js]
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..8b2aaa2feb
--- /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..2b99a018c5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug822367.js
@@ -0,0 +1,249 @@
+/*
+ * 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.loadURI(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,
+ });
+
+ let { gIdentityHandler } = 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"
+ );
+ });
+});
+
+// Mixed Display Test - Doorhanger should not appear
+add_task(async function MixedTest2() {
+ var url = HTTPS_TEST_ROOT_2 + "file_bug822367_2.html";
+ BrowserTestUtils.loadURI(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.loadURI(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+add_task(async function MixedTest3A() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ let { gIdentityHandler } = 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,
+ });
+});
+
+// Location change - User override on one page doesn't propogate to another page after location change.
+add_task(async function MixedTest4() {
+ var url = HTTPS_TEST_ROOT_2 + "file_bug822367_4.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+add_task(async function MixedTest4A() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ let { gIdentityHandler } = 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!"
+ );
+ });
+});
+
+// Mixed script attempts to load in a document.open()
+add_task(async function MixedTest5() {
+ var url = HTTPS_TEST_ROOT + "file_bug822367_5.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ await BrowserTestUtils.browserLoaded(gTestBrowser, false, url);
+});
+
+add_task(async function MixedTest5A() {
+ await assertMixedContentBlockingState(gTestBrowser, {
+ activeLoaded: false,
+ activeBlocked: true,
+ passiveLoaded: false,
+ });
+
+ let { gIdentityHandler } = 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"
+ );
+ });
+});
+
+// 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.loadURI(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,
+ });
+
+ let { gIdentityHandler } = 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,
+ });
+});
+
+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..2735fd2a79
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug902156.js
@@ -0,0 +1,169 @@
+/*
+ * 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_task(async function setup() {
+ 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.loadURI(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"
+ );
+ });
+ });
+});
+
+// ------------------------ 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"
+ );
+ });
+ });
+});
+
+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..9bc42df9bb
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_bug906190.js
@@ -0,0 +1,331 @@
+/* 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);
+ gIdentityHandler.disableMixedContentProtection();
+ await promiseReloaded;
+
+ // Wait for the script in the page to update the contents of the test div.
+ await SpecialPowers.spawn(
+ browser,
+ [childTabSpec],
+ async childTabSpecContent => {
+ let testDiv = content.document.getElementById("mctestdiv");
+ await ContentTaskUtils.waitForCondition(
+ () => testDiv.innerHTML == "Mixed Content Blocker disabled"
+ );
+
+ // Add the link for the child tab to the page.
+ let mainDiv = content.document.createElement("div");
+
+ // eslint-disable-next-line no-unsanitized/property
+ mainDiv.innerHTML =
+ '<p><a id="linkToOpenInNewTab" href="' +
+ childTabSpecContent +
+ '">Link</a></p>';
+ content.document.body.appendChild(mainDiv);
+ }
+ );
+
+ // Execute the test in the child tabs with the two methods to open it.
+ for (let openFn of [simulateCtrlClick, simulateContextMenuOpenInTab]) {
+ let promiseTabLoaded = waitForSomeTabToLoad();
+ openFn(browser);
+ await promiseTabLoaded;
+ gBrowser.selectTabAtIndex(2);
+
+ if (waitForMetaRefresh) {
+ await waitForSomeTabToLoad();
+ }
+
+ await testTaskFn();
+
+ gBrowser.removeCurrentTab();
+ }
+ }
+ );
+}
+
+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]],
+ });
+});
+
+/**
+ * 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..8c47d4a196
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_check_identity_state.js
@@ -0,0 +1,706 @@
+/*
+ * Test the identity mode UI for a variety of page types
+ */
+
+"use strict";
+
+const DUMMY = "browser/browser/base/content/test/siteIdentity/dummy_page.html";
+const INSECURE_ICON_PREF = "security.insecure_connection_icon.enabled";
+const INSECURE_TEXT_PREF = "security.insecure_connection_text.enabled";
+const INSECURE_PBMODE_ICON_PREF =
+ "security.insecure_connection_icon.pbmode.enabled";
+
+function loadNewTab(url) {
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
+}
+
+function getIdentityMode(aWindow = window) {
+ return aWindow.document.getElementById("identity-box").className;
+}
+
+function getConnectionState() {
+ // Prevents items that are being lazy loaded causing issues
+ document.getElementById("identity-box").click();
+ gIdentityHandler.refreshIdentityPopup();
+ return document.getElementById("identity-popup").getAttribute("connection");
+}
+
+function getSecurityConnectionBG() {
+ // Get the background image of the security connection.
+ document.getElementById("identity-box").click();
+ gIdentityHandler.refreshIdentityPopup();
+ return gBrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-mainView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("background-image");
+}
+
+function getReaderModeURL() {
+ // Gets the reader mode URL from "identity-popup mainView panel header span"
+ document.getElementById("identity-box").click();
+ gIdentityHandler.refreshIdentityPopup();
+ return document.getElementById("identity-popup-mainView-panel-header-span")
+ .innerHTML;
+}
+
+// This test is slow on Linux debug e10s
+requestLongerTimeout(2);
+
+async function webpageTest(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let oldTab = await loadNewTab("about:robots");
+
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage() {
+ await webpageTest(false);
+ await webpageTest(true);
+});
+
+async function webpageTestTextWarning(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_TEXT_PREF, secureCheck]] });
+ let oldTab = await loadNewTab("about:robots");
+
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should have not secure text"
+ );
+ } else {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should have not secure text"
+ );
+ } else {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage_text_warning() {
+ await webpageTestTextWarning(false);
+ await webpageTestTextWarning(true);
+});
+
+async function webpageTestTextWarningCombined(secureCheck) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [INSECURE_TEXT_PREF, secureCheck],
+ [INSECURE_ICON_PREF, secureCheck],
+ ],
+ });
+ let oldTab = await loadNewTab("about:robots");
+
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should be not secure"
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(
+ getIdentityMode(),
+ "notSecure notSecureText",
+ "Identity should be not secure"
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage_text_warning_combined() {
+ await webpageTestTextWarning(false);
+ await webpageTestTextWarning(true);
+});
+
+async function blankPageTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("about:blank");
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "pageproxystate should be invalid"
+ );
+
+ gBrowser.selectedTab = oldTab;
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "pageproxystate should be valid"
+ );
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "pageproxystate should be invalid"
+ );
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_blank() {
+ await blankPageTest(true);
+ await blankPageTest(false);
+});
+
+async function secureTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("https://example.com/" + DUMMY);
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_secure_enabled() {
+ await secureTest(true);
+ await secureTest(false);
+});
+
+async function viewSourceTest() {
+ let sourceTab = await loadNewTab("view-source:https://example.com/" + DUMMY);
+
+ gBrowser.selectedTab = sourceTab;
+ is(
+ getIdentityMode(),
+ "verifiedDomain",
+ "Identity should be verified while viewing source"
+ );
+
+ gBrowser.removeTab(sourceTab);
+}
+
+add_task(async function test_viewSource() {
+ await viewSourceTest();
+});
+
+async function insecureTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("http://example.com/" + DUMMY);
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ is(
+ document.getElementById("identity-icon").getAttribute("tooltiptext"),
+ gNavigatorBundle.getString("identity.notSecure.tooltip"),
+ "The insecure lock icon has a correct tooltip text."
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ is(
+ document.getElementById("identity-icon").getAttribute("tooltiptext"),
+ gNavigatorBundle.getString("identity.notSecure.tooltip"),
+ "The insecure lock icon has a correct tooltip text."
+ );
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_insecure() {
+ await insecureTest(true);
+ await insecureTest(false);
+});
+
+async function addonsTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("about:addons");
+ is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_addons() {
+ await addonsTest(true);
+ await addonsTest(false);
+});
+
+async function fileTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let fileURI = getTestFilePath("");
+
+ let newTab = await loadNewTab(fileURI);
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_file() {
+ await fileTest(true);
+ await fileTest(false);
+});
+
+async function resourceUriTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let dataURI = "resource://gre/modules/Services.jsm";
+
+ let newTab = await loadNewTab(dataURI);
+
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(
+ getIdentityMode(),
+ "localResource",
+ "Identity should be a local a resource"
+ );
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_resource_uri() {
+ await resourceUriTest(true);
+ await resourceUriTest(false);
+});
+
+async function noCertErrorTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(gBrowser, "https://nocert.example.com/");
+ await promise;
+ is(
+ getIdentityMode(),
+ "certErrorPage",
+ "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",
+ "Identity should be the cert error page."
+ );
+ is(
+ getConnectionState(),
+ "cert-error-page",
+ "Connection should be the cert error page."
+ );
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_net_error_uri() {
+ await noCertErrorTest(true);
+ await noCertErrorTest(false);
+});
+
+add_task(async function httpsOnlyErrorTest() {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(gBrowser, "http://nocert.example.com/");
+ await promise;
+ is(
+ getIdentityMode(),
+ "httpsOnlyErrorPage",
+ "Identity should be the https-only mode error page."
+ );
+ is(
+ getConnectionState(),
+ "https-only-error-page",
+ "Connection should be the https-only mode error page."
+ );
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(
+ getIdentityMode(),
+ "httpsOnlyErrorPage",
+ "Identity should be the https-only mode error page."
+ );
+ is(
+ getConnectionState(),
+ "https-only-error-page",
+ "Connection should be the https-only mode page."
+ );
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+async function noCertErrorFromNavigationTest(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ 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",
+ "Identity should be the cert error page."
+ );
+ is(
+ getConnectionState(),
+ "cert-error-page",
+ "Connection should be the cert error page."
+ );
+
+ gBrowser.removeTab(newTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_net_error_uri_from_navigation_tab() {
+ await noCertErrorFromNavigationTest(true);
+ await noCertErrorFromNavigationTest(false);
+});
+
+async function aboutBlockedTest(secureCheck) {
+ let url = "http://www.itisatrap.org/firefox/its-an-attack.html";
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [INSECURE_ICON_PREF, secureCheck],
+ ["urlclassifier.blockedTable", "moztest-block-simple"],
+ ],
+ });
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url,
+ true
+ );
+
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown.");
+ is(getConnectionState(), "not-secure", "Connection should be not secure.");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown.");
+ is(getConnectionState(), "not-secure", "Connection should be not secure.");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_blocked() {
+ await aboutBlockedTest(true);
+ await aboutBlockedTest(false);
+});
+
+add_task(async function noCertErrorSecurityConnectionBGTest() {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = tab;
+ let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(gBrowser, "https://nocert.example.com/");
+ await promise;
+
+ is(
+ getSecurityConnectionBG(),
+ `url("chrome://global/skin/icons/connection-mixed-passive-loaded.svg")`,
+ "Security connection should show a warning lock icon."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+async function aboutUriTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let aboutURI = "about:robots";
+
+ let newTab = await loadNewTab(aboutURI);
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_about_uri() {
+ await aboutUriTest(true);
+ await aboutUriTest(false);
+});
+
+async function readerUriTest(secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+
+ let newTab = await loadNewTab("about:reader?url=http://example.com");
+ gBrowser.selectedTab = newTab;
+ let readerURL = getReaderModeURL();
+ is(
+ readerURL,
+ "Site Information for example.com",
+ "should be the correct URI in reader mode"
+ );
+
+ gBrowser.removeTab(newTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_reader_uri() {
+ await readerUriTest(true);
+ await readerUriTest(false);
+});
+
+async function dataUriTest(secureCheck) {
+ let oldTab = await loadNewTab("about:robots");
+ await SpecialPowers.pushPrefEnv({ set: [[INSECURE_ICON_PREF, secureCheck]] });
+ let dataURI = "data:text/html,hi";
+
+ let newTab = await loadNewTab(dataURI);
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "localResource", "Identity should be localResource");
+
+ gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(getIdentityMode(), "notSecure", "Identity should be not secure");
+ } else {
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+ }
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(oldTab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_data_uri() {
+ await dataUriTest(true);
+ await dataUriTest(false);
+});
+
+async function pbModeTest(prefs, secureCheck) {
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let oldTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "about:robots"
+ );
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "http://example.com/" + DUMMY
+ );
+
+ if (secureCheck) {
+ is(
+ getIdentityMode(privateWin),
+ "notSecure",
+ "Identity should be not secure"
+ );
+ } else {
+ is(
+ getIdentityMode(privateWin),
+ "unknownIdentity",
+ "Identity should be unknown"
+ );
+ }
+
+ privateWin.gBrowser.selectedTab = oldTab;
+ is(
+ getIdentityMode(privateWin),
+ "localResource",
+ "Identity should be localResource"
+ );
+
+ privateWin.gBrowser.selectedTab = newTab;
+ if (secureCheck) {
+ is(
+ getIdentityMode(privateWin),
+ "notSecure",
+ "Identity should be not secure"
+ );
+ } else {
+ is(
+ getIdentityMode(privateWin),
+ "unknownIdentity",
+ "Identity should be unknown"
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_pb_mode() {
+ let prefs = [
+ [INSECURE_ICON_PREF, true],
+ [INSECURE_PBMODE_ICON_PREF, true],
+ ];
+ await pbModeTest(prefs, true);
+ prefs = [
+ [INSECURE_ICON_PREF, false],
+ [INSECURE_PBMODE_ICON_PREF, true],
+ ];
+ await pbModeTest(prefs, true);
+ prefs = [
+ [INSECURE_ICON_PREF, false],
+ [INSECURE_PBMODE_ICON_PREF, false],
+ ];
+ await pbModeTest(prefs, false);
+});
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..0f34fdc973
--- /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.loadURI(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..1f41596666
--- /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.loadURI(browser, HTTPS_TLS1_0);
+ await BrowserTestUtils.browserLoaded(browser);
+ isSecurityState(browser, "broken");
+ is(
+ getIdentityMode(),
+ "unknownIdentity weakCipher",
+ "Identity should be unknownIdentity"
+ );
+ await checkConnectionState("not-secure");
+
+ BrowserTestUtils.loadURI(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.loadURI(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.loadURI(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.loadURI(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..bc593cdfe1
--- /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);
+
+ChromeUtils.import("resource:///modules/PermissionUI.jsm", this);
+ChromeUtils.import("resource:///modules/SitePermissions.jsm", this);
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+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 BrowserTestUtils.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 BrowserTestUtils.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 testIdentityPopupGeoContainer(
+ 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 openIdentityPopup if popup is closed, otherwise it does not resolve
+ if (!gIdentityHandler._identityBox.hasAttribute("open")) {
+ await openIdentityPopup();
+ }
+
+ let checkContainer = checkForDOMElement(
+ containerVisible,
+ "identity-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 BrowserTestUtils.waitForCondition(
+ () => container.childElementCount == 2,
+ "identity-popup-geo-container should have two elements."
+ );
+ is(
+ container.childNodes[0].classList[0],
+ "identity-popup-permission-item",
+ "Geo container should have permission item."
+ );
+ is(
+ container.childNodes[1].id,
+ "geo-access-indicator-item",
+ "Geo container should have indicator item."
+ );
+ }
+ let checkAccessIndicator = checkForDOMElement(
+ timestampVisible,
+ "geo-access-indicator-item"
+ );
+
+ return Promise.all([checkContainer, checkAccessIndicator]);
+}
+
+function openExamplePage(tabbrowser = gBrowser) {
+ return BrowserTestUtils.openNewForegroundTab(tabbrowser, EXAMPLE_PAGE_URL);
+}
+
+function requestGeoLocation(browser) {
+ return SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve => {
+ content.navigator.geolocation.getCurrentPosition(
+ () => resolve(true),
+ error => resolve(error.code !== 1) // PERMISSION_DENIED = 1
+ );
+ });
+ });
+}
+
+function answerGeoLocationPopup(allow, remember = false) {
+ let notification = PopupNotifications.getNotification("geolocation");
+ ok(
+ PopupNotifications.isPanelOpen && notification,
+ "Geolocation notification is open"
+ );
+
+ let rememberCheck = PopupNotifications.panel.querySelector(
+ ".popup-notification-checkbox"
+ );
+ rememberCheck.checked = remember;
+
+ let popupHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ if (allow) {
+ let allowBtn = PopupNotifications.panel.querySelector(
+ ".popup-notification-primary-button"
+ );
+ allowBtn.click();
+ } else {
+ let denyBtn = PopupNotifications.panel.querySelector(
+ ".popup-notification-secondary-button"
+ );
+ denyBtn.click();
+ }
+ return popupHidden;
+}
+
+function setGeoLastAccess(browser, state) {
+ return new Promise(resolve => {
+ let host = browser.currentURI.host;
+ let handler = {
+ handleCompletion: () => resolve(),
+ };
+
+ if (!state) {
+ CP.removeByDomainAndName(
+ host,
+ GEO_CONTENT_PREF_KEY,
+ browser.loadContext,
+ handler
+ );
+ return;
+ }
+ CP.set(
+ host,
+ GEO_CONTENT_PREF_KEY,
+ new Date().toString(),
+ browser.loadContext,
+ handler
+ );
+ });
+}
+
+async function testGeoLocationLastAccessSet(browser) {
+ let timestamp = await new Promise(resolve => {
+ let lastAccess = null;
+ CP.getByDomainAndName(
+ gBrowser.currentURI.spec,
+ GEO_CONTENT_PREF_KEY,
+ browser.loadContext,
+ {
+ handleResult(pref) {
+ lastAccess = pref.value;
+ },
+ handleCompletion() {
+ resolve(lastAccess);
+ },
+ }
+ );
+ });
+
+ ok(timestamp != null, "Geo last access timestamp set");
+
+ let parseSuccess = true;
+ try {
+ timestamp = new Date(timestamp);
+ } catch (e) {
+ parseSuccess = false;
+ }
+ ok(
+ parseSuccess && !isNaN(timestamp),
+ "Geo last access timestamp is valid Date"
+ );
+}
+
+async function cleanup(tab) {
+ await setGeoLastAccess(tab.linkedBrowser, false);
+ SitePermissions.removeFromPrincipal(
+ tab.linkedBrowser.contentPrincipal,
+ "geo",
+ tab.linkedBrowser
+ );
+ gBrowser.resetBrowserSharing(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testIndicatorGeoSharingState(active) {
+ let tab = await openExamplePage();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: active });
+ await testGeoSharingIconVisible(active);
+
+ await cleanup(tab);
+}
+
+async function testIndicatorExplicitAllow(persistent) {
+ let tab = await openExamplePage();
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ info("Requesting geolocation");
+ let request = requestGeoLocation(tab.linkedBrowser);
+ await popupShown;
+ info("Allowing geolocation via popup");
+ answerGeoLocationPopup(true, persistent);
+ await request;
+
+ await Promise.all([
+ testGeoSharingIconVisible(true),
+ testIdentityPopupGeoContainer(true, true),
+ testGeoLocationLastAccessSet(tab.linkedBrowser),
+ ]);
+
+ await cleanup(tab);
+}
+
+// Indicator and identity 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 identity 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),
+ testIdentityPopupGeoContainer(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);
+});
+
+// Identity popup shows permission if geo permission is set to persistent allow
+add_task(async function test_identity_popup_permission_scope_permanent() {
+ PermissionTestUtils.add(
+ EXAMPLE_PAGE_URI,
+ "geo",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+ let tab = await openExamplePage();
+
+ await testIdentityPopupGeoContainer(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_identity_popup_permission_sharing_state() {
+ let tab = await openExamplePage();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ await testIdentityPopupGeoContainer(true, false);
+
+ await cleanup(tab);
+});
+
+// Identity popup has correct state if sharing state and last geo access timestamp are set
+add_task(
+ async function test_identity_popup_permission_sharing_state_timestamp() {
+ let tab = await openExamplePage();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ await setGeoLastAccess(tab.linkedBrowser, true);
+
+ await testIdentityPopupGeoContainer(true, true);
+
+ await cleanup(tab);
+ }
+);
+
+// Clicking permission clear button clears permission and resets geo sharing state
+add_task(async function test_identity_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 openIdentityPopup();
+
+ let clearButton = document.querySelector(
+ "#identity-popup-geo-container button"
+ );
+ ok(clearButton, "Clear button is visible");
+ clearButton.click();
+
+ await Promise.all([
+ testGeoSharingIconVisible(false),
+ testIdentityPopupGeoContainer(false, false),
+ BrowserTestUtils.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_identity_no_duplicate_last_access_label() {
+ let tab = await openExamplePage();
+ await setGeoLastAccess(tab.linkedBrowser, true);
+ await openIdentityPopup();
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { geo: true });
+ await testIdentityPopupGeoContainer(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..42a956844c
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_getSecurityInfo.js
@@ -0,0 +1,77 @@
+/* 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;
+
+const IFRAME_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ ) + "dummy_iframe_page.html";
+
+// Tests the getSecurityInfo() function exposed on WindowGlobalParent.
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
+ let loaded = BrowserTestUtils.waitForErrorPage(browser);
+ BrowserTestUtils.loadURI(browser, "https://self-signed.example.com");
+ await loaded;
+
+ let securityInfo = await browser.browsingContext.currentWindowGlobal.getSecurityInfo();
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ ok(securityInfo, "Found some security info");
+ ok(securityInfo.failedCertChain, "Has a failed cert chain");
+ is(
+ securityInfo.errorCode,
+ MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT,
+ "Has the correct error code"
+ );
+ is(
+ securityInfo.serverCert.commonName,
+ "self-signed.example.com",
+ "Has the correct certificate"
+ );
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURI(browser, "http://example.com");
+ await loaded;
+
+ securityInfo = await browser.browsingContext.currentWindowGlobal.getSecurityInfo();
+ ok(!securityInfo, "Found no security info");
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURI(browser, "https://example.com");
+ await loaded;
+
+ securityInfo = await browser.browsingContext.currentWindowGlobal.getSecurityInfo();
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ 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"
+ );
+
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURI(browser, IFRAME_PAGE);
+ await loaded;
+
+ // Get the info of the parent, which is HTTP.
+ securityInfo = await browser.browsingContext.currentWindowGlobal.getSecurityInfo();
+ ok(!securityInfo, "Found no security info");
+
+ // Get the info of the frame, which is HTTPS.
+ securityInfo = await browser.browsingContext.children[0].currentWindowGlobal.getSecurityInfo();
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ 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..1e8d24943a
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityBlock_flicker.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests that the identity icons don't flicker when navigating,
+ * i.e. the box should show no intermediate identity state. */
+
+add_task(async function test() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots",
+ true
+ );
+ let identityBox = document.getElementById("identity-box");
+
+ is(
+ identityBox.className,
+ "localResource",
+ "identity box has the correct class"
+ );
+
+ let observerOptions = {
+ attributes: true,
+ attributeFilter: ["class"],
+ };
+ let classChanges = 0;
+
+ let observer = new MutationObserver(function(mutations) {
+ for (let mutation of mutations) {
+ is(mutation.type, "attributes");
+ is(mutation.attributeName, "class");
+ classChanges++;
+ is(
+ identityBox.className,
+ "verifiedDomain",
+ "identity box class changed correctly"
+ );
+ }
+ });
+ observer.observe(identityBox, observerOptions);
+
+ let loaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.loadURI(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..b08ce75e36
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js
@@ -0,0 +1,117 @@
+/* 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();
+});
+
+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._identityBox,
+ "ArrowRight"
+ );
+ is(
+ document.activeElement,
+ gIdentityHandler._identityBox,
+ "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(
+ gBrowser.getTabForBrowser(browser),
+ "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..09cfc9edf6
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityIcon_img_url.js
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/**
+ * 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",
+ testURL: "http://example.com",
+ img_url: `url("chrome://global/skin/icons/connection-mixed-active-loaded.svg")`,
+ },
+ {
+ type: "https",
+ testURL: "https://example.com",
+ img_url: `url("chrome://browser/skin/connection-secure.svg")`,
+ },
+ {
+ type: "non-chrome about page",
+ testURL: "about:about",
+ img_url: `url("chrome://global/skin/icons/document.svg")`,
+ },
+ {
+ type: "chrome about page",
+ testURL: "about:preferences",
+ img_url: `url("chrome://branding/content/identity-icons-brand.svg")`,
+ },
+ {
+ type: "file",
+ testURL: "dummy_page.html",
+ img_url: `url("chrome://global/skin/icons/document.svg")`,
+ },
+ {
+ type: "resource",
+ testURL: "resource://gre/modules/Log.jsm",
+ img_url: `url("chrome://global/skin/icons/document.svg")`,
+ },
+ {
+ type: "mixedPassiveContent",
+ testURL: kBaseURI + "file_mixedPassiveContent.html",
+ img_url: `url("chrome://global/skin/icons/connection-mixed-passive-loaded.svg")`,
+ },
+ {
+ type: "mixedActiveContent",
+ testURL: kBaseURI + "file_csp_block_all_mixedcontent.html",
+ img_url: `url("chrome://browser/skin/connection-secure.svg")`,
+ },
+ {
+ type: "certificateError",
+ testURL: "https://self-signed.example.com",
+ img_url: `url("chrome://global/skin/icons/connection-mixed-passive-loaded.svg")`,
+ },
+ {
+ type: "localhost",
+ testURL: "http://127.0.0.1",
+ img_url: `url("chrome://global/skin/icons/document.svg")`,
+ },
+ {
+ type: "localhost + http frame",
+ testURL: kBaseURILocalhost + "file_csp_block_all_mixedcontent.html",
+ img_url: `url("chrome://global/skin/icons/document.svg")`,
+ },
+ {
+ type: "data URI",
+ testURL: "data:text/html,<div>",
+ img_url: `url("chrome://global/skin/icons/connection-mixed-active-loaded.svg")`,
+ },
+ {
+ type: "view-source HTTP",
+ testURL: "view-source:http://example.com/",
+ img_url: `url("chrome://global/skin/icons/connection-mixed-active-loaded.svg")`,
+ },
+ {
+ type: "view-source HTTPS",
+ testURL: "view-source:https://example.com/",
+ img_url: `url("chrome://browser/skin/connection-secure.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..10cd715201
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_HttpsOnlyMode.js
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const HTTPS_ONLY_PERMISSION = "https-only-load-insecure";
+const WEBSITE = scheme => `${scheme}://example.com`;
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ // Site is already HTTPS, so the UI should not be visible.
+ await runTest({
+ name: "No HTTPS-Only UI",
+ initialScheme: "https",
+ initialPermission: 0,
+ permissionScheme: "https",
+ isUiVisible: false,
+ });
+
+ // Site gets upgraded to HTTPS, so the UI should be visible.
+ // Disabling HTTPS-Only Mode through the menulist should reload the page and
+ // set the permission accordingly.
+ await runTest({
+ name: "Disable HTTPS-Only",
+ initialScheme: "http",
+ initialPermission: 0,
+ permissionScheme: "https",
+ isUiVisible: true,
+ selectPermission: 1,
+ expectReload: true,
+ finalScheme: "https",
+ });
+
+ // HTTPS-Only Mode is disabled for this site, so the UI should be visible.
+ // Disabling HTTPS-Only Mode through the menulist should not reload the page
+ // but set the permission accordingly.
+ await runTest({
+ name: "Switch between off states",
+ initialScheme: "http",
+ initialPermission: 1,
+ permissionScheme: "http",
+ isUiVisible: true,
+ selectPermission: 2,
+ expectReload: false,
+ finalScheme: "http",
+ });
+
+ // HTTPS-Only Mode is disabled for this site, so the UI should be visible.
+ // Enabling HTTPS-Only Mode through the menulist should reload and upgrade the
+ // page and set the permission accordingly.
+ await runTest({
+ name: "Enable HTTPS-Only again",
+ initialScheme: "http",
+ initialPermission: 2,
+ permissionScheme: "http",
+ isUiVisible: true,
+ selectPermission: 0,
+ expectReload: true,
+ finalScheme: "https",
+ });
+});
+
+async function runTest(options) {
+ // Set the initial permission
+ setPermission(WEBSITE(options.permissionScheme), options.initialPermission);
+
+ await BrowserTestUtils.withNewTab(
+ WEBSITE(options.initialScheme),
+ async function(browser) {
+ const name = options.name + " | ";
+
+ // Check if the site has the expected scheme
+ is(
+ browser.currentURI.scheme,
+ options.permissionScheme,
+ name + "Expected scheme should match actual scheme"
+ );
+
+ // Open the identity popup.
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ await promisePanelOpen;
+
+ // Check if the HTTPS-Only UI is visible
+ const httpsOnlyUI = document.getElementById(
+ "identity-popup-security-httpsonlymode"
+ );
+ is(
+ gBrowser.ownerGlobal.getComputedStyle(httpsOnlyUI).display != "none",
+ options.isUiVisible,
+ options.isUiVisible
+ ? name + "HTTPS-Only UI should be visible."
+ : name + "HTTPS-Only UI shouldn't be visible."
+ );
+
+ // If it's not visible we can't do much else :)
+ if (!options.isUiVisible) {
+ return;
+ }
+
+ // Check if the value of the menulist matches the initial permission
+ const httpsOnlyMenulist = document.getElementById(
+ "identity-popup-security-httpsonlymode-menulist"
+ );
+ is(
+ parseInt(httpsOnlyMenulist.value, 10),
+ options.initialPermission,
+ name + "Menulist value should match expected permission value."
+ );
+
+ // Select another HTTPS-Only state and potentially wait for the page to reload
+ if (options.expectReload) {
+ const loaded = BrowserTestUtils.browserLoaded(browser);
+ httpsOnlyMenulist.getItemAtIndex(options.selectPermission).doCommand();
+ await loaded;
+ } else {
+ httpsOnlyMenulist.getItemAtIndex(options.selectPermission).doCommand();
+ }
+
+ // Check if the site has the expected scheme
+ is(
+ browser.currentURI.scheme,
+ options.finalScheme,
+ name + "Unexpected scheme after page reloaded."
+ );
+
+ // Check if the permission was sucessfully changed
+ is(
+ getPermission(WEBSITE(options.permissionScheme)),
+ options.selectPermission,
+ name + "Set permission should match the one selected from the menulist."
+ );
+ }
+ );
+
+ // Reset permission
+ Services.perms.removeAll();
+}
+
+function setPermission(url, newValue) {
+ let uri = Services.io.newURI(url);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ if (newValue === 0) {
+ Services.perms.removeFromPrincipal(principal, HTTPS_ONLY_PERMISSION);
+ } else if (newValue === 1) {
+ Services.perms.addFromPrincipal(
+ principal,
+ HTTPS_ONLY_PERMISSION,
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW,
+ Ci.nsIPermissionManager.EXPIRE_NEVER
+ );
+ } else {
+ Services.perms.addFromPrincipal(
+ principal,
+ HTTPS_ONLY_PERMISSION,
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
+ Ci.nsIPermissionManager.EXPIRE_SESSION
+ );
+ }
+}
+
+function getPermission(url) {
+ let uri = Services.io.newURI(url);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ const state = Services.perms.testPermissionFromPrincipal(
+ principal,
+ HTTPS_ONLY_PERMISSION
+ );
+ switch (state) {
+ case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION:
+ return 2; // Off temporarily
+ case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW:
+ return 1; // Off
+ default:
+ return 0; // On
+ }
+}
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js
new file mode 100644
index 0000000000..a76358dc25
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js
@@ -0,0 +1,180 @@
+/* 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 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.defineModuleGetter(
+ this,
+ "SiteDataTestUtils",
+ "resource://testing-common/SiteDataTestUtils.jsm"
+);
+
+async function testClearing(
+ testQuota,
+ testCookies,
+ testURI,
+ origin,
+ subOrigin
+) {
+ // Add some test quota storage.
+ if (testQuota) {
+ await SiteDataTestUtils.addToIndexedDB(origin);
+ await SiteDataTestUtils.addToIndexedDB(subOrigin);
+ }
+
+ // Add some test cookies.
+ if (testCookies) {
+ SiteDataTestUtils.addToCookies(origin, "test1", "1");
+ SiteDataTestUtils.addToCookies(origin, "test2", "2");
+ SiteDataTestUtils.addToCookies(subOrigin, "test3", "1");
+ }
+
+ await BrowserTestUtils.withNewTab(testURI, async function(browser) {
+ // Verify we have added quota storage.
+ if (testQuota) {
+ let usage = await SiteDataTestUtils.getQuotaUsage(origin);
+ Assert.greater(usage, 0, "Should have data for the base origin.");
+
+ usage = await SiteDataTestUtils.getQuotaUsage(subOrigin);
+ Assert.greater(usage, 0, "Should have data for the sub origin.");
+ }
+
+ // Open the identity popup.
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gBrowser.ownerGlobal,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ await promisePanelOpen;
+
+ let clearFooter = document.getElementById(
+ "identity-popup-clear-sitedata-footer"
+ );
+ let clearButton = document.getElementById(
+ "identity-popup-clear-sitedata-button"
+ );
+ TestUtils.waitForCondition(
+ () => !clearFooter.hidden,
+ "The clear data footer is not hidden."
+ );
+
+ let cookiesCleared;
+ if (testCookies) {
+ cookiesCleared = Promise.all([
+ TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted" && subj.name == "test1"
+ ),
+ TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted" && subj.name == "test2"
+ ),
+ TestUtils.topicObserved(
+ "cookie-changed",
+ (subj, data) => data == "deleted" && subj.name == "test3"
+ ),
+ ]);
+ }
+
+ // 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(origin);
+ is(
+ Services.cookies.countCookiesFromHost(uri.host),
+ 0,
+ "Cookies from the base domain should be cleared"
+ );
+ uri = Services.io.newURI(subOrigin);
+ is(
+ Services.cookies.countCookiesFromHost(uri.host),
+ 0,
+ "Cookies from the sub domain should be cleared"
+ );
+ }
+
+ // Check that quota storage was deleted.
+ if (testQuota) {
+ await TestUtils.waitForCondition(async () => {
+ let usage = await SiteDataTestUtils.getQuotaUsage(origin);
+ return usage == 0;
+ }, "Should have no data for the base origin.");
+
+ let usage = await SiteDataTestUtils.getQuotaUsage(subOrigin);
+ is(usage, 0, "Should have no data for the sub origin.");
+ }
+
+ // Open the site identity panel again to check that the button isn't shown anymore.
+ promisePanelOpen = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popupshown"
+ );
+ gIdentityHandler._identityBox.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 removing cookies.
+add_task(async function test_ClearCookies() {
+ await testClearing(false, true, TEST_ORIGIN, TEST_ORIGIN, TEST_SUB_ORIGIN);
+});
+
+// Test removing both.
+add_task(async function test_ClearCookiesAndSiteData() {
+ await testClearing(true, true, TEST_ORIGIN, TEST_ORIGIN, TEST_SUB_ORIGIN);
+});
+
+// Test IDN Domains
+add_task(async function test_IDN_ClearCookiesAndSiteData() {
+ await testClearing(
+ true,
+ true,
+ TEST_IDN_ORIGIN,
+ TEST_PUNY_ORIGIN,
+ TEST_PUNY_SUB_ORIGIN
+ );
+});
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..d261d0a8fc
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_custom_roots.js
@@ -0,0 +1,81 @@
+/* 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._identityBox.click();
+ await promisePanelOpen;
+ let customRootWarning = document.getElementById(
+ "identity-popup-security-decription-custom-root"
+ );
+ ok(
+ BrowserTestUtils.is_visible(customRootWarning),
+ "custom root warning is visible"
+ );
+
+ let securityView = document.getElementById("identity-popup-securityView");
+ let shown = BrowserTestUtils.waitForEvent(securityView, "ViewShown");
+ document.getElementById("identity-popup-security-expander").click();
+ await shown;
+
+ let subPanelInfo = document.getElementById(
+ "identity-popup-content-verifier-unknown"
+ );
+ ok(
+ BrowserTestUtils.is_visible(subPanelInfo),
+ "custom root warning in sub panel is visible"
+ );
+ });
+});
+
+// Also check that there are conditions where this isn't shown.
+add_task(async function test_http() {
+ await BrowserTestUtils.withNewTab("http://example.com", async function() {
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ await promisePanelOpen;
+ let customRootWarning = document.getElementById(
+ "identity-popup-security-decription-custom-root"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(customRootWarning),
+ "custom root warning is hidden"
+ );
+
+ let securityView = document.getElementById("identity-popup-securityView");
+ let shown = BrowserTestUtils.waitForEvent(securityView, "ViewShown");
+ document.getElementById("identity-popup-security-expander").click();
+ await shown;
+
+ let subPanelInfo = document.getElementById(
+ "identity-popup-content-verifier-unknown"
+ );
+ ok(
+ BrowserTestUtils.is_hidden(subPanelInfo),
+ "custom root warning in sub panel is hidden"
+ );
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js b/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js
new file mode 100644
index 0000000000..37d3961669
--- /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._identityBox,
+ "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._identityBox, {});
+ await shown;
+ isnot(
+ Services.focus.focusedElement,
+ document.getElementById("identity-popup-security-expander")
+ );
+ });
+});
+
+// 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-expander")
+ );
+ });
+});
+
+// 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-expander")
+ );
+
+ // 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..c5cdcf96cc
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identity_UI.js
@@ -0,0 +1,166 @@
+/* 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",
+ location: "http://test1.example.org/",
+ hostForDisplay: "test1.example.org",
+ },
+ {
+ name: "view-source",
+ location: "view-source:http://example.com/",
+ newURI: "http://example.com/",
+ hostForDisplay: "example.com",
+ },
+ {
+ name: "normal HTTPS",
+ location: "https://example.com/",
+ hostForDisplay: "example.com",
+ },
+ {
+ name: "IDN subdomain",
+ location: "http://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/",
+ hostForDisplay: "sub1." + idnDomain,
+ },
+ {
+ name: "subdomain with port",
+ location: "http://sub1.test1.example.org:8000/",
+ hostForDisplay: "sub1.test1.example.org",
+ },
+ {
+ name: "subdomain HTTPS",
+ location: "https://test1.example.com/",
+ hostForDisplay: "test1.example.com",
+ },
+ {
+ name: "view-source HTTPS",
+ location: "view-source:https://example.com/",
+ newURI: "https://example.com/",
+ hostForDisplay: "example.com",
+ },
+ {
+ name: "IP address",
+ location: "http://127.0.0.1:8888/",
+ hostForDisplay: "127.0.0.1",
+ },
+ {
+ name: "about:certificate",
+ location:
+ "about:certificate?cert=MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO&cert=1pVzllk7ZFHzANBgkqhkiG9w0BAQ",
+ hostForDisplay: "about:certificate",
+ },
+ {
+ name: "about:reader",
+ location: "about:reader?url=http://example.com",
+ hostForDisplay: "example.com",
+ },
+ {
+ name: "chrome:",
+ location: "chrome://global/skin/in-content/info-pages.css",
+ hostForDisplay: "chrome://global/skin/in-content/info-pages.css",
+ },
+];
+
+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.loadURI(gBrowser.selectedBrowser, currentTest.location);
+ await loaded;
+ await popupHidden;
+ ok(
+ !gIdentityHandler._identityPopup ||
+ BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup),
+ "Control Center is hidden"
+ );
+
+ // Sanity check other values, and the value of gIdentityHandler.getHostForDisplay()
+ is(
+ gIdentityHandler._uri.spec,
+ currentTest.newURI || currentTest.location,
+ "location matches for test " + testDesc
+ );
+ // getHostForDisplay can't be called for all modes
+ if (currentTest.hostForDisplay !== null) {
+ is(
+ gIdentityHandler.getHostForDisplay(),
+ currentTest.hostForDisplay,
+ "hostForDisplay matches for test " + testDesc
+ );
+ }
+
+ // Open the Control Center and make sure it closes after nav (Bug 1207542).
+ let popupShown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ info("Waiting for the Control Center to be shown");
+ await popupShown;
+ ok(
+ !BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup),
+ "Control Center is visible"
+ );
+ let displayedHost = currentTest.hostForDisplay || currentTest.location;
+ ok(
+ gIdentityHandler._identityPopupMainViewHeaderLabel.textContent.includes(
+ displayedHost
+ ),
+ "identity UI header shows the host for test " + testDesc
+ );
+
+ // 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"
+ );
+ gBrowser.ownerDocument
+ .querySelector("#identity-popup-security-expander")
+ .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..fbd908c760
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_iframe_navigation.js
@@ -0,0 +1,107 @@
+/* -*- 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";
+const INSECURE_TEST_URI = SECURE_TEST_URI.replace("https://", "http://");
+
+// From a secure URI, navigate the iframe to about:blank (should still be
+// secure).
+add_task(async function() {
+ let uri = SECURE_TEST_URI + "#blank";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "verifiedDomain", "identity should be secure before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode = window.document.getElementById("identity-box")
+ .className;
+ is(newIdentityMode, "verifiedDomain", "identity should be secure after");
+ });
+});
+
+// From a secure URI, navigate the iframe to an insecure URI (http://...)
+// (mixed active content should be blocked, should still be secure).
+add_task(async function() {
+ let uri = SECURE_TEST_URI + "#insecure";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "verifiedDomain", "identity should be secure before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode = window.document.getElementById("identity-box")
+ .classList;
+ ok(
+ newIdentityMode.contains("mixedActiveBlocked"),
+ "identity should be blocked mixed active content after"
+ );
+ ok(
+ newIdentityMode.contains("verifiedDomain"),
+ "identity should still contain 'verifiedDomain'"
+ );
+ is(newIdentityMode.length, 2, "shouldn't have any other identity states");
+ });
+});
+
+// From an insecure URI (http://..), navigate the iframe to about:blank (should
+// still be insecure).
+add_task(async function() {
+ let uri = INSECURE_TEST_URI + "#blank";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "notSecure", "identity should be 'not secure' before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode = window.document.getElementById("identity-box")
+ .className;
+ is(newIdentityMode, "notSecure", "identity should be 'not secure' after");
+ });
+});
+
+// From an insecure URI (http://..), navigate the iframe to a secure URI
+// (https://...) (should still be insecure).
+add_task(async function() {
+ let uri = INSECURE_TEST_URI + "#secure";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "notSecure", "identity should be 'not secure' before");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.postMessage("", "*"); // This kicks off the navigation.
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.body.classList.contains("running");
+ });
+ });
+
+ let newIdentityMode = window.document.getElementById("identity-box")
+ .className;
+ is(newIdentityMode, "notSecure", "identity should be 'not secure' after");
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_ignore_same_page_navigation.js b/browser/base/content/test/siteIdentity/browser_ignore_same_page_navigation.js
new file mode 100644
index 0000000000..98941521f4
--- /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.loadURI(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..c465745119
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mcb_redirect.js
@@ -0,0 +1,359 @@
+/*
+ * 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",
+ "http://example.com"
+);
+const PREF_INSECURE_ICON = "security.insecure_connection_icon.enabled";
+
+var origBlockActive;
+var origBlockDisplay;
+var origUpgradeDisplay;
+var origInsecurePref;
+var gTestBrowser = null;
+
+// ------------------------ Helper Functions ---------------------
+
+registerCleanupFunction(function() {
+ // Set preferences back to their original values
+ Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
+ Services.prefs.setBoolPref(PREF_DISPLAY, origBlockDisplay);
+ Services.prefs.setBoolPref(PREF_DISPLAY_UPGRADE, origUpgradeDisplay);
+ Services.prefs.setBoolPref(PREF_INSECURE_ICON, origInsecurePref);
+
+ // Make sure we are online again
+ Services.io.offline = false;
+});
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+ finish();
+}
+
+// ------------------------ Test 1 ------------------------------
+
+function test1() {
+ Services.prefs.setBoolPref(PREF_INSECURE_ICON, false);
+
+ var url = HTTPS_TEST_ROOT + "test_mcb_redirect.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkUIForTest1
+ );
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+}
+
+function testInsecure1() {
+ Services.prefs.setBoolPref(PREF_INSECURE_ICON, true);
+
+ var url = HTTPS_TEST_ROOT + "test_mcb_redirect.html";
+ BrowserTestUtils.browserLoaded(gTestBrowser, false, url).then(
+ checkUIForTest1
+ );
+ BrowserTestUtils.loadURI(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.loadURI(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.loadURI(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.loadURI(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.loadURI(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.loadURI(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.loadURI(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.loadURI(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.loadURI(gTestBrowser, url);
+}
+
+function checkLoadEventForTest9() {
+ SpecialPowers.spawn(gTestBrowser, [], async function() {
+ var expected = "image blocked";
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("mctestdiv").innerHTML == expected,
+ "OK: Expected result in innerHTML for Test9!"
+ );
+ }).then(() => {
+ // Go back online
+ Services.io.offline = false;
+ cleanUpAfterTests();
+ });
+}
+
+// ------------------------ SETUP ------------------------------
+
+function test() {
+ // Performing async calls, e.g. 'onload', we have to wait till all of them finished
+ waitForExplicitFinish();
+
+ // Store original preferences so we can restore settings after testing
+ origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE);
+ origBlockDisplay = Services.prefs.getBoolPref(PREF_DISPLAY);
+ origUpgradeDisplay = Services.prefs.getBoolPref(PREF_DISPLAY_UPGRADE);
+ origInsecurePref = Services.prefs.getBoolPref(PREF_INSECURE_ICON);
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+ Services.prefs.setBoolPref(PREF_DISPLAY, true);
+ Services.prefs.setBoolPref(PREF_DISPLAY_UPGRADE, false);
+
+ var newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop();
+
+ executeSoon(testInsecure1);
+}
diff --git a/browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js b/browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js
new file mode 100644
index 0000000000..7bca58bf50
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixedContentFramesOnHttp.js
@@ -0,0 +1,36 @@
+/*
+ * 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",
+ "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..069889f290
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_mixedContentFromOnunload.js
@@ -0,0 +1,66 @@
+/*
+ * 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",
+ "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",
+ "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.loadURI(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.loadURI(browser, url);
+ await BrowserTestUtils.browserLoaded(browser);
+ url = HTTPS_TEST_ROOT_2 + "file_mixedContentFromOnunload_test2.html";
+ BrowserTestUtils.loadURI(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..6d681b4621
--- /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("connection-mixed-passive-loaded.svg");
+
+ // check that the crossed out icon is shown when disabling mixed content protection
+ gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ checkIdentityPopup("connection-mixed-active-loaded.svg");
+
+ // check that a warning is shown even without mixed content
+ BrowserTestUtils.loadURI(
+ gBrowser.selectedBrowser,
+ "https://self-signed.example.com"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ checkIdentityPopup("connection-mixed-passive-loaded.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..cf3444d919
--- /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.loadURI(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.loadURI(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.loadURI(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..cf33d75844
--- /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..9f492284ac
--- /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..4a92883de0
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_navigation_failures.js
@@ -0,0 +1,177 @@
+/* -*- 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.loadURI(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,
+ "unknownIdentity",
+ "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.loadURI(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.securityInfo.QueryInterface(
+ 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 certService = Cc["@mozilla.org/security/local-cert-service;1"].getService(
+ Ci.nsILocalCertService
+ );
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+
+ let cert = await new Promise((resolve, reject) => {
+ certService.getOrCreateCert("broken-tls-server", {
+ handleCert(c, rv) {
+ if (!Components.isSuccessCode(rv)) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+ // Start a server and trust its certificate.
+ let server = startServer(cert);
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ "localhost",
+ server.port,
+ cert,
+ overrideBits,
+ 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.loadURI(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, "unknownIdentity", "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..7c3034ddf4
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js
@@ -0,0 +1,80 @@
+/* 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) {
+ ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", this);
+ 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..3b91e718a8
--- /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_WHITELIST = "dom.securecontext.whitelist_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_WHITELIST, true]] });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_URL
+ ).catch(Cu.reportError);
+ 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..5d4d5ecf82
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_no_mcb_on_http_site.js
@@ -0,0 +1,126 @@
+/*
+ * 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",
+ "http://example.com"
+);
+
+var gTestBrowser = null;
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+}
+
+add_task(async function init() {
+ 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.loadURI(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.loadURI(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..1c0d3e2dc5
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that an insecure resource routed over a secure transport is considered
+// insecure in terms of the site identity panel. We achieve this by running an
+// HTTP-over-TLS "proxy" and having Firefox request an http:// URI over it.
+
+/**
+ * Tests that the page info dialog "security" section labels a
+ * connection as unencrypted and does not show certificate.
+ * @param {string} uri - URI of the page to test with.
+ */
+async function testPageInfoNotEncrypted(uri) {
+ let pageInfo = BrowserPageInfo(uri, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "load");
+ let pageInfoDoc = pageInfo.document;
+ let securityTab = pageInfoDoc.getElementById("securityTab");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(securityTab),
+ "Security tab should be visible."
+ );
+
+ let secLabel = pageInfoDoc.getElementById("security-technical-shortform");
+ await TestUtils.waitForCondition(
+ () => secLabel.value == "Connection Not Encrypted",
+ "pageInfo 'Security Details' should show not encrypted"
+ );
+
+ let viewCertBtn = pageInfoDoc.getElementById("security-view-cert");
+ ok(
+ viewCertBtn.collapsed,
+ "pageInfo 'View Cert' button should not be visible"
+ );
+ pageInfo.close();
+}
+
+// But first, a quick test that we don't incorrectly treat a
+// blob:https://example.com URI as secure.
+add_task(async function() {
+ let uri =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+ await BrowserTestUtils.withNewTab(uri, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let debug = { hello: "world" };
+ let blob = new Blob([JSON.stringify(debug, null, 2)], {
+ type: "application/json",
+ });
+ let blobUri = URL.createObjectURL(blob);
+ content.document.location = blobUri;
+ });
+ await BrowserTestUtils.browserLoaded(browser);
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "localResource", "identity should be 'localResource'");
+ await testPageInfoNotEncrypted(uri);
+ });
+});
+
+// This server pretends to be a HTTP over TLS proxy. It isn't really, but this
+// is sufficient for the purposes of this test.
+function startServer(cert) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let input, output;
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ let connectionInfo = transport.securityInfo.QueryInterface(
+ 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 certService = Cc["@mozilla.org/security/local-cert-service;1"].getService(
+ Ci.nsILocalCertService
+ );
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+
+ let cert = await new Promise((resolve, reject) => {
+ certService.getOrCreateCert("http-over-https-proxy", {
+ handleCert(c, rv) {
+ if (!Components.isSuccessCode(rv)) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+ // Start the proxy and configure Firefox to trust its certificate.
+ let server = startServer(cert);
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ "localhost",
+ server.port,
+ cert,
+ overrideBits,
+ 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.import(
+ "resource://testing-common/MockRegistrar.jsm"
+ );
+ 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.
+ await BrowserTestUtils.withNewTab("http://example.com/", async browser => {
+ let identityMode = window.document.getElementById("identity-box").className;
+ is(identityMode, "notSecure", "identity should be 'not secure'");
+
+ await testPageInfoNotEncrypted("http://example.com");
+ });
+});
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..003b57d745
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/file_bug906190.sjs
@@ -0,0 +1,17 @@
+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/head.js b/browser/base/content/test/siteIdentity/head.js
new file mode 100644
index 0000000000..04e0f52228
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/head.js
@@ -0,0 +1,412 @@
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+function openIdentityPopup() {
+ gIdentityHandler._initializePopup();
+ let mainView = document.getElementById("identity-popup-mainView");
+ let viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+ gIdentityHandler._identityBox.click();
+ return viewShown;
+}
+
+/**
+ * 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.loadURI(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+// Compares the security state of the page with what is expected
+function isSecurityState(browser, expectedState) {
+ let ui = browser.securityUI;
+ if (!ui) {
+ ok(false, "No security UI to get the security state");
+ return;
+ }
+
+ const wpl = Ci.nsIWebProgressListener;
+
+ // determine the security state
+ let isSecure = ui.state & wpl.STATE_IS_SECURE;
+ let isBroken = ui.state & wpl.STATE_IS_BROKEN;
+ let isInsecure = ui.state & wpl.STATE_IS_INSECURE;
+
+ let actualState;
+ if (isSecure && !(isBroken || isInsecure)) {
+ actualState = "secure";
+ } else if (isBroken && !(isSecure || isInsecure)) {
+ actualState = "broken";
+ } else if (isInsecure && !(isSecure || isBroken)) {
+ actualState = "insecure";
+ } else {
+ actualState = "unknown";
+ }
+
+ is(
+ expectedState,
+ actualState,
+ "Expected state " +
+ expectedState +
+ " and the actual state is " +
+ actualState +
+ "."
+ );
+}
+
+/**
+ * Test the state of the identity box and control center to make
+ * sure they are correctly showing the expected mixed content states.
+ *
+ * @note The checks are done synchronously, but new code should wait on the
+ * returned Promise object to ensure the identity panel has closed.
+ * Bug 1221114 is filed to fix the existing code.
+ *
+ * @param tabbrowser
+ * @param Object states
+ * MUST include the following properties:
+ * {
+ * activeLoaded: true|false,
+ * activeBlocked: true|false,
+ * passiveLoaded: true|false,
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished and the identity panel has closed.
+ */
+async function assertMixedContentBlockingState(tabbrowser, states = {}) {
+ if (
+ !tabbrowser ||
+ !("activeLoaded" in states) ||
+ !("activeBlocked" in states) ||
+ !("passiveLoaded" in states)
+ ) {
+ throw new Error(
+ "assertMixedContentBlockingState requires a browser and a states object"
+ );
+ }
+
+ let { passiveLoaded, activeLoaded, activeBlocked } = states;
+ let { gIdentityHandler } = tabbrowser.ownerGlobal;
+ let doc = tabbrowser.ownerDocument;
+ let identityBox = gIdentityHandler._identityBox;
+ let classList = identityBox.classList;
+ let identityIcon = doc.getElementById("identity-icon");
+ let identityIconImage = tabbrowser.ownerGlobal
+ .getComputedStyle(identityIcon)
+ .getPropertyValue("list-style-image");
+
+ let stateSecure =
+ gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
+ let stateBroken =
+ gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
+ let stateInsecure =
+ gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_INSECURE;
+ let stateActiveBlocked =
+ gIdentityHandler._state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
+ let stateActiveLoaded =
+ gIdentityHandler._state &
+ Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
+ let statePassiveLoaded =
+ gIdentityHandler._state &
+ Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
+
+ is(
+ activeBlocked,
+ !!stateActiveBlocked,
+ "Expected state for activeBlocked matches UI state"
+ );
+ is(
+ activeLoaded,
+ !!stateActiveLoaded,
+ "Expected state for activeLoaded matches UI state"
+ );
+ is(
+ passiveLoaded,
+ !!statePassiveLoaded,
+ "Expected state for passiveLoaded matches UI state"
+ );
+
+ if (stateInsecure) {
+ const insecureConnectionIcon = Services.prefs.getBoolPref(
+ "security.insecure_connection_icon.enabled"
+ );
+ if (!insecureConnectionIcon) {
+ // HTTP request, there should be no MCB classes for the identity box and the non secure icon
+ // should always be visible regardless of MCB state.
+ ok(classList.contains("unknownIdentity"), "unknownIdentity on HTTP page");
+ ok(
+ BrowserTestUtils.is_visible(identityIcon),
+ "information icon should be still visible"
+ );
+ } else {
+ // HTTP request, there should be a broken padlock shown always.
+ ok(classList.contains("notSecure"), "notSecure on HTTP page");
+ ok(
+ !BrowserTestUtils.is_hidden(identityIcon),
+ "information icon should be visible"
+ );
+ }
+
+ ok(!classList.contains("mixedActiveContent"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedActiveBlocked"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedDisplayContent"), "No MCB icon on HTTP page");
+ ok(
+ !classList.contains("mixedDisplayContentLoadedActiveBlocked"),
+ "No MCB icon on HTTP page"
+ );
+ } else {
+ // Make sure the identity box UI has the correct mixedcontent states and icons
+ is(
+ classList.contains("mixedActiveContent"),
+ activeLoaded,
+ "identityBox has expected class for activeLoaded"
+ );
+ is(
+ classList.contains("mixedActiveBlocked"),
+ activeBlocked && !passiveLoaded,
+ "identityBox has expected class for activeBlocked && !passiveLoaded"
+ );
+ is(
+ classList.contains("mixedDisplayContent"),
+ passiveLoaded && !(activeLoaded || activeBlocked),
+ "identityBox has expected class for passiveLoaded && !(activeLoaded || activeBlocked)"
+ );
+ is(
+ classList.contains("mixedDisplayContentLoadedActiveBlocked"),
+ passiveLoaded && activeBlocked,
+ "identityBox has expected class for passiveLoaded && activeBlocked"
+ );
+
+ ok(
+ !BrowserTestUtils.is_hidden(identityIcon),
+ "information icon should be visible"
+ );
+ if (activeLoaded) {
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/connection-mixed-active-loaded.svg")',
+ "Using active loaded icon"
+ );
+ }
+ if (activeBlocked && !passiveLoaded) {
+ is(
+ identityIconImage,
+ 'url("chrome://browser/skin/connection-secure.svg")',
+ "Using active blocked icon"
+ );
+ }
+ if (passiveLoaded && !(activeLoaded || activeBlocked)) {
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/connection-mixed-passive-loaded.svg")',
+ "Using passive loaded icon"
+ );
+ }
+ if (passiveLoaded && activeBlocked) {
+ is(
+ identityIconImage,
+ 'url("chrome://global/skin/icons/connection-mixed-passive-loaded.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._identityBox.click();
+ await promisePanelOpen;
+ let popupAttr = doc
+ .getElementById("identity-popup")
+ .getAttribute("mixedcontent");
+ let bodyAttr = doc
+ .getElementById("identity-popup-securityView-body")
+ .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("background-image");
+ let securityContentBG = tabbrowser.ownerGlobal
+ .getComputedStyle(
+ document
+ .getElementById("identity-popup-mainView")
+ .getElementsByClassName("identity-popup-security-connection")[0]
+ )
+ .getPropertyValue("background-image");
+
+ if (stateInsecure) {
+ is(
+ securityViewBG,
+ 'url("chrome://global/skin/icons/connection-mixed-active-loaded.svg")',
+ "CC using 'not secure' icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/connection-mixed-active-loaded.svg")',
+ "CC using 'not secure' icon"
+ );
+ }
+
+ if (stateSecure) {
+ is(
+ securityViewBG,
+ 'url("chrome://browser/skin/connection-secure.svg")',
+ "CC using secure icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://browser/skin/connection-secure.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/connection-mixed-passive-loaded.svg")',
+ "CC using degraded icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://global/skin/icons/connection-mixed-passive-loaded.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://browser/skin/connection-secure.svg")',
+ "CC using degraded icon"
+ );
+ is(
+ securityContentBG,
+ 'url("chrome://browser/skin/connection-secure.svg")',
+ "CC using degraded icon"
+ );
+ }
+ }
+
+ if (activeLoaded || activeBlocked || passiveLoaded) {
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "ViewShown"
+ );
+ doc.getElementById("identity-popup-security-expander").click();
+ await promiseViewShown;
+ is(
+ Array.prototype.filter.call(
+ doc
+ .getElementById("identity-popup-securityView")
+ .querySelectorAll(".identity-popup-mcb-learn-more"),
+ element => !BrowserTestUtils.is_hidden(element)
+ ).length,
+ 1,
+ "The 'Learn more' link should be visible once."
+ );
+ }
+
+ if (gIdentityHandler._identityPopup.state != "closed") {
+ let hideEvent = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ info("Hiding identity popup");
+ gIdentityHandler._identityPopup.hidePopup();
+ await hideEvent;
+ }
+}
+
+async function loadBadCertPage(url) {
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(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/siteIdentity/iframe_navigation.html b/browser/base/content/test/siteIdentity/iframe_navigation.html
new file mode 100644
index 0000000000..7df818154b
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/iframe_navigation.html
@@ -0,0 +1,43 @@
+<!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 =
+ "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/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..a7e3a31021
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_mcb_redirect.sjs
@@ -0,0 +1,22 @@
+function handleRequest(request, response) {
+ var page = "<!DOCTYPE html><html><body>bug 418354 and bug 1082837</body></html>";
+
+ if (request.queryString === "script") {
+ var redirect = "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") {
+ var 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") {
+ var redirect = "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") {
+ var 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..015d0ce2ce
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_for_loopback.html
@@ -0,0 +1,55 @@
+<!-- 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");
+ 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..9715d526bf
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_for_onions.html
@@ -0,0 +1,28 @@
+<!-- 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>
+ 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..68a6954ccd
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css
@@ -0,0 +1,10 @@
+@font-face {
+ font-family: testFont;
+ src: url(http://example.com/browser/devtools/client/fontinspector/test/browser_font.woff);
+}
+body {
+ font-family: Arial;
+}
+div {
+ font-family: testFont;
+}
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html
new file mode 100644
index 0000000000..7b39be064c
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 2 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ async function checkLoadStates() {
+ let state = await SpecialPowers.getSecurityState(window);
+
+ var loadedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http font";
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that includes http font
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css
new file mode 100644
index 0000000000..3ac6c87a6b
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css
@@ -0,0 +1 @@
+@import url(http://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font.css);
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html
new file mode 100644
index 0000000000..3da31592dd
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 3 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_font2.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ async function checkLoadStates() {
+ let state = await SpecialPowers.getSecurityState(window);
+
+ var loadedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page ";
+ newValue += "with https css that imports another http css which includes http font";
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that imports another http css which includes http font
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css
new file mode 100644
index 0000000000..d045e21ba0
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css
@@ -0,0 +1,3 @@
+#testDiv {
+ background: url(http://example.com/tests/image/test/mochitest/blue.png)
+}
diff --git a/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html
new file mode 100644
index 0000000000..10aa281959
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/siteIdentity/test_no_mcb_on_http_site_img.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ async function checkLoadStates() {
+ let state = await SpecialPowers.getSecurityState(window);
+
+ var loadedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay =
+ !!(state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http image";
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that includes http image
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/startup/.eslintrc.js b/browser/base/content/test/startup/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/startup/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/startup/browser.ini b/browser/base/content/test/startup/browser.ini
new file mode 100644
index 0000000000..00ee1d9ed2
--- /dev/null
+++ b/browser/base/content/test/startup/browser.ini
@@ -0,0 +1,2 @@
+[browser_preXULSkeletonUIRegistry.js]
+skip-if = !(os == 'win' && os_version == '10.0') # We only enable the skele UI on Win10 \ No newline at end of file
diff --git a/browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js b/browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js
new file mode 100644
index 0000000000..347253aeb6
--- /dev/null
+++ b/browser/base/content/test/startup/browser_preXULSkeletonUIRegistry.js
@@ -0,0 +1,137 @@
+ChromeUtils.defineModuleGetter(
+ this,
+ "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm"
+);
+
+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.jsm, 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.jsm 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.setBoolPref("browser.tabs.drawInTitlebar", 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");
+
+ 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",
+ ];
+
+ // 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/.eslintrc.js b/browser/base/content/test/static/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/static/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/static/browser.ini b/browser/base/content/test/static/browser.ini
new file mode 100644
index 0000000000..09e94fedea
--- /dev/null
+++ b/browser/base/content/test/static/browser.ini
@@ -0,0 +1,19 @@
+[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 and debug builds.
+skip-if = asan || tsan || debug
+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
+[browser_parsable_css.js]
+support-files =
+ dummy_page.html
+[browser_parsable_script.js]
+skip-if = ccov && os == 'linux' # https://bugzilla.mozilla.org/show_bug.cgi?id=1608081
+[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..109a4a772a
--- /dev/null
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -0,0 +1,1000 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Note to run this test similar to try server, you need to run:
+// ./mach package
+// ./mach mochitest --appname dist <path to test>
+
+// Slow on asan builds.
+requestLongerTimeout(5);
+
+var isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
+
+// This list should contain only path prefixes. It is meant to stop the test
+// from reporting things that *are* referenced, but for which the test can't
+// find any reference because the URIs are constructed programatically.
+// If you need to whitelist specific files, please use the 'whitelist' object.
+var gExceptionPaths = [
+ "chrome://browser/content/defaultthemes/",
+ "resource://app/defaults/settings/blocklists/",
+ "resource://app/defaults/settings/security-state/",
+ "resource://app/defaults/settings/main/",
+ "resource://app/defaults/settings/pinning/",
+ "resource://app/defaults/preferences/",
+ "resource://gre/modules/commonjs/",
+ "resource://gre/defaults/pref/",
+
+ // These resources are referenced using relative paths from html files.
+ "resource://payments/",
+
+ // 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/",
+
+ // toolkit/components/pdfjs/content/build/pdf.js
+ "resource://pdf.js/web/images/",
+
+ // Exclude all the metadata paths under the country metadata folder because these
+ // paths will be concatenated in FormAutofillUtils.jsm based on different country/region.
+ "resource://formautofill/addressmetadata/",
+
+ // Exclude all search-extensions because they aren't referenced by filename
+ "resource://search-extensions/",
+
+ // Exclude all services-automation because they are used through webdriver
+ "resource://gre/modules/services-automation/",
+ "resource://services-automation/ServicesAutomation.jsm",
+];
+
+// 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/");
+}
+
+// Each whitelist entry should have a comment indicating which file is
+// referencing the whitelisted file in a way that the test can't detect, or a
+// bug number to remove or use the file if it is indeed currently unreferenced.
+var whitelist = [
+ // pocket/content/panels/tmpl/loggedoutvariants/variant_a.handlebars
+ { file: "chrome://pocket/content/panels/img/glyph.svg" },
+
+ // toolkit/components/pdfjs/content/PdfStreamConverter.jsm
+ { file: "chrome://pdf.js/locale/chrome.properties" },
+ { file: "chrome://pdf.js/locale/viewer.properties" },
+
+ // security/manager/pki/resources/content/device_manager.js
+ { file: "chrome://pippki/content/load_device.xhtml" },
+
+ // The l10n build system can't package string files only for some platforms.
+ // See bug 1339424 for why this is hard to fix.
+ {
+ file: "chrome://global/locale/fallbackMenubar.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/localization/en-US/toolkit/printing/printDialogs.ftl",
+ platforms: ["macosx"],
+ },
+
+ // 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,
+ },
+
+ // SpiderMonkey parser API, currently unused in browser/ and toolkit/
+ { file: "resource://gre/modules/reflect.jsm" },
+
+ // extensions/pref/autoconfig/src/nsReadConfig.cpp
+ { file: "resource://gre/defaults/autoconfig/prefcalls.js" },
+
+ // modules/libpref/Preferences.cpp
+ { file: "resource://gre/greprefs.js" },
+
+ // layout/mathml/nsMathMLChar.cpp
+ { file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties" },
+ { file: "resource://gre/res/fonts/mathfontUnicode.properties" },
+
+ // 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/build/pdf.sandbox.external.js" },
+ { file: "resource://pdf.js/build/pdf.scripting.js" },
+ { file: "resource://pdf.js/web/debugger.js" },
+
+ // resource://app/modules/translation/TranslationContentHandler.jsm
+ { file: "resource://app/modules/translation/BingTranslator.jsm" },
+ { file: "resource://app/modules/translation/GoogleTranslator.jsm" },
+ { file: "resource://app/modules/translation/YandexTranslator.jsm" },
+
+ // Starting from here, files in the whitelist are bugs that need fixing.
+ // Bug 1339424 (wontfix?)
+ {
+ file: "chrome://browser/locale/taskbar.properties",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1619090 to clean up platform-specific crypto
+ {
+ file: "resource://gre/modules/OSCrypto.jsm",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1344267
+ { file: "chrome://marionette/content/test.xhtml" },
+ { file: "chrome://marionette/content/test_dialog.properties" },
+ { file: "chrome://marionette/content/test_dialog.xhtml" },
+ { file: "chrome://marionette/content/test_menupopup.xhtml" },
+ // Bug 1348559
+ { file: "chrome://pippki/content/resetpassword.xhtml" },
+ // Bug 1337345
+ { file: "resource://gre/modules/Manifest.jsm" },
+ // Bug 1356045
+ { file: "chrome://global/content/test-ipc.xhtml" },
+ // Bug 1378173 (warning: still used by devtools)
+ { file: "resource://gre/modules/Promise.jsm" },
+ // 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" },
+
+ // Bug 1559554
+ { file: "chrome://browser/content/aboutlogins/aboutLoginsUtils.js" },
+
+ // Referenced from the screenshots webextension
+ { file: "resource://app/localization/en-US/browser/screenshots.ftl" },
+
+ // services/fxaccounts/RustFxAccount.js
+ { file: "resource://gre/modules/RustFxAccount.js" },
+
+ // dom/media/mediacontrol/MediaControlService.cpp
+ { file: "resource://gre/localization/en-US/dom/media.ftl" },
+];
+
+if (AppConstants.NIGHTLY_BUILD && AppConstants.platform != "win") {
+ // This path is refereneced in nsFxrCommandLineHandler.cpp, which is only
+ // compiled in Windows. Whitelisted this path so that non-Windows builds
+ // can access the FxR UI via --chrome rather than --fxr (which includes VR-
+ // specific functionality)
+ whitelist.push({ file: "chrome://fxr/content/fxrui.html" });
+}
+
+if (AppConstants.platform == "android") {
+ // The l10n build system can't package string files only for some platforms.
+ // Referenced by aboutGlean.html
+ whitelist.push({
+ file: "resource://gre/localization/en-US/toolkit/about/aboutGlean.ftl",
+ });
+}
+
+whitelist = new Set(
+ whitelist
+ .filter(
+ item =>
+ "isFromDevTools" in item == isDevtools &&
+ (!item.skipUnofficial || !AppConstants.MOZILLA_OFFICIAL) &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))
+ )
+ .map(item => item.file)
+);
+
+const ignorableWhitelist = new Set([
+ // The following files are outside of the omni.ja file, so we only catch them
+ // when testing on a non-packaged build.
+
+ // toolkit/mozapps/extensions/nsBlocklistService.js
+ "resource://app/blocklist.xml",
+
+ // dom/media/gmp/GMPParent.cpp
+ "resource://gre/gmp-clearkey/0.1/manifest.json",
+
+ // Bug 1351669 - obsolete test file
+ "resource://gre/res/test.properties",
+]);
+for (let entry of ignorableWhitelist) {
+ whitelist.add(entry);
+}
+
+if (!isDevtools) {
+ // services/sync/modules/main.js
+ whitelist.add("resource://services-sync/service.js");
+ // services/sync/modules/service.js
+ for (let module of [
+ "addons.js",
+ "bookmarks.js",
+ "forms.js",
+ "history.js",
+ "passwords.js",
+ "prefs.js",
+ "tabs.js",
+ "extension-storage.js",
+ ]) {
+ whitelist.add("resource://services-sync/engines/" + module);
+ }
+ // resource://devtools/shared/worker/loader.js,
+ // resource://devtools/shared/builtin-modules.js
+ if (!AppConstants.ENABLE_REMOTE_AGENT) {
+ whitelist.add("resource://gre/modules/jsdebugger.jsm");
+ }
+}
+
+if (AppConstants.MOZ_CODE_COVERAGE) {
+ whitelist.add("chrome://marionette/content/PerTestCoverageUtils.jsm");
+}
+
+const gInterestingCategories = new Set([
+ "agent-style-sheets",
+ "addon-provider-module",
+ "webextension-modules",
+ "webextension-scripts",
+ "webextension-schemas",
+ "webextension-scripts-addon",
+ "webextension-scripts-content",
+ "webextension-scripts-devtools",
+]);
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+);
+var gChromeMap = new Map();
+var gOverrideMap = new Map();
+var gComponentsSet = new Set();
+
+// In this map when the value is a Set of URLs, the file is referenced if any
+// of the files in the Set is referenced.
+// When the value is null, the file is referenced unconditionally.
+// When the value is a string, "whitelist-direct" means that we have not found
+// any reference in the code, but have a matching whitelist entry for this file.
+// "whitelist" means that the file is indirectly whitelisted, ie. a whitelisted
+// file causes this file to be referenced.
+var gReferencesFromCode = new Map();
+
+var resHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+var gResourceMap = [];
+function trackResourcePrefix(prefix) {
+ let uri = Services.io.newURI("resource://" + prefix + "/");
+ gResourceMap.unshift([prefix, resHandler.resolveURI(uri)]);
+}
+trackResourcePrefix("gre");
+trackResourcePrefix("app");
+
+function getBaseUriForChromeUri(chromeUri) {
+ let chromeFile = chromeUri + "gobbledygooknonexistentfile.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("formautofill");
+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.
+ // Same issue with fxmonitor, which is also pref'd off.
+ if (chromeUri === "chrome://report-site-issue/locale/") {
+ gChromeMap.set("chrome://report-site-issue/locale/", true);
+ } else if (chromeUri === "chrome://fxmonitor/locale/") {
+ gChromeMap.set("chrome://fxmonitor/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.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 (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);
+ }
+
+ 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|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|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|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;
+ }
+ }
+}
+
+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 &&
+ e.result != Cr.NS_ERROR_NOT_AVAILABLE
+ ) {
+ todo(false, "Failed to check if " + aURI + "exists: " + e);
+ }
+ }
+ return available > 0;
+}
+
+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() {
+ let libxulPath = OS.Constants.Path.libxul;
+ if (AppConstants.platform != "macosx") {
+ libxulPath = OS.Constants.Path.libDir + "/" + libxulPath;
+ }
+ let libxul = await OS.File.read(libxulPath);
+ findChromeUrlsFromArray(libxul, "chrome://");
+ findChromeUrlsFromArray(libxul, "resource://");
+ // Handle NS_LITERAL_STRING.
+ let uint16 = new Uint16Array(libxul.buffer);
+ findChromeUrlsFromArray(uint16, "chrome://");
+ findChromeUrlsFromArray(uint16, "resource://");
+
+ const kCodeExtensions = [
+ ".xul",
+ ".xml",
+ ".xsl",
+ ".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 throttledMapPromises(manifestURIs, parseManifest);
+
+ for (let jsm of Components.manager.getComponentJSMs()) {
+ gReferencesFromCode.set(jsm, 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 throttledMapPromises(allPromises, ([task, uri]) => task(uri));
+
+ // Keep only chrome:// files, and filter out either the devtools paths or
+ // the non-devtools paths:
+ let devtoolsPrefixes = [
+ "chrome://devtools",
+ "resource://devtools/",
+ "resource://devtools-client-jsonview/",
+ "resource://devtools-client-shared/",
+ "resource://app/modules/devtools",
+ "resource://gre/modules/devtools",
+ "resource://app/localization/en-US/startup/aboutDevTools.ftl",
+ "resource://app/localization/en-US/devtools/",
+ ];
+ let hasDevtoolsPrefix = uri =>
+ devtoolsPrefixes.some(prefix => uri.startsWith(prefix));
+ let chromeFiles = [];
+ for (let uri of uris) {
+ uri = convertToCodeURI(uri.spec);
+ if (
+ (uri.startsWith("chrome://") || uri.startsWith("resource://")) &&
+ isDevtools == hasDevtoolsPrefix(uri)
+ ) {
+ chromeFiles.push(uri);
+ }
+ }
+
+ if (isDevtools) {
+ // chrome://devtools/skin/devtools-browser.css is included from browser.xhtml
+ gReferencesFromCode.set(AppConstants.BROWSER_CHROME_URL, null);
+ // devtools' css is currently included from browser.css, see bug 1204810.
+ gReferencesFromCode.set("chrome://browser/skin/browser.css", null);
+ }
+
+ let isUnreferenced = file => {
+ if (gExceptionPaths.some(e => file.startsWith(e))) {
+ return false;
+ }
+ if (gReferencesFromCode.has(file)) {
+ let refs = gReferencesFromCode.get(file);
+ if (refs === null) {
+ return false;
+ }
+ for (let ref of refs) {
+ if (isDevtools) {
+ if (
+ ref.startsWith("resource://app/components/") ||
+ (file.startsWith("chrome://") && ref.startsWith("resource://"))
+ ) {
+ return false;
+ }
+ }
+
+ if (gReferencesFromCode.has(ref)) {
+ let refType = gReferencesFromCode.get(ref);
+ if (
+ refType === null || // unconditionally referenced
+ refType == "whitelist" ||
+ refType == "whitelist-direct"
+ ) {
+ return false;
+ }
+ }
+ }
+ }
+ return !gOverrideMap.has(file) || isUnreferenced(gOverrideMap.get(file));
+ };
+
+ let unreferencedFiles = chromeFiles;
+
+ let removeReferenced = useWhitelist => {
+ let foundReference = false;
+ unreferencedFiles = unreferencedFiles.filter(f => {
+ let rv = isUnreferenced(f);
+ if (rv && f.startsWith("resource://app/")) {
+ rv = isUnreferenced(f.replace("resource://app/", "resource:///"));
+ }
+ if (rv && /^resource:\/\/(?:app|gre)\/components\/[^/]+\.js$/.test(f)) {
+ rv = !gComponentsSet.has(f.replace(/.*\//, ""));
+ }
+ if (!rv) {
+ foundReference = true;
+ if (useWhitelist) {
+ info(
+ "indirectly whitelisted file: " +
+ f +
+ " used from " +
+ listCodeReferences(gReferencesFromCode.get(f))
+ );
+ }
+ gReferencesFromCode.set(f, useWhitelist ? "whitelist" : null);
+ }
+ return rv;
+ });
+ return foundReference;
+ };
+ // First filter out the files that are referenced.
+ while (removeReferenced(false)) {
+ // As long as removeReferenced returns true, some files have been marked
+ // as referenced, so we need to run it again.
+ }
+ // Marked as referenced the files that have been explicitly whitelisted.
+ unreferencedFiles = unreferencedFiles.filter(file => {
+ if (whitelist.has(file)) {
+ whitelist.delete(file);
+ gReferencesFromCode.set(file, "whitelist-direct");
+ return false;
+ }
+ return true;
+ });
+ // Run the process again, this time when more files are marked as referenced,
+ // it's a consequence of the whitelist.
+ while (removeReferenced(true)) {
+ // As long as removeReferenced returns true, we need to run it again.
+ }
+
+ unreferencedFiles.sort();
+
+ if (isDevtools) {
+ // Bug 1351878 - handle devtools resource files
+ unreferencedFiles = unreferencedFiles.filter(file => {
+ if (file.startsWith("resource://")) {
+ info("unreferenced devtools resource file: " + file);
+ return false;
+ }
+ return true;
+ });
+ }
+
+ is(unreferencedFiles.length, 0, "there should be no unreferenced files");
+ for (let file of unreferencedFiles) {
+ let refs = gReferencesFromCode.get(file);
+ if (refs === undefined) {
+ ok(false, "unreferenced file: " + file);
+ } else {
+ let refList = listCodeReferences(refs);
+ let msg = "file only referenced from unreferenced files: " + file;
+ if (refList) {
+ msg += " referenced from " + refList;
+ }
+ ok(false, msg);
+ }
+ }
+
+ for (let file of whitelist) {
+ if (ignorableWhitelist.has(file)) {
+ info("ignored unused whitelist entry: " + file);
+ } else {
+ ok(false, "unused whitelist entry: " + file);
+ }
+ }
+
+ for (let [file, refs] of gReferencesFromCode) {
+ if (
+ isDevtools != devtoolsPrefixes.some(prefix => file.startsWith(prefix))
+ ) {
+ continue;
+ }
+
+ if (
+ (file.startsWith("chrome://") || file.startsWith("resource://")) &&
+ !chromeFileExists(file)
+ ) {
+ // Ignore chrome prefixes that have been automatically expanded.
+ let pathParts =
+ file.match("chrome://([^/]+)/content/([^/.]+).xul") ||
+ file.match("chrome://([^/]+)/skin/([^/.]+).css");
+ if (pathParts && pathParts[1] == pathParts[2]) {
+ continue;
+ }
+
+ // TODO: bug 1349010 - add a whitelist and make this reliable enough
+ // that we could make the test fail when this catches something new.
+ let refList = listCodeReferences(refs);
+ let msg = "missing file: " + file;
+ if (refList) {
+ msg += " referenced from " + refList;
+ }
+ info(msg);
+ }
+ }
+});
diff --git a/browser/base/content/test/static/browser_misused_characters_in_strings.js b/browser/base/content/test/static/browser_misused_characters_in_strings.js
new file mode 100644
index 0000000000..8928a1d4f4
--- /dev/null
+++ b/browser/base/content/test/static/browser_misused_characters_in_strings.js
@@ -0,0 +1,341 @@
+/* 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: "netError.dtd",
+ key: "certerror.introPara2",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "certerror.sts.introPara",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "certerror.expiredCert.introPara",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "certerror.expiredCert.whatCanYouDoAboutIt2",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "certerror.whatShouldIDo.badStsCertExplanation1",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "inadequateSecurityError.longDesc",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "clockSkewError.longDesc",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "certerror.mitm.longDesc",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "certerror.mitm.whatCanYouDoAboutIt3",
+ type: "single-quote",
+ },
+ {
+ file: "netError.dtd",
+ key: "certerror.mitm.sts.whatCanYouDoAboutIt3",
+ type: "single-quote",
+ },
+ {
+ file: "mathfont.properties",
+ key: "operator.\\u002E\\u002E\\u002E.postfix",
+ type: "ellipsis",
+ },
+ {
+ 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: "netError.dtd",
+ key: "inadequateSecurityError.longDesc",
+ type: "single-quote",
+ },
+ {
+ file: "netErrorApp.dtd",
+ key: "securityOverride.warningContent",
+ type: "single-quote",
+ },
+ {
+ file: "pocket.properties",
+ key: "tos",
+ type: "double-quote",
+ },
+ // This string contains HTML markup describing `<link rel="preload">` and therefore
+ // is meant to contain actual double quotes.
+ {
+ file: "features.ftl",
+ key: "experimental-features-web-api-link-preload-description",
+ type: "double-quote",
+ },
+];
+
+/**
+ * Check if an error should be ignored due to matching one of the exceptions
+ * defined in gExceptionsList.
+ *
+ * @param filepath The URI spec of the locale file
+ * @param key The key of the entity that is being checked
+ * @param type The type of error that has been found
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(filepath, key, type) {
+ for (let index in gExceptionsList) {
+ let exceptionItem = gExceptionsList[index];
+ if (
+ filepath.endsWith(exceptionItem.file) &&
+ key == exceptionItem.key &&
+ type == exceptionItem.type
+ ) {
+ gExceptionsList.splice(index, 1);
+ return true;
+ }
+ }
+ return false;
+}
+
+function testForError(filepath, key, str, pattern, type, helpText) {
+ if (str.match(pattern) && !ignoredError(filepath, key, type)) {
+ ok(false, `${filepath} with key=${key} has a misused ${type}. ${helpText}`);
+ }
+}
+
+function testForErrors(filepath, key, str) {
+ testForError(
+ filepath,
+ key,
+ str,
+ /(\w|^)'\w/,
+ "apostrophe",
+ "Strings with apostrophes should use foo\u2019s instead of foo's."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /\w\u2018\w/,
+ "incorrect-apostrophe",
+ "Strings with apostrophes should use foo\u2019s instead of foo\u2018s."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /'.+'/,
+ "single-quote",
+ "Single-quoted strings should use Unicode \u2018foo\u2019 instead of 'foo'."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /"/,
+ "double-quote",
+ 'Double-quoted strings should use Unicode \u201cfoo\u201d instead of "foo".'
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /\.\.\./,
+ "ellipsis",
+ "Strings with an ellipsis should use the Unicode \u2026 character instead of three periods."
+ );
+}
+
+async function getAllTheFiles(extension) {
+ let appDirGreD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let appDirXCurProcD = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ if (appDirGreD.contains(appDirXCurProcD)) {
+ return generateURIsFromDirTree(appDirGreD, [extension]);
+ }
+ if (appDirXCurProcD.contains(appDirGreD)) {
+ return generateURIsFromDirTree(appDirXCurProcD, [extension]);
+ }
+ let urisGreD = await generateURIsFromDirTree(appDirGreD, [extension]);
+ let urisXCurProcD = await generateURIsFromDirTree(appDirXCurProcD, [
+ extension,
+ ]);
+ return Array.from(new Set(urisGreD.concat(urisXCurProcD)));
+}
+
+add_task(async function checkAllTheProperties() {
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let uris = await getAllTheFiles(".properties");
+ ok(
+ uris.length,
+ `Found ${uris.length} .properties files to scan for misused characters`
+ );
+
+ for (let uri of uris) {
+ let bundle = Services.strings.createBundle(uri.spec);
+
+ for (let entity of bundle.getSimpleEnumeration()) {
+ testForErrors(uri.spec, entity.key, entity.value);
+ }
+ }
+});
+
+var checkDTD = async function(aURISpec) {
+ let rawContents = await fetchFile(aURISpec);
+ // The regular expression below is adapted from:
+ // https://hg.mozilla.org/mozilla-central/file/68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8/python/compare-locales/compare_locales/parser.py#l233
+ let entities = rawContents.match(
+ /<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/g
+ );
+ if (!entities) {
+ // Some files have no entities defined.
+ return;
+ }
+ for (let entity of entities) {
+ let [, key, str] = entity.match(
+ /<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/
+ );
+ // The matched string includes the enclosing quotation marks,
+ // we need to slice them off.
+ str = str.slice(1, -1);
+ testForErrors(aURISpec, key, str);
+ }
+};
+
+add_task(async function checkAllTheDTDs() {
+ let uris = await getAllTheFiles(".dtd");
+ ok(
+ uris.length,
+ `Found ${uris.length} .dtd files to scan for misused characters`
+ );
+ for (let uri of uris) {
+ await checkDTD(uri.spec);
+ }
+
+ // This support DTD file supplies a string with a newline to make sure
+ // the regex in checkDTD works correctly for that case.
+ let dtdLocation = gTestPath.replace(
+ /\/[^\/]*$/i,
+ "/bug1262648_string_with_newlines.dtd"
+ );
+ await checkDTD(dtdLocation);
+});
+
+add_task(async function checkAllTheFluents() {
+ let uris = await getAllTheFiles(".ftl");
+ let { FluentParser, Visitor } = ChromeUtils.import(
+ "resource://testing-common/FluentSyntax.jsm",
+ {}
+ );
+
+ class TextElementVisitor extends Visitor {
+ constructor() {
+ super();
+ let domParser = new DOMParser();
+ domParser.forceEnableDTD();
+
+ this.domParser = domParser;
+ this.uri = null;
+ this.id = null;
+ this.attr = null;
+ }
+
+ visitMessage(node) {
+ this.id = node.id.name;
+ this.attr = null;
+ this.genericVisit(node);
+ }
+
+ visitTerm(node) {
+ this.id = node.id.name;
+ this.attr = null;
+ this.genericVisit(node);
+ }
+
+ visitAttribute(node) {
+ this.attr = node.id.name;
+ this.genericVisit(node);
+ }
+
+ get key() {
+ if (this.attr) {
+ return `${this.id}.${this.attr}`;
+ }
+ return this.id;
+ }
+
+ visitTextElement(node) {
+ let stripped_val = this.domParser.parseFromString(node.value, "text/html")
+ .documentElement.textContent;
+ testForErrors(this.uri, this.key, stripped_val);
+ }
+ }
+
+ const ftlParser = new FluentParser({ withSpans: false });
+ const visitor = new TextElementVisitor();
+
+ for (let uri of uris) {
+ let rawContents = await fetchFile(uri.spec);
+ let ast = ftlParser.parse(rawContents);
+
+ visitor.uri = uri.spec;
+ visitor.visit(ast);
+ }
+});
+
+add_task(async function ensureExceptionsListIsEmpty() {
+ is(gExceptionsList.length, 0, "No remaining exceptions exist");
+});
diff --git a/browser/base/content/test/static/browser_parsable_css.js b/browser/base/content/test/static/browser_parsable_css.js
new file mode 100644
index 0000000000..38a8cea3d5
--- /dev/null
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -0,0 +1,489 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' CSS issues to remain, while we
+ * detect newly occurring issues in shipping CSS. It is a list of objects
+ * specifying conditions under which an error should be ignored.
+ *
+ * Every property of the objects in it needs to consist of a regular expression
+ * matching the offending error. If an object has multiple regex criteria, they
+ * ALL need to match an error in order for that error not to cause a test
+ * failure. */
+let whitelist = [
+ // CodeMirror is imported as-is, see bug 1004423.
+ { sourceName: /codemirror\.css$/i, isFromDevTools: true },
+ {
+ sourceName: /devtools\/content\/debugger\/src\/components\/([A-z\/]+).css/i,
+ isFromDevTools: true,
+ },
+ // Highlighter CSS uses a UA-only pseudo-class, see bug 985597.
+ {
+ sourceName: /highlighters\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
+ isFromDevTools: true,
+ },
+ // UA-only media features.
+ {
+ sourceName: /\b(autocomplete-item)\.css$/,
+ errorMessage: /Expected media feature name but found \u2018-moz.*/i,
+ isFromDevTools: false,
+ platforms: ["windows"],
+ },
+ {
+ sourceName: /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua|pluginproblem)\.css$/i,
+ errorMessage: /Unknown pseudo-class.*-moz-/i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName: /\b(minimal-xul|html|mathml|ua|forms|svg)\.css$/i,
+ errorMessage: /Unknown property.*-moz-/i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName: /(minimal-xul|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,
+ },
+];
+
+if (!Services.prefs.getBoolPref("layout.css.math-depth.enabled")) {
+ // mathml.css UA sheet rule for math-depth.
+ whitelist.push({
+ sourceName: /\b(minimal-xul|mathml)\.css$/i,
+ errorMessage: /Unknown property .*\bmath-depth\b/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.math-style.enabled")) {
+ // mathml.css UA sheet rule for math-style.
+ whitelist.push({
+ sourceName: /(?:res|gre-resources)\/mathml\.css$/i,
+ errorMessage: /Unknown property .*\bmath-style\b/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.scroll-anchoring.enabled")) {
+ whitelist.push({
+ sourceName: /webconsole\.css$/i,
+ errorMessage: /Unknown property .*\boverflow-anchor\b/i,
+ isFromDevTools: true,
+ });
+}
+
+let propNameWhitelist = [
+ // These custom properties are retrieved directly from CSSOM
+ // in videocontrols.xml to get pre-defined style instead of computed
+ // dimensions, which is why they are not referenced by CSS.
+ { propName: "--clickToPlay-width", isFromDevTools: false },
+ { propName: "--playButton-width", isFromDevTools: false },
+ { propName: "--muteButton-width", isFromDevTools: false },
+ { propName: "--castingButton-width", isFromDevTools: false },
+ { propName: "--closedCaptionButton-width", isFromDevTools: false },
+ { propName: "--fullscreenButton-width", isFromDevTools: false },
+ { propName: "--durationSpan-width", isFromDevTools: false },
+ { propName: "--durationSpan-width-long", isFromDevTools: false },
+ { propName: "--positionDurationBox-width", isFromDevTools: false },
+ { propName: "--positionDurationBox-width-long", isFromDevTools: false },
+
+ // These variables are used in a shorthand, but the CSS parser deletes the values
+ // when expanding the shorthands. See https://github.com/w3c/csswg-drafts/issues/2515
+ { propName: "--bezier-diagonal-color", isFromDevTools: true },
+ { propName: "--bezier-grid-color", 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 dumpWhitelistItem(item) {
+ return JSON.stringify(item, (key, value) => {
+ return value instanceof RegExp ? value.toString() : value;
+ });
+}
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in whitelist
+ *
+ * @param aErrorObject the error to check
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(aErrorObject) {
+ for (let whitelistItem of whitelist) {
+ let matches = true;
+ let catchAll = true;
+ for (let prop of ["sourceName", "errorMessage"]) {
+ if (whitelistItem.hasOwnProperty(prop)) {
+ catchAll = false;
+ if (!whitelistItem[prop].test(aErrorObject[prop] || "")) {
+ matches = false;
+ break;
+ }
+ }
+ }
+ if (catchAll) {
+ ok(
+ false,
+ "A whitelist item is catching all errors. " +
+ dumpWhitelistItem(whitelistItem)
+ );
+ continue;
+ }
+ if (matches) {
+ whitelistItem.used = true;
+ let { sourceName, errorMessage } = aErrorObject;
+ info(
+ `Ignored error "${errorMessage}" on ${sourceName} ` +
+ "because of whitelist item " +
+ dumpWhitelistItem(whitelistItem)
+ );
+ return true;
+ }
+ }
+ return false;
+}
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+);
+var gChromeMap = new Map();
+
+var resHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+var gResourceMap = [];
+function trackResourcePrefix(prefix) {
+ let uri = Services.io.newURI("resource://" + prefix + "/");
+ gResourceMap.unshift([prefix, resHandler.resolveURI(uri)]);
+}
+trackResourcePrefix("gre");
+trackResourcePrefix("app");
+
+function getBaseUriForChromeUri(chromeUri) {
+ let chromeFile = chromeUri + "gobbledygooknonexistentfile.reallynothere";
+ let uri = Services.io.newURI(chromeFile);
+ let fileUri = gChromeReg.convertChromeURL(uri);
+ return fileUri.resolve(".");
+}
+
+function parseManifest(manifestUri) {
+ return fetchFile(manifestUri.spec).then(data => {
+ for (let line of data.split("\n")) {
+ let [type, ...argv] = line.split(/\s+/);
+ if (type == "content" || type == "skin") {
+ let chromeUri = `chrome://${argv[0]}/${type}/`;
+ gChromeMap.set(getBaseUriForChromeUri(chromeUri), chromeUri);
+ } else if (type == "resource") {
+ trackResourcePrefix(argv[0]);
+ }
+ }
+ });
+}
+
+function convertToCodeURI(fileUri) {
+ let baseUri = fileUri;
+ let path = "";
+ while (true) {
+ let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
+ if (slashPos <= 0) {
+ // File not accessible from chrome protocol, try resource://
+ for (let res of gResourceMap) {
+ if (fileUri.startsWith(res[1])) {
+ return fileUri.replace(res[1], "resource://" + res[0] + "/");
+ }
+ }
+ // Give up and return the original URL.
+ return fileUri;
+ }
+ path = baseUri.slice(slashPos + 1) + path;
+ baseUri = baseUri.slice(0, slashPos + 1);
+ if (gChromeMap.has(baseUri)) {
+ return gChromeMap.get(baseUri) + path;
+ }
+ }
+}
+
+function messageIsCSSError(msg) {
+ // Only care about CSS errors generated by our iframe:
+ if (
+ msg instanceof Ci.nsIScriptError &&
+ msg.category.includes("CSS") &&
+ msg.sourceName.endsWith(kPathSuffix)
+ ) {
+ let sourceName = msg.sourceName.slice(0, -kPathSuffix.length);
+ let msgInfo = { sourceName, errorMessage: msg.errorMessage };
+ // Check if this error is whitelisted in whitelist
+ if (!ignoredError(msgInfo)) {
+ ok(false, `Got error message for ${sourceName}: ${msg.errorMessage}`);
+ return true;
+ }
+ }
+ return false;
+}
+
+let imageURIsToReferencesMap = new Map();
+let customPropsToReferencesMap = new Map();
+
+function processCSSRules(sheet) {
+ for (let rule of sheet.cssRules) {
+ if (rule instanceof CSSConditionRule || rule instanceof CSSKeyframesRule) {
+ processCSSRules(rule);
+ continue;
+ }
+ if (!(rule instanceof CSSStyleRule) && !(rule instanceof CSSKeyframeRule)) {
+ continue;
+ }
+
+ // Extract urls from the css text.
+ // Note: CSSRule.cssText always has double quotes around URLs even
+ // when the original CSS file didn't.
+ let urls = rule.cssText.match(/url\("[^"]*"\)/g);
+ // Extract props by searching all "--" preceeded by "var(" or a non-word
+ // character.
+ let props = rule.cssText.match(/(var\(|\W)(--[\w\-]+)/g);
+ if (!urls && !props) {
+ continue;
+ }
+
+ for (let url of urls || []) {
+ // Remove the url(" prefix and the ") suffix.
+ url = url.replace(/url\("(.*)"\)/, "$1");
+ if (url.startsWith("data:")) {
+ continue;
+ }
+
+ // Make the url absolute and remove the ref.
+ let baseURI = Services.io.newURI(rule.parentStyleSheet.href);
+ url = Services.io.newURI(url, null, baseURI).specIgnoringRef;
+
+ // Store the image url along with the css file referencing it.
+ let baseUrl = baseURI.spec.split("?always-parse-css")[0];
+ if (!imageURIsToReferencesMap.has(url)) {
+ imageURIsToReferencesMap.set(url, new Set([baseUrl]));
+ } else {
+ imageURIsToReferencesMap.get(url).add(baseUrl);
+ }
+ }
+
+ for (let prop of props || []) {
+ if (prop.startsWith("var(")) {
+ prop = prop.substring(4);
+ let prevValue = customPropsToReferencesMap.get(prop) || 0;
+ customPropsToReferencesMap.set(prop, prevValue + 1);
+ } else {
+ // Remove the extra non-word character captured by the regular
+ // expression.
+ 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");
+ Cu.reportError(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.import(
+ "resource://gre/modules/HiddenFrame.jsm",
+ {}
+ ).HiddenFrame;
+ 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 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 throttledMapPromises(allPromises, loadCSS);
+
+ // Check if all the files referenced from CSS actually exist.
+ for (let [image, references] of imageURIsToReferencesMap) {
+ if (!chromeFileExists(image)) {
+ for (let ref of references) {
+ ok(false, "missing " + image + " referenced from " + ref);
+ }
+ }
+ }
+
+ // Check if all the properties that are defined are referenced.
+ for (let [prop, refCount] of customPropsToReferencesMap) {
+ if (!refCount) {
+ let ignored = false;
+ for (let item of propNameWhitelist) {
+ if (item.propName == prop && isDevtools == item.isFromDevTools) {
+ item.used = true;
+ if (
+ !item.platforms ||
+ item.platforms.includes(AppConstants.platform)
+ ) {
+ ignored = true;
+ }
+ break;
+ }
+ }
+ if (!ignored) {
+ ok(false, "custom property `" + prop + "` is not referenced");
+ }
+ }
+ }
+
+ let messages = Services.console.getMessageArray();
+ // Count errors (the test output will list actual issues for us, as well
+ // as the ok(false) in messageIsCSSError.
+ let errors = messages.filter(messageIsCSSError);
+ is(
+ errors.length,
+ 0,
+ "All the styles (" + allPromises.length + ") loaded without errors."
+ );
+
+ // Confirm that all whitelist rules have been used.
+ function checkWhitelist(list) {
+ for (let item of list) {
+ if (
+ !item.used &&
+ isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform)) &&
+ !item.intermittent
+ ) {
+ ok(false, "Unused whitelist item: " + dumpWhitelistItem(item));
+ }
+ }
+ }
+ checkWhitelist(whitelist);
+ checkWhitelist(propNameWhitelist);
+
+ // Clean up to avoid leaks:
+ doc.head.innerHTML = "";
+ doc = null;
+ iframe.remove();
+ iframe = null;
+ win = null;
+ hiddenFrame.destroy();
+ hiddenFrame = null;
+ imageURIsToReferencesMap = null;
+ customPropsToReferencesMap = null;
+});
diff --git a/browser/base/content/test/static/browser_parsable_script.js b/browser/base/content/test/static/browser_parsable_script.js
new file mode 100644
index 0000000000..1658a1b24d
--- /dev/null
+++ b/browser/base/content/test/static/browser_parsable_script.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' JS issues to remain, while we
+ * detect newly occurring issues in shipping JS. It is a list of regexes
+ * matching files which have errors:
+ */
+
+requestLongerTimeout(2);
+
+const kWhitelist = new Set([
+ /browser\/content\/browser\/places\/controller.js$/,
+]);
+
+const kESModuleList = new Set([
+ /browser\/res\/payments\/(components|containers|mixins)\/.*\.js$/,
+ /browser\/res\/payments\/paymentRequest\.js$/,
+ /browser\/res\/payments\/PaymentsStore\.js$/,
+ /browser\/aboutlogins\/components\/.*\.js$/,
+ /browser\/aboutlogins\/.*\.js$/,
+ /browser\/protections.js$/,
+ /browser\/lockwise-card.js$/,
+ /browser\/monitor-card.js$/,
+ /browser\/proxy-card.js$/,
+ /browser\/vpn-card.js$/,
+ /browser\/content\/browser\/aboutNetError\.js$/,
+ /toolkit\/content\/global\/certviewer\/components\/.*\.js$/,
+ /toolkit\/content\/global\/certviewer\/.*\.js$/,
+]);
+
+// Normally we would use reflect.jsm to get Reflect.parse. However, if
+// we do that, then all the AST data is allocated in reflect.jsm's
+// zone. That exposes a bug in our GC. The GC collects reflect.jsm's
+// zone but not the zone in which our test code lives (since no new
+// data is being allocated in it). The cross-compartment wrappers in
+// our zone that point to the AST data never get collected, and so the
+// AST data itself is never collected. We need to GC both zones at
+// once to fix the problem.
+const init = Cc["@mozilla.org/jsreflect;1"].createInstance();
+init();
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in kWhitelist
+ *
+ * @param uri the uri to check against the whitelist
+ * @return true if the uri should be skipped, false otherwise.
+ */
+function uriIsWhiteListed(uri) {
+ for (let whitelistItem of kWhitelist) {
+ if (whitelistItem.test(uri.spec)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Check if a URI should be parsed as an ES module.
+ *
+ * @param uri the uri to check against the ES module list
+ * @return true if the uri should be parsed as a module, otherwise parse it as a script.
+ */
+function uriIsESModule(uri) {
+ for (let whitelistItem of kESModuleList) {
+ if (whitelistItem.test(uri.spec)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function parsePromise(uri, parseTarget) {
+ let promise = new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ let scriptText = this.responseText;
+ try {
+ info(`Checking ${parseTarget} ${uri}`);
+ let parseOpts = {
+ source: uri,
+ target: parseTarget,
+ };
+ Reflect.parse(scriptText, parseOpts);
+ resolve(true);
+ } catch (ex) {
+ let errorMsg = "Script error reading " + uri + ": " + ex;
+ ok(false, errorMsg);
+ resolve(false);
+ }
+ }
+ };
+ xhr.onerror = error => {
+ ok(false, "XHR error reading " + uri + ": " + error);
+ resolve(false);
+ };
+ xhr.overrideMimeType("application/javascript");
+ xhr.send(null);
+ });
+ return promise;
+}
+
+add_task(async function checkAllTheJS() {
+ // In debug builds, even on a fast machine, collecting the file list may take
+ // more than 30 seconds, and parsing all files may take four more minutes.
+ // For this reason, this test must be explictly requested in debug builds by
+ // using the "--setpref parse=<filter>" argument to mach. You can specify:
+ // - A case-sensitive substring of the file name to test (slow).
+ // - A single absolute URI printed out by a previous run (fast).
+ // - An empty string to run the test on all files (slowest).
+ let parseRequested = Services.prefs.prefHasUserValue("parse");
+ let parseValue = parseRequested && Services.prefs.getCharPref("parse");
+ if (SpecialPowers.isDebugBuild) {
+ if (!parseRequested) {
+ ok(
+ true,
+ "Test disabled on debug build. To run, execute: ./mach" +
+ " mochitest-browser --setpref parse=<case_sensitive_filter>" +
+ " browser/base/content/test/general/browser_parsable_script.js"
+ );
+ return;
+ }
+ // Request a 15 minutes timeout (30 seconds * 30) for debug builds.
+ requestLongerTimeout(30);
+ }
+
+ let uris;
+ // If an absolute URI is specified on the command line, use it immediately.
+ if (parseValue && parseValue.includes(":")) {
+ uris = [NetUtil.newURI(parseValue)];
+ } else {
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let startTimeMs = Date.now();
+ info("Collecting URIs");
+ uris = await generateURIsFromDirTree(appDir, [".js", ".jsm"]);
+ 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 throttledMapPromises(uris, uri => {
+ if (uriIsWhiteListed(uri)) {
+ info("Not checking whitelisted " + uri.spec);
+ return undefined;
+ }
+ let target = "script";
+ if (uriIsESModule(uri)) {
+ target = "module";
+ }
+ return parsePromise(uri.spec, target);
+ });
+ ok(true, "All files parsed");
+});
diff --git a/browser/base/content/test/static/browser_title_case_menus.js b/browser/base/content/test/static/browser_title_case_menus.js
new file mode 100644
index 0000000000..8cc0cd6e7e
--- /dev/null
+++ b/browser/base/content/test/static/browser_title_case_menus.js
@@ -0,0 +1,143 @@
+/* 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);
+});
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..5be974349d
--- /dev/null
+++ b/browser/base/content/test/static/head.js
@@ -0,0 +1,193 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { OS, require } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+/**
+ * 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;
+ })();
+}
+
+/**
+ * Uses OS.File.DirectoryIterator to asynchronously iterate over a directory.
+ * It returns a promise that is resolved with an object with two 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)
+ *
+ * @param path the path to check (string)
+ * @param extensions the file extensions we're interested in.
+ */
+function iterateOverPath(path, extensions) {
+ let iterator = new OS.File.DirectoryIterator(path);
+ let parentDir = new LocalFile(path);
+ let subdirs = [];
+ let files = [];
+
+ let pathEntryIterator = entry => {
+ if (entry.isDir) {
+ subdirs.push(entry.path);
+ } else if (extensions.some(extension => entry.name.endsWith(extension))) {
+ let file = parentDir.clone();
+ file.append(entry.name);
+ // the build system might leave dead symlinks hanging around, which are
+ // returned as part of the directory iterator, but don't actually exist:
+ if (file.exists()) {
+ let uriSpec = getURLForFile(file);
+ files.push(Services.io.newURI(uriSpec));
+ }
+ } else if (
+ entry.name.endsWith(".ja") ||
+ entry.name.endsWith(".jar") ||
+ entry.name.endsWith(".zip") ||
+ entry.name.endsWith(".xpi")
+ ) {
+ let file = parentDir.clone();
+ file.append(entry.name);
+ for (let extension of extensions) {
+ let jarEntryIterator = generateEntriesFromJarFile(file, extension);
+ files.push(...jarEntryIterator);
+ }
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ (async function() {
+ try {
+ // Iterate through the directory
+ await iterator.forEach(pathEntryIterator);
+ resolve({ files, subdirs });
+ } catch (ex) {
+ reject(ex);
+ } finally {
+ iterator.close();
+ }
+ })();
+ });
+}
+
+/* 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);
+ });
+}
+
+async function throttledMapPromises(iterable, task, limit = 64) {
+ let promises = new Set();
+ for (let data of iterable) {
+ while (promises.size >= limit) {
+ await Promise.race(promises);
+ }
+
+ let promise = task(data);
+ if (promise) {
+ promise.finally(() => promises.delete(promise));
+ promises.add(promise);
+ }
+ }
+
+ await Promise.all(promises);
+}
+
+/**
+ * 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/.eslintrc.js b/browser/base/content/test/statuspanel/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/statuspanel/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/statuspanel/browser.ini b/browser/base/content/test/statuspanel/browser.ini
new file mode 100644
index 0000000000..89ab74dd1a
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_show_statuspanel_twice.js]
+[browser_show_statuspanel_idn.js]
+skip-if = webrender && verify
diff --git a/browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js b/browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js
new file mode 100644
index 0000000000..62e35448f0
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser_show_statuspanel_idn.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = encodeURI(
+ `data:text/html;charset=utf-8,<a id="foo" href="http://nic.xn--rhqv96g/">abc</a><span id="bar">def</span>`
+);
+const TEST_STATUS_TEXT = "nic.\u4E16\u754C";
+
+/**
+ * Test that if the StatusPanel displays an IDN
+ * (Bug 1450538).
+ */
+add_task(async function test_show_statuspanel_twice() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URL
+ );
+
+ let promise = promiseStatusPanelShown(window, TEST_STATUS_TEXT);
+ SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.links[0].focus();
+ });
+ await promise;
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js b/browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js
new file mode 100644
index 0000000000..c6caca1039
--- /dev/null
+++ b/browser/base/content/test/statuspanel/browser_show_statuspanel_twice.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+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..4ef05405f3
--- /dev/null
+++ b/browser/base/content/test/statuspanel/head.js
@@ -0,0 +1,53 @@
+/* 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);
+ resolve();
+ }
+ };
+ panel.addEventListener("transitionend", l);
+ panel.addEventListener("transitioncancel", l);
+ });
+}
diff --git a/browser/base/content/test/sync/.eslintrc.js b/browser/base/content/test/sync/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/sync/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/sync/browser.ini b/browser/base/content/test/sync/browser.ini
new file mode 100644
index 0000000000..88576cf836
--- /dev/null
+++ b/browser/base/content/test/sync/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_sync.js]
+[browser_contextmenu_sendtab.js]
+[browser_contextmenu_sendpage.js]
+[browser_fxa_web_channel.js]
+support-files=
+ browser_fxa_web_channel.html
+[browser_fxa_badge.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..9233dfae39
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -0,0 +1,428 @@
+/* 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" },
+ },
+ { id: 2, name: "Bar", clientRecord: "bar" }, // Legacy send tab target (no availableCommands).
+ { id: 3, name: "Homer" }, // Incompatible target.
+];
+
+add_task(async function setup() {
+ 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 tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab 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);
+ });
+
+ await openContentContextMenu(
+ "#testingLink",
+ "context-sendlinktodevice",
+ "context-sendlinktodevice-popup"
+ );
+ 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"
+ );
+ document
+ .getElementById("context-sendlinktodevice-popup")
+ .querySelector("menuitem")
+ .click();
+ await hideContentContextMenu();
+
+ expectation.verify();
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_no_remote_clients() {
+ const sandbox = setupSendTabMocks({ fxaDevices: [] });
+
+ await openContentContextMenu("#moztext", "context-sendpagetodevice");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+ checkPopup([
+ { label: "No Devices Connected", disabled: true },
+ "----",
+ { label: "Connect Another Device..." },
+ { label: "Learn About Sending Tabs..." },
+ ]);
+ 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 tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab 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,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ true,
+ "Send tab 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,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ true,
+ "Send tab 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,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ true,
+ "Send tab 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", "context-sendpagetodevice");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+ checkPopup([
+ { label: "Account Not Verified", disabled: true },
+ "----",
+ { label: "Verify Your Account..." },
+ ]);
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_unconfigured() {
+ const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_CONFIGURED });
+
+ await openContentContextMenu("#moztext", "context-sendpagetodevice");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+ checkPopup([
+ { label: "Not Signed In", disabled: true },
+ "----",
+ { label: "Sign in to Firefox..." },
+ { label: "Learn About Sending Tabs..." },
+ ]);
+
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_not_verified() {
+ const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_VERIFIED });
+
+ await openContentContextMenu("#moztext", "context-sendpagetodevice");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+ checkPopup([
+ { label: "Account Not Verified", disabled: true },
+ "----",
+ { label: "Verify Your Account..." },
+ ]);
+
+ await hideContentContextMenu();
+
+ sandbox.restore();
+});
+
+add_task(async function test_page_contextmenu_login_failed() {
+ const sandbox = setupSendTabMocks({ state: UIState.STATUS_LOGIN_FAILED });
+
+ await openContentContextMenu("#moztext", "context-sendpagetodevice");
+ is(
+ document.getElementById("context-sendpagetodevice").hidden,
+ false,
+ "Send tab to device is shown"
+ );
+ is(
+ document.getElementById("context-sendpagetodevice").disabled,
+ false,
+ "Send tab to device is enabled"
+ );
+ checkPopup([
+ { label: "Account Not Verified", disabled: true },
+ "----",
+ { label: "Verify Your Account..." },
+ ]);
+
+ 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 tab to device is hidden"
+ );
+ is(
+ document.getElementById("context-sep-sendpagetodevice").hidden,
+ true,
+ "Separator is also 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 menuPopup = document.getElementById(openSubmenuId).menupopup;
+ const menuPopupPromise = BrowserTestUtils.waitForEvent(
+ menuPopup,
+ "popupshown"
+ );
+ menuPopup.openPopup();
+ await menuPopupPromise;
+ }
+}
+
+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..cecc738aac
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js
@@ -0,0 +1,267 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const chrome_base =
+ "chrome://mochitests/content/browser/browser/base/content/test/general/";
+Services.scriptloader.loadSubScript(chrome_base + "head.js", this);
+/* import-globals-from ../general/head.js */
+
+const fxaDevices = [
+ {
+ id: 1,
+ name: "Foo",
+ availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "baz" },
+ },
+ { id: 2, name: "Bar", clientRecord: "bar" }, // Legacy send tab target (no availableCommands).
+ { id: 3, 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_task(async function setup() {
+ 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");
+ document
+ .getElementById("context_sendTabToDevicePopupMenu")
+ .querySelector("menuitem")
+ .click();
+
+ await promiseObserved;
+ await hideTabContextMenu();
+ sandbox.restore();
+});
+
+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"
+ );
+
+ 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"
+ );
+
+ document
+ .getElementById("context_sendTabToDevicePopupMenu")
+ .querySelector("menuitem")
+ .click();
+
+ await hideTabContextMenu();
+ 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,
+ false,
+ "Send tab to device is shown"
+ );
+ 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,
+ false,
+ "Send tab to device is shown"
+ );
+ 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,
+ false,
+ "Send tab to device is shown"
+ );
+ 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,
+ false,
+ "Send tab to device is shown"
+ );
+ 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,
+ false,
+ "Send tab to device is shown"
+ );
+ 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;
+ }
+}
+
+async function hideTabContextMenu() {
+ const contextMenu = document.getElementById("tabContextMenu");
+ const awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+}
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ };
+ Services.obs.addObserver(obs, topic);
+ });
+}
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..f6a6155c16
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_badge.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppMenuNotifications } = ChromeUtils.import(
+ "resource://gre/modules/AppMenuNotifications.jsm"
+);
+
+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 isShown = false;
+ for (let notification of AppMenuNotifications.notifications) {
+ if (notification.id == "fxa-needs-authentication") {
+ isShown = true;
+ break;
+ }
+ }
+ 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..c169a9744f
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.html
@@ -0,0 +1,138 @@
+<!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;
+ }
+ };
+
+ 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);
+ }
+</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..5e5f43aa32
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.js
@@ -0,0 +1,248 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
+ return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js", {});
+});
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "WebChannel",
+ "resource://gre/modules/WebChannel.jsm"
+);
+
+// FxAccountsWebChannel isn't explicitly exported by FxAccountsWebChannel.jsm
+// but we can get it here via a backstage pass.
+var { FxAccountsWebChannel } = ChromeUtils.import(
+ "resource://gre/modules/FxAccountsWebChannel.jsm",
+ null
+);
+
+const TEST_HTTP_PATH = "http://example.com";
+const TEST_BASE_URL =
+ TEST_HTTP_PATH +
+ "/browser/browser/base/content/test/sync/browser_fxa_web_channel.html";
+const TEST_CHANNEL_ID = "account_updates_test";
+
+var gTests = [
+ {
+ desc: "FxA Web Channel - should receive message about profile changes",
+ async run() {
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ });
+ let promiseObserver = new Promise((resolve, reject) => {
+ makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function(
+ subject,
+ topic,
+ data
+ ) {
+ Assert.equal(data, "abc123");
+ client.tearDown();
+ resolve();
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?profile_change",
+ },
+ async function() {
+ await promiseObserver;
+ }
+ );
+ },
+ },
+ {
+ desc:
+ "fxa web channel - login messages should notify the fxAccounts object",
+ async run() {
+ let promiseLogin = new Promise((resolve, reject) => {
+ let login = accountData => {
+ Assert.equal(typeof accountData.authAt, "number");
+ Assert.equal(accountData.email, "testuser@testuser.com");
+ Assert.equal(accountData.keyFetchToken, "key_fetch_token");
+ Assert.equal(accountData.sessionToken, "session_token");
+ Assert.equal(accountData.uid, "uid");
+ Assert.equal(accountData.unwrapBKey, "unwrap_b_key");
+ Assert.equal(accountData.verified, true);
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ login,
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?login",
+ },
+ async function() {
+ await promiseLogin;
+ }
+ );
+ },
+ },
+ {
+ desc: "fxa web channel - can_link_account messages should respond",
+ async run() {
+ let properUrl = TEST_BASE_URL + "?can_link_account";
+
+ let promiseEcho = new Promise((resolve, reject) => {
+ let webChannelOrigin = Services.io.newURI(properUrl);
+ // responses sent to content are echoed back over the
+ // `fxaccounts_webchannel_response_echo` channel. Ensure the
+ // fxaccounts:can_link_account message is responded to.
+ let echoWebChannel = new WebChannel(
+ "fxaccounts_webchannel_response_echo",
+ webChannelOrigin
+ );
+ echoWebChannel.listen((webChannelId, message, target) => {
+ Assert.equal(message.command, "fxaccounts:can_link_account");
+ Assert.equal(message.messageId, 2);
+ Assert.equal(message.data.ok, true);
+
+ client.tearDown();
+ echoWebChannel.stopListening();
+
+ resolve();
+ });
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ shouldAllowRelink(acctName) {
+ return acctName === "testuser@testuser.com";
+ },
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: properUrl,
+ },
+ async function() {
+ await promiseEcho;
+ }
+ );
+ },
+ },
+ {
+ desc:
+ "fxa web channel - logout messages should notify the fxAccounts object",
+ async run() {
+ let promiseLogout = new Promise((resolve, reject) => {
+ let logout = uid => {
+ Assert.equal(uid, "uid");
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout,
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?logout",
+ },
+ async function() {
+ await promiseLogout;
+ }
+ );
+ },
+ },
+ {
+ desc:
+ "fxa web channel - delete messages should notify the fxAccounts object",
+ async run() {
+ let promiseDelete = new Promise((resolve, reject) => {
+ let logout = uid => {
+ Assert.equal(uid, "uid");
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout,
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_BASE_URL + "?delete",
+ },
+ async function() {
+ await promiseDelete;
+ }
+ );
+ },
+ },
+]; // 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..c1b62ae6fb
--- /dev/null
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -0,0 +1,608 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function setup() {
+ // gSync.init() is called in a requestIdleCallback. Force its initialization.
+ gSync.init();
+});
+
+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_ui_state_signedin() {
+ await openTabAndRemoteTabsPanel();
+
+ 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);
+ checkRemoteTabsPanel("PanelUI-remotetabs-main", false);
+ checkMenuBarItem("sync-syncnowitem");
+ checkFxaToolbarButtonPanel({
+ headerTitle: "foo@bar.com",
+ headerDescription: "Account Settings",
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-connect-device-button",
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-remotetabs-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ "PanelUI-fxa-menu-logins-button",
+ "PanelUI-fxa-menu-monitor-button",
+ "PanelUI-fxa-menu-send-button",
+ ],
+ disabledItems: [],
+ hiddenItems: ["PanelUI-fxa-menu-setup-sync-button"],
+ });
+ checkFxAAvatar("signedin");
+ gSync.relativeTimeFormat = origRelativeTimeFormat;
+ await closeRemoteTabsPanel();
+
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ label: "foo@bar.com",
+ fxastatus: "signedin",
+ syncing: false,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_syncing() {
+ 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_unconfigured() {
+ await openTabAndRemoteTabsPanel();
+
+ let state = {
+ status: UIState.STATUS_NOT_CONFIGURED,
+ };
+
+ gSync.updateAllUI(state);
+
+ let signedOffLabel = gSync.appMenuStatus.getAttribute("defaultlabel");
+ checkPanelUIStatusBar({
+ label: signedOffLabel,
+ });
+ checkRemoteTabsPanel("PanelUI-remotetabs-setupsync");
+ checkMenuBarItem("sync-setup");
+ checkFxaToolbarButtonPanel({
+ headerTitle: signedOffLabel,
+ headerDescription: "Turn on Sync",
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-setup-sync-button",
+ "PanelUI-fxa-menu-remotetabs-button",
+ "PanelUI-fxa-menu-logins-button",
+ "PanelUI-fxa-menu-monitor-button",
+ "PanelUI-fxa-menu-send-button",
+ ],
+ disabledItems: ["PanelUI-fxa-menu-connect-device-button"],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+ checkFxAAvatar("not_configured");
+ await closeRemoteTabsPanel();
+
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ label: signedOffLabel,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_syncdisabled() {
+ await openTabAndRemoteTabsPanel();
+
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: false,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ };
+
+ gSync.updateAllUI(state);
+ checkRemoteTabsPanel("PanelUI-remotetabs-syncdisabled", false);
+ checkMenuBarItem("sync-enable");
+ checkFxaToolbarButtonPanel({
+ headerTitle: "foo@bar.com",
+ headerDescription: "Account Settings",
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-connect-device-button",
+ "PanelUI-fxa-menu-setup-sync-button",
+ "PanelUI-fxa-menu-remotetabs-button",
+ "PanelUI-fxa-menu-logins-button",
+ "PanelUI-fxa-menu-monitor-button",
+ "PanelUI-fxa-menu-send-button",
+ ],
+ disabledItems: [],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+ checkFxAAvatar("signedin");
+ await closeRemoteTabsPanel();
+
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ label: "foo@bar.com",
+ fxastatus: "signedin",
+ syncing: false,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_unverified() {
+ await openTabAndRemoteTabsPanel();
+
+ let state = {
+ status: UIState.STATUS_NOT_VERIFIED,
+ email: "foo@bar.com",
+ syncing: false,
+ };
+
+ gSync.updateAllUI(state);
+
+ const expectedLabel = gSync.fxaStrings.GetStringFromName(
+ "account.finishAccountSetup"
+ );
+
+ checkRemoteTabsPanel("PanelUI-remotetabs-unverified", false);
+ checkMenuBarItem("sync-unverifieditem");
+ checkFxaToolbarButtonPanel({
+ headerTitle: expectedLabel,
+ headerDescription: state.email,
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-setup-sync-button",
+ "PanelUI-fxa-menu-remotetabs-button",
+ "PanelUI-fxa-menu-logins-button",
+ "PanelUI-fxa-menu-monitor-button",
+ "PanelUI-fxa-menu-send-button",
+ ],
+ disabledItems: ["PanelUI-fxa-menu-connect-device-button"],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+ checkFxAAvatar("unverified");
+ await closeRemoteTabsPanel();
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ label: expectedLabel,
+ fxastatus: "unverified",
+ syncing: false,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_ui_state_loginFailed() {
+ await openTabAndRemoteTabsPanel();
+
+ let state = {
+ status: UIState.STATUS_LOGIN_FAILED,
+ email: "foo@bar.com",
+ };
+
+ gSync.updateAllUI(state);
+
+ const expectedLabel = gSync.fxaStrings.GetStringFromName(
+ "account.reconnectToFxA"
+ );
+
+ checkRemoteTabsPanel("PanelUI-remotetabs-reauthsync", false);
+ checkMenuBarItem("sync-reauthitem");
+ checkFxaToolbarButtonPanel({
+ headerTitle: expectedLabel,
+ headerDescription: state.email,
+ enabledItems: [
+ "PanelUI-fxa-menu-sendtab-button",
+ "PanelUI-fxa-menu-setup-sync-button",
+ "PanelUI-fxa-menu-remotetabs-button",
+ "PanelUI-fxa-menu-logins-button",
+ "PanelUI-fxa-menu-monitor-button",
+ "PanelUI-fxa-menu-send-button",
+ ],
+ disabledItems: ["PanelUI-fxa-menu-connect-device-button"],
+ hiddenItems: [
+ "PanelUI-fxa-menu-syncnow-button",
+ "PanelUI-fxa-menu-sync-prefs-button",
+ ],
+ });
+ checkFxAAvatar("login-failed");
+ await closeRemoteTabsPanel();
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ label: expectedLabel,
+ fxastatus: "login-failed",
+ syncing: false,
+ });
+
+ await closeTabAndMainPanel();
+});
+
+add_task(async function test_account_settings_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 checkAccountPanel([
+ "PanelUI-fxa-menu-account-settings-button",
+ "PanelUI-fxa-menu-account-signout-button",
+ ]);
+ await closeRemoteTabsPanel();
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ label: "foo@bar.com",
+ fxastatus: "signedin",
+ syncing: false,
+ });
+
+ 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");
+
+ ok(
+ BrowserTestUtils.is_hidden(
+ newWin.document.getElementById("appMenu-fxa-status")
+ ),
+ "Fxa status is hidden"
+ );
+
+ [...newWin.document.querySelectorAll(".sync-ui-item")].forEach(
+ e => (e.hidden = false)
+ );
+ let mainView = newWin.document.getElementById("appMenu-mainView");
+ let hidden = BrowserTestUtils.waitForEvent(
+ newWin.document,
+ "popuphidden",
+ true
+ );
+ mainView.closest("panel").hidePopup();
+ await hidden;
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+function checkPanelUIStatusBar({ label, fxastatus, syncing }) {
+ let labelNode = document.getElementById("appMenu-fxa-label");
+ is(labelNode.getAttribute("label"), label, "fxa label has the right value");
+}
+
+function checkRemoteTabsPanel(expectedShownItemId, syncing, syncNowTooltip) {
+ checkItemsVisibilities(
+ [
+ "PanelUI-remotetabs-main",
+ "PanelUI-remotetabs-setupsync",
+ "PanelUI-remotetabs-reauthsync",
+ "PanelUI-remotetabs-unverified",
+ ],
+ expectedShownItemId
+ );
+
+ if (syncing != undefined && syncNowTooltip != undefined) {
+ checkSyncNowButtons(syncing, syncNowTooltip);
+ }
+}
+
+function checkMenuBarItem(expectedShownItemId) {
+ checkItemsVisibilities(
+ [
+ "sync-setup",
+ "sync-enable",
+ "sync-syncnowitem",
+ "sync-reauthitem",
+ "sync-unverifieditem",
+ ],
+ expectedShownItemId
+ );
+}
+
+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"
+ );
+ }
+
+ is(
+ syncButton.hasAttribute("disabled"),
+ syncing,
+ "disabled has the right value"
+ );
+ if (syncing) {
+ is(
+ document.l10n.getAttributes(syncButton).id,
+ syncButton.getAttribute("syncinglabel"),
+ "label is set to the right value"
+ );
+ } else {
+ is(
+ document.l10n.getAttributes(syncButton).id,
+ "fxa-toolbar-sync-now",
+ "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);
+ let elShown = window.getComputedStyle(el).display == "none";
+ is(elShown, true, id + " is hidden");
+ }
+}
+
+async function checkFxABadged() {
+ const button = document.getElementById("fxa-toolbar-menu-button");
+ await BrowserTestUtils.waitForCondition(() => {
+ return button.querySelector("label.feature-callout");
+ });
+ const badge = button.querySelector("label.feature-callout");
+ ok(badge, "expected feature-callout style badge");
+ ok(BrowserTestUtils.is_visible(badge), "expected the badge to be visible");
+}
+
+// fxaStatus is one of 'not_configured', 'unverified', 'login-failed', or 'signedin'.
+function checkFxAAvatar(fxaStatus) {
+ // Unhide the panel so computed styles can be read
+ document.querySelector("#appMenu-popup").hidden = false;
+
+ const avatarContainers = [
+ document.getElementById("fxa-menu-avatar"),
+ 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-confirm.svg")',
+ signedin: 'url("chrome://browser/skin/fxa/avatar.svg")',
+ "login-failed": 'url("chrome://browser/skin/fxa/avatar-alert.svg")',
+ };
+ ok(
+ avatarURL == expected[fxaStatus],
+ `expected avatar URL to be ${expected[fxaStatus]}, got ${avatarURL}`
+ );
+ }
+}
+
+async function checkAccountPanel(enabledItems) {
+ let fxaButton = document.getElementById("fxa-toolbar-menu-button");
+ fxaButton.click();
+
+ let fxaView = document.getElementById("PanelUI-fxa");
+ await BrowserTestUtils.waitForEvent(fxaView, "ViewShown");
+
+ let manageAccountButton = document.getElementById(
+ "fxa-manage-account-button"
+ );
+ PanelUI.showSubView("PanelUI-fxa-menu-account-panel", manageAccountButton);
+
+ let manageAccountPanel = document.getElementById(
+ "PanelUI-fxa-menu-account-panel"
+ );
+ await BrowserTestUtils.waitForEvent(manageAccountPanel, "ViewShown");
+
+ for (const id of enabledItems) {
+ const el = document.getElementById(id);
+ is(el.hasAttribute("disabled"), false, id + " is enabled");
+ }
+}
+
+// Only one item displayed at a time.
+function checkItemsDisplayed(itemsIds, expectedShownItemId) {
+ for (let id of itemsIds) {
+ if (id == expectedShownItemId) {
+ ok(
+ BrowserTestUtils.is_visible(document.getElementById(id)),
+ `view ${id} should be visible`
+ );
+ } else {
+ ok(
+ BrowserTestUtils.is_hidden(document.getElementById(id)),
+ `view ${id} should be hidden`
+ );
+ }
+ }
+}
+
+// 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 openTabAndRemoteTabsPanel() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
+
+ let fxaButton = document.getElementById("fxa-toolbar-menu-button");
+ fxaButton.click();
+
+ let fxaView = document.getElementById("PanelUI-fxa");
+ await BrowserTestUtils.waitForEvent(fxaView, "ViewShown");
+
+ let remoteTabsButton = document.getElementById(
+ "PanelUI-fxa-menu-remotetabs-button"
+ );
+ remoteTabsButton.click();
+
+ let remoteTabsView = document.getElementById("PanelUI-remotetabs");
+ await BrowserTestUtils.waitForEvent(remoteTabsView, "ViewShown");
+}
+
+async function closeRemoteTabsPanel() {
+ let fxaView = document.getElementById("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() {
+ let mainView = document.getElementById("appMenu-mainView");
+ let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);
+ mainView.closest("panel").hidePopup();
+ await hidden;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
diff --git a/browser/base/content/test/sync/head.js b/browser/base/content/test/sync/head.js
new file mode 100644
index 0000000000..d6ca8d9b98
--- /dev/null
+++ b/browser/base/content/test/sync/head.js
@@ -0,0 +1,24 @@
+ChromeUtils.import("resource://services-sync/UIState.jsm", this);
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+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,
+ });
+ sandbox.stub(gSync, "isSendableURI").returns(isSendableURI);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ return sandbox;
+}
diff --git a/browser/base/content/test/tabMediaIndicator/.eslintrc.js b/browser/base/content/test/tabMediaIndicator/.eslintrc.js
new file mode 100644
index 0000000000..90c8ff7bef
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+ rules: {
+ "no-shadow": "off",
+ },
+};
diff --git a/browser/base/content/test/tabMediaIndicator/almostSilentAudioTrack.webm b/browser/base/content/test/tabMediaIndicator/almostSilentAudioTrack.webm
new file mode 100644
index 0000000000..0b8f8f746f
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/almostSilentAudioTrack.webm
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/audio.ogg b/browser/base/content/test/tabMediaIndicator/audio.ogg
new file mode 100644
index 0000000000..bed764fbf1
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/audio.ogg
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/audioEndedDuringPlaying.webm b/browser/base/content/test/tabMediaIndicator/audioEndedDuringPlaying.webm
new file mode 100644
index 0000000000..4f82b5da76
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/audioEndedDuringPlaying.webm
Binary files differ
diff --git a/browser/base/content/test/tabMediaIndicator/browser.ini b/browser/base/content/test/tabMediaIndicator/browser.ini
new file mode 100644
index 0000000000..bb7a65cef5
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser.ini
@@ -0,0 +1,31 @@
+[DEFAULT]
+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]
+[browser_mediaplayback_audibility_change.js]
+[browser_mediaPlayback.js]
+[browser_mediaPlayback_mute.js]
+[browser_mute.js]
+[browser_mute2.js]
+[browser_mute_webAudio.js]
+[browser_sound_indicator_silent_video.js]
+[browser_webaudio_audibility_change.js]
+[browser_webAudio_hideSoundPlayingIcon.js]
+[browser_webAudio_silentData.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..5477fda20d
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_destroy_iframe.js
@@ -0,0 +1,49 @@
+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) {
+ 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..9dd47e2cfb
--- /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, event => {
+ is(
+ event.originalTarget,
+ browser,
+ "Event must be dispatched to correct browser."
+ );
+ ok(!event.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.loadURI(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..48ec22e329
--- /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, event => {
+ is(
+ event.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.loadURI(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.loadURI(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..b5c0e2d95c
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mediaplayback_audibility_change.js
@@ -0,0 +1,253 @@
+/**
+ * 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.loadURI(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],
+ 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],
+ 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) {
+ 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],
+ (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..b51adc270b
--- /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..2d65276fc2
--- /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..3ecb4714cc
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_mute_webAudio.js
@@ -0,0 +1,70 @@
+// The tab closing code leaves an uncaught rejection. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+ ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", this);
+ 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.loadURI(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");
+ await clickIcon(tab.soundPlayingIcon);
+ 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 clickIcon(tab.soundPlayingIcon);
+ 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..28e1985d57
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_sound_indicator_silent_video.js
@@ -0,0 +1,88 @@
+const SILENT_PAGE = GetTestWebBasedURL("file_silentAudioTrack.html");
+const ALMOST_SILENT_PAGE = GetTestWebBasedURL(
+ "file_almostSilentAudioTrack.html"
+);
+
+function check_audio_playing_state(isPlaying) {
+ let autoPlay = content.document.getElementById("autoplay");
+ if (!autoPlay) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ is(
+ autoPlay.paused,
+ !isPlaying,
+ "The playing state of autoplay audio is correct."
+ );
+
+ // wait for a while to make sure the video is playing and related event has
+ // been dispatched (if any).
+ let PLAYING_TIME_SEC = 0.5;
+ ok(PLAYING_TIME_SEC < autoPlay.duration, "The playing time is valid.");
+
+ return new Promise(resolve => {
+ autoPlay.ontimeupdate = function() {
+ if (autoPlay.currentTime > PLAYING_TIME_SEC) {
+ resolve();
+ }
+ };
+ });
+}
+
+add_task(async function should_not_show_sound_indicator_for_silent_video() {
+ info("- open new foreground tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+
+ info("- tab should not have sound indicator before playing silent video -");
+ await waitForTabSoundIndicatorDisappears(tab);
+
+ info("- loading autoplay silent video -");
+ BrowserTestUtils.loadURI(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.loadURI(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..e3e6bf519d
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/browser_webaudio_audibility_change.js
@@ -0,0 +1,171 @@
+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.loadURI(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 } = {}) {
+ 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..c6e9a90123
--- /dev/null
+++ b/browser/base/content/test/tabMediaIndicator/head.js
@@ -0,0 +1,152 @@
+/**
+ * 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) {
+ 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);
+
+ 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, document.getElementById("tabbrowser-tab-tooltip"));
+ 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/.eslintrc.js b/browser/base/content/test/tabPrompts/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/tabPrompts/browser.ini b/browser/base/content/test/tabPrompts/browser.ini
new file mode 100644
index 0000000000..ad88b73060
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser.ini
@@ -0,0 +1,8 @@
+[browser_beforeunload_urlbar.js]
+support-files = file_beforeunload_stop.html
+[browser_closeTabSpecificPanels.js]
+skip-if = verify && debug && (os == 'linux')
+[browser_confirmFolderUpload.js]
+[browser_multiplePrompts.js]
+[browser_openPromptInBackgroundTab.js]
+support-files = openPromptOffTimeout.html
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..8d6e9bdf40
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js
@@ -0,0 +1,61 @@
+/* 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",
+ "http://example.com"
+);
+
+add_task(async function test_beforeunload_stay_clears_urlbar() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ const TEST_URL = TEST_ROOT + "file_beforeunload_stop.html";
+ await BrowserTestUtils.withNewTab(TEST_URL, async function(browser) {
+ gURLBar.focus();
+ const inputValue = "http://example.org/?q=typed";
+ gURLBar.inputField.value = inputValue.slice(0, -1);
+ EventUtils.sendString(inputValue.slice(-1));
+
+ 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..3919957957
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js
@@ -0,0 +1,51 @@
+"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");
+ let generalPanel = document.createXULElement("panel");
+ 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..e9231647f9
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromptTestUtils.jsm"
+);
+
+/**
+ * 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_task(async function setup() {
+ 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) {
+ 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_multiplePrompts.js b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
new file mode 100644
index 0000000000..18f41245fd
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
@@ -0,0 +1,96 @@
+"use strict";
+
+/*
+ * 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() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ const PROMPTCOUNT = 9;
+
+ let contentScript = function(MAX_PROMPT) {
+ var i = MAX_PROMPT;
+ let fns = ["alert", "prompt", "confirm"];
+ function openDialog() {
+ i--;
+ if (i) {
+ SpecialPowers.Services.tm.dispatchToMainThread(openDialog);
+ }
+ window[fns[i % 3]](fns[i % 3] + " countdown #" + i);
+ }
+ SpecialPowers.Services.tm.dispatchToMainThread(openDialog);
+ };
+ let url =
+ "data:text/html,<script>(" +
+ encodeURIComponent(contentScript.toSource()) +
+ ")(" +
+ PROMPTCOUNT +
+ ");</script>";
+
+ let promptsOpenedPromise = new Promise(function(resolve) {
+ let unopenedPromptCount = PROMPTCOUNT;
+ Services.obs.addObserver(function observer() {
+ unopenedPromptCount--;
+ if (!unopenedPromptCount) {
+ Services.obs.removeObserver(observer, "tabmodal-dialog-loaded");
+ info("Prompts opened.");
+ resolve();
+ }
+ }, "tabmodal-dialog-loaded");
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
+ info("Tab loaded");
+
+ await promptsOpenedPromise;
+
+ 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.");
+
+ 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..7d2fe03db6
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
@@ -0,0 +1,170 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "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() {
+ 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,
+ "true"
+ );
+ // 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.getAttribute("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 == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityBox.click();
+ await shown;
+ let labelText = SitePermissions.getPermissionLabel("focus-tab-by-prompt");
+ let permissionsList = document.getElementById(
+ "identity-popup-permission-list"
+ );
+ let label = permissionsList.querySelector(".identity-popup-permission-label");
+ is(label.textContent, labelText);
+ gIdentityHandler._identityPopup.hidePopup();
+
+ // Check if the identity icon signals granted permission.
+ ok(
+ gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "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() {
+ 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,
+ "true"
+ );
+ // 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.getAttribute("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(
+ ".content-prompt-dialog"
+ );
+ is(promptElements.length, 1, "There should be 1 prompt");
+
+ BrowserTestUtils.removeTab(openedTab);
+});
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/tabcrashed/.eslintrc.js b/browser/base/content/test/tabcrashed/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/tabcrashed/browser.ini b/browser/base/content/test/tabcrashed/browser.ini
new file mode 100644
index 0000000000..e9d618925c
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+skip-if = (!e10s || !crashreporter)
+support-files =
+ head.js
+ file_contains_emptyiframe.html
+ file_iframe.html
+
+[browser_autoSubmitRequest.js]
+[browser_clearEmail.js]
+[browser_launchFail.js]
+[browser_multipleCrashedTabs.js]
+[browser_noPermanentKey.js]
+skip-if = true # Bug 1383315
+[browser_printpreview_crash.js]
+[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_autoSubmitRequest.js b/browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js
new file mode 100644
index 0000000000..2d6fc4b86b
--- /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.import(
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+
+// 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_clearEmail.js b/browser/base/content/test/tabcrashed/browser_clearEmail.js
new file mode 100644
index 0000000000..5bfa67db80
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_clearEmail.js
@@ -0,0 +1,71 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const EMAIL = "foo@privacy.com";
+
+add_task(async function setup() {
+ await setupLocalCrashReportServer();
+ // By default, requesting the email address of the user is disabled.
+ // For the purposes of this test, we turn it back on.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.crashReporting.requestEmail", true]],
+ });
+});
+
+/**
+ * Test that if we have an email address stored in prefs, and we decide
+ * not to submit the email address in the next crash report, that we
+ * clear the email address.
+ */
+add_task(async function test_clear_email() {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function(browser) {
+ let prefs = TabCrashHandler.prefs;
+ let originalSendReport = prefs.getBoolPref("sendReport");
+ let originalEmailMe = prefs.getBoolPref("emailMe");
+ let originalIncludeURL = prefs.getBoolPref("includeURL");
+ let originalEmail = prefs.getCharPref("email");
+
+ // Pretend that we stored an email address from the previous
+ // crash
+ prefs.setCharPref("email", EMAIL);
+ prefs.setBoolPref("emailMe", true);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ await BrowserTestUtils.crashFrame(
+ browser,
+ /* shouldShowTabCrashPage */ true,
+ /* shouldClearMinidumps */ false
+ );
+ let doc = browser.contentDocument;
+
+ // Since about:tabcrashed will run in the parent process, we can safely
+ // manipulate its DOM nodes directly
+ let emailMe = doc.getElementById("emailMe");
+ emailMe.checked = false;
+
+ let crashReport = promiseCrashReport({
+ Email: "",
+ });
+
+ let restoreTab = browser.contentDocument.getElementById("restoreTab");
+ restoreTab.click();
+ await BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
+ await crashReport;
+
+ is(prefs.getCharPref("email"), "", "No email address should be stored");
+
+ // 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("emailMe", originalEmailMe);
+ prefs.setBoolPref("includeURL", originalIncludeURL);
+ prefs.setCharPref("email", originalEmail);
+ }
+ );
+});
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..66b446d785
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_launchFail.js
@@ -0,0 +1,57 @@
+/* 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() {
+ 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;
+ 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..644a6f774f
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_multipleCrashedTabs.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE_1 = "http://example.com";
+const PAGE_2 = "http://example.org";
+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..239938f990
--- /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_task(async function setup() {
+ 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..74e983d3c3
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_printpreview_crash.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_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() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", false]],
+ });
+ // 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 =
+ "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(
+ !gInPrintPreviewMode,
+ "Should NOT be in print preview mode at the start of this test."
+ );
+
+ // Enter print preview
+ let ppBrowser = PrintPreviewListener.getPrintPreviewBrowser();
+
+ const { PrintingParent } = ChromeUtils.import(
+ "resource://gre/actors/PrintingParent.jsm"
+ );
+ let printPreviewEntered = new Promise(resolve => {
+ PrintingParent.setTestListener(browserPreviewing => {
+ if (browserPreviewing == ppBrowser) {
+ PrintingParent.setTestListener(null);
+ resolve();
+ }
+ });
+ });
+ document.getElementById("cmd_printPreview").doCommand();
+ await printPreviewEntered;
+
+ // Ensure we are in print preview
+ await BrowserTestUtils.waitForCondition(
+ () => gInPrintPreviewMode,
+ "Should be in print preview mode now."
+ );
+ ok(true, "We did not crash.");
+
+ // We haven't crashed! Exit the print preview.
+ await BrowserTestUtils.switchTab(gBrowser, () => {
+ PrintUtils.exitPrintPreview();
+ });
+
+ await BrowserTestUtils.waitForCondition(() => !window.gInPrintPreviewMode);
+ 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..ae6465138a
--- /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..16e698c5a6
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_shown.js
@@ -0,0 +1,202 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const COMMENTS = "Here's my test comment!";
+const EMAIL = "foo@privacy.com";
+
+// Avoid timeouts, as in bug 1325530
+requestLongerTimeout(2);
+
+add_task(async function setup() {
+ 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
+ * email (String)
+ * The email address to put in the email address input
+ * emailMe (bool)
+ * The checked value of the "Email me" checkbox
+ * 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 originalEmailMe = prefs.getBoolPref("emailMe");
+ let originalIncludeURL = prefs.getBoolPref("includeURL");
+ let originalEmail = prefs.getCharPref("email");
+
+ 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 email = doc.getElementById("email");
+ let emailMe = doc.getElementById("emailMe");
+ let includeURL = doc.getElementById("includeURL");
+
+ if (fieldValues.hasOwnProperty("comments")) {
+ comments.value = fieldValues.comments;
+ }
+
+ if (fieldValues.hasOwnProperty("email")) {
+ email.value = fieldValues.email;
+ }
+
+ if (fieldValues.hasOwnProperty("emailMe")) {
+ emailMe.checked = fieldValues.emailMe;
+ }
+
+ 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("emailMe", originalEmailMe);
+ prefs.setBoolPref("includeURL", originalIncludeURL);
+ prefs.setCharPref("email", originalEmail);
+ }
+ );
+}
+
+/**
+ * Tests what we send with the crash report by default. By default, we do not
+ * send any comments, the URL of the crashing page, or the email address of
+ * the user.
+ */
+add_task(async function test_default() {
+ await crashTabTestHelper(
+ {},
+ {
+ Comments: null,
+ URL: "",
+ Email: null,
+ }
+ );
+});
+
+/**
+ * Test just sending a comment.
+ */
+add_task(async function test_just_a_comment() {
+ await crashTabTestHelper(
+ {
+ comments: COMMENTS,
+ },
+ {
+ Comments: COMMENTS,
+ URL: "",
+ Email: null,
+ }
+ );
+});
+
+/**
+ * Test that we don't send email if emailMe is unchecked
+ */
+add_task(async function test_no_email() {
+ await crashTabTestHelper(
+ {
+ email: EMAIL,
+ emailMe: false,
+ },
+ {
+ Comments: null,
+ URL: "",
+ Email: null,
+ }
+ );
+});
+
+/**
+ * Test that we can send an email address if emailMe is checked
+ */
+add_task(async function test_yes_email() {
+ await crashTabTestHelper(
+ {
+ email: EMAIL,
+ emailMe: true,
+ },
+ {
+ Comments: null,
+ URL: "",
+ Email: EMAIL,
+ }
+ );
+});
+
+/**
+ * Test that we will send the URL of the page if includeURL is checked.
+ */
+add_task(async function test_send_URL() {
+ await crashTabTestHelper(
+ {
+ includeURL: true,
+ },
+ {
+ Comments: null,
+ URL: PAGE,
+ Email: null,
+ }
+ );
+});
+
+/**
+ * Test that we can send comments, the email address, and the URL
+ */
+add_task(async function test_send_all() {
+ await crashTabTestHelper(
+ {
+ includeURL: true,
+ emailMe: true,
+ email: EMAIL,
+ comments: COMMENTS,
+ },
+ {
+ Comments: COMMENTS,
+ URL: PAGE,
+ Email: EMAIL,
+ }
+ );
+});
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..cacbd069b1
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_shownRestartRequired.js
@@ -0,0 +1,114 @@
+"use strict";
+
+const PAGE =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+async function assertIsAtRestartRequiredPage(browser) {
+ let doc = browser.contentDocument;
+ // There's no guarantee that the about:restartrequired page
+ // has finished loading yet after the crash. If that's the
+ // case, wait for it. We wait for DOMContentLoaded, since error
+ // pages don't fire "load" events.
+ if (doc.readyState == "loading") {
+ await BrowserTestUtils.waitForEvent(
+ browser.contentWindow,
+ "DOMContentLoaded"
+ );
+ }
+ // 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;
+
+ await BrowserTestUtils.crashFrame(browser, false);
+ 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() {
+ 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;
+ 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.browserLoaded(browser, 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..fa261da434
--- /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_task(async function setup() {
+ 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..d59ace620f
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/head.js
@@ -0,0 +1,140 @@
+/**
+ * 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 =
+ "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 env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
+ let serverUrl = env.get("MOZ_CRASHREPORTER_URL");
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
+ env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
+
+ registerCleanupFunction(function() {
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
+ 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;
+ });
+}
diff --git a/browser/base/content/test/tabdialogs/.eslintrc.js b/browser/base/content/test/tabdialogs/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/tabdialogs/browser.ini b/browser/base/content/test/tabdialogs/browser.ini
new file mode 100644
index 0000000000..4c3369b022
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files =
+ subdialog.xhtml
+
+[browser_tabdialogbox_content_prompts.js]
+[browser_tabdialogbox_navigation.js]
+[browser_tabdialogbox_tab_switch_focus.js]
+[browser_subdialog_esc.js]
+support-files =
+ loadDelayedReply.sjs
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..5ad3335b50
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_subdialog_esc.js
@@ -0,0 +1,118 @@
+/* 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",
+ "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("http://example.com", async function(
+ browser
+ ) {
+ // Start loading a page
+ let loadStartedPromise = BrowserTestUtils.loadURI(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,
+ });
+
+ 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("http://example.com", async function(
+ browser
+ ) {
+ // Open the test dialog
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let dialogClose = dialogBox.open(TEST_DIALOG_PATH, {
+ keepOpenSameOriginNav: true,
+ });
+
+ 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.waitForEvent(
+ select,
+ "popupshowing",
+ true
+ );
+
+ info("Opening dropdown");
+ select.focus();
+ EventUtils.synthesizeKey("VK_SPACE", {}, dialog._frame.contentWindow);
+
+ await shownPromise;
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ select,
+ "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..5544853c91
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_content_prompts.js
@@ -0,0 +1,76 @@
+/* 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_URL = "data:text/html,<body onload='alert(1)'>";
+
+// Setup.
+add_task(async function setup() {
+ 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("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 that title text is shown in tabmodal alert/confirm/prompt dialogs.
+ */
+add_task(async function test_tabdialog_show_title() {
+ let dialogShown = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "DOMWillOpenModalDialog"
+ );
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async function(browser) {
+ info("Waiting for dialog to open.");
+ await dialogShown;
+
+ 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 infoTitle = dialogDoc.querySelector("#infoTitle");
+
+ ok(BrowserTestUtils.is_visible(infoTitle), "Title text is visible");
+ });
+});
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..7520875afd
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js
@@ -0,0 +1,162 @@
+/* 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),
+ dialogBox.open(TEST_DIALOG_PATH),
+ ];
+
+ 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.loadURI(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, {}, () => {
+ 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,
+ });
+
+ 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, {}, () => {
+ 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, {}, () => {
+ 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),
+ dialogBox.open(TEST_DIALOG_PATH),
+ ];
+
+ let dialogs = dialogBox.getTabDialogManager()._dialogs;
+
+ is(
+ dialogBox.getTabDialogManager()._dialogs.length,
+ 2,
+ "Dialog manager has two dialogs."
+ );
+
+ info("Waiting for dialogs to open.");
+ await Promise.all(dialogs.map(dialog => dialog._dialogReady));
+
+ ok(
+ !BrowserTestUtils.is_hidden(dialogBoxManager._dialogStack),
+ "Dialog stack is showing"
+ );
+
+ dialogBoxManager.hideDialog(browser);
+
+ is(
+ dialogBoxManager._dialogs.length,
+ 2,
+ "Dialog manager still has two dialogs."
+ );
+
+ ok(
+ BrowserTestUtils.is_hidden(dialogBoxManager._dialogStack),
+ "Dialog stack is hidden"
+ );
+
+ // Navigate to a different page
+ BrowserTestUtils.loadURI(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/browser_tabdialogbox_tab_switch_focus.js b/browser/base/content/test/tabdialogs/browser_tabdialogbox_tab_switch_focus.js
new file mode 100644
index 0000000000..2f19d48022
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_tab_switch_focus.js
@@ -0,0 +1,131 @@
+/* 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,
+ "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 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,
+ "http://example.com",
+ true
+ )
+ );
+ }
+
+ // Wait for tabs to be ready
+ let tabs = await Promise.all(tabPromises);
+
+ // Open subdialog in tabs
+ let dialogs = [];
+ let dialogBox, dialogBoxManager, browser;
+ for (let i = 0; i < 2; i += 1) {
+ dialogBox = gBrowser.getTabDialogBox(tabs[i].linkedBrowser);
+ browser = tabs[i].linkedBrowser;
+ dialogBox.open(TEST_DIALOG_PATH);
+ dialogBoxManager = dialogBox.getTabDialogManager();
+ dialogs.push(dialogBoxManager._topDialog);
+ }
+
+ // Wait for dialogs to be ready
+ await Promise.all([dialogs[0]._dialogReady, dialogs[1]._dialogReady]);
+
+ // Hide the top dialog
+ dialogBoxManager.hideDialog(browser);
+
+ ok(
+ BrowserTestUtils.is_hidden(dialogBoxManager._dialogStack),
+ "Dialog stack is hidden"
+ );
+
+ // Switch to first tab
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+
+ // Check the dialog stack is showing in first tab
+ dialogBoxManager = gBrowser
+ .getTabDialogBox(tabs[0].linkedBrowser)
+ .getTabDialogManager();
+ is(dialogBoxManager._dialogStack.hidden, false, "Dialog stack is showing");
+
+ // Cleanup
+ tabs.forEach(tab => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
diff --git a/browser/base/content/test/tabdialogs/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..3dfe537f47
--- /dev/null
+++ b/browser/base/content/test/tabdialogs/subdialog.xhtml
@@ -0,0 +1,33 @@
+<?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++;
+ }
+ </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>
+
+ <separator class="thin"/>
+
+ <button oncommand="window.close();" label="Close" />
+
+</dialog>
+</window>
diff --git a/browser/base/content/test/tabs/.eslintrc.js b/browser/base/content/test/tabs/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/tabs/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/tabs/204.sjs b/browser/base/content/test/tabs/204.sjs
new file mode 100644
index 0000000000..22b1d300e3
--- /dev/null
+++ b/browser/base/content/test/tabs/204.sjs
@@ -0,0 +1,3 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 204, "No Content");
+}
diff --git a/browser/base/content/test/tabs/blank.html b/browser/base/content/test/tabs/blank.html
new file mode 100644
index 0000000000..bcc2e389b8
--- /dev/null
+++ b/browser/base/content/test/tabs/blank.html
@@ -0,0 +1,2 @@
+<!doctype html>
+This page intentionally left blank.
diff --git a/browser/base/content/test/tabs/browser.ini b/browser/base/content/test/tabs/browser.ini
new file mode 100644
index 0000000000..0414f97d53
--- /dev/null
+++ b/browser/base/content/test/tabs/browser.ini
@@ -0,0 +1,122 @@
+[DEFAULT]
+support-files =
+ head.js
+ dummy_page.html
+ ../general/audio.ogg
+ file_mediaPlayback.html
+ test_process_flags_chrome.html
+ helper_origin_attrs_testing.js
+
+[browser_accessibility_indicator.js]
+skip-if = (verify && debug && (os == 'linux')) || (os == 'win' && processor == 'aarch64')
+[browser_addTab_index.js]
+[browser_allow_process_switches_despite_related_browser.js]
+[browser_audioTabIcon.js]
+tags = audiochannel
+[browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
+[browser_bug580956.js]
+[browser_close_during_beforeunload.js]
+[browser_close_tab_by_dblclick.js]
+[browser_contextmenu_openlink_after_tabnavigated.js]
+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]
+skip-if = verify
+support-files =
+ file_about_child.html
+ file_about_parent.html
+[browser_e10s_switchbrowser.js]
+[browser_e10s_about_process.js]
+[browser_e10s_mozillaweb_process.js]
+[browser_e10s_chrome_process.js]
+skip-if = debug # Bug 1444565, Bug 1457887
+[browser_e10s_javascript.js]
+[browser_file_to_http_script_closable.js]
+support-files = tab_that_closes.html
+[browser_file_to_http_named_popup.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_other_tabs.js]
+[browser_multiselect_tabs_close_tabs_to_the_right.js]
+[browser_multiselect_tabs_close_using_shortcuts.js]
+[browser_multiselect_tabs_close.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_to_another_window_drag.js]
+[browser_multiselect_tabs_move_to_new_window_contextmenu.js]
+[browser_multiselect_tabs_move.js]
+[browser_multiselect_tabs_mute_unmute.js]
+[browser_multiselect_tabs_open_related.js]
+[browser_multiselect_tabs_pin_unpin.js]
+[browser_multiselect_tabs_positional_attrs.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_keyboard.js]
+skip-if = os == 'mac' # Skipped because macOS keyboard support requires changing system settings
+[browser_multiselect_tabs_using_selectedTabs.js]
+[browser_multiselect_tabs_using_Shift_and_Ctrl.js]
+[browser_multiselect_tabs_using_Shift.js]
+[browser_navigate_home_focuses_addressbar.js]
+[browser_navigatePinnedTab.js]
+[browser_new_file_whitelisted_http_tab.js]
+skip-if = !e10s # Test only relevant for e10s.
+[browser_new_tab_insert_position.js]
+skip-if = (debug && os == 'linux' && bits == 32) #Bug 1455882, disabled on Linux32 for almost permafailing
+support-files = file_new_tab_page.html
+[browser_new_tab_in_privilegedabout_process_pref.js]
+skip-if = !e10s || (os == 'linux' && debug && bits == 64) || fission # Pref and test only relevant for e10s, Bug 1581500. Bug 1668809
+[browser_privilegedmozilla_process_pref.js]
+skip-if = !e10s # Pref and test only relevant for e10s.
+[browser_newwindow_tabstrip_overflow.js]
+[browser_open_newtab_start_observer_notification.js]
+[browser_opened_file_tab_navigated_to_web.js]
+[browser_overflowScroll.js]
+[browser_paste_event_at_middle_click_on_link.js]
+support-files = file_anchor_elements.html
+[browser_pinnedTabs_clickOpen.js]
+[browser_pinnedTabs_closeByKeyboard.js]
+[browser_pinnedTabs.js]
+[browser_positional_attributes.js]
+skip-if = (verify && (os == 'win' || os == 'mac'))
+[browser_preloadedBrowser_zoom.js]
+[browser_progress_keyword_search_handling.js]
+[browser_reload_deleted_file.js]
+skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
+[browser_tabCloseSpacer.js]
+skip-if = (os == 'linux' && bits == 64) || (os == 'win' && debug) || (os == "mac") #Bug 1549985
+[browser_tab_a11y_description.js]
+[browser_tab_label_during_reload.js]
+[browser_tab_manager_visibility.js]
+[browser_tabCloseProbes.js]
+[browser_tabContextMenu_keyboard.js]
+[browser_tabReorder_overflow.js]
+[browser_tabReorder.js]
+[browser_tabSpinnerProbe.js]
+skip-if = !e10s # Tab spinner is e10s only.
+[browser_tabSuccessors.js]
+[browser_tabSwitchPrintPreview.js]
+skip-if = os == 'mac'
+[browser_tabswitch_updatecommands.js]
+[browser_undo_close_tabs.js]
+skip-if = true #bug 1642084
+[browser_viewsource_of_data_URI_in_file_process.js]
+[browser_visibleTabs_bookmarkAllTabs.js]
+[browser_visibleTabs_contextMenu.js]
+[browser_tabswitch_window_focus.js]
+support-files = open_window_in_new_tab.html
+[browser_origin_attrs_in_remote_type.js]
+[browser_origin_attrs_rel.js]
+skip-if = (verify && os == 'mac' && webrender)
+support-files = file_rel_opener_noopener.html
+[browser_navigate_through_urls_origin_attributes.js]
+skip-if = (verify && os == 'mac' && webrender)
diff --git a/browser/base/content/test/tabs/browser_accessibility_indicator.js b/browser/base/content/test/tabs/browser_accessibility_indicator.js
new file mode 100644
index 0000000000..4187cb28b4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_accessibility_indicator.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const A11Y_INDICATOR_ENABLED_PREF = "accessibility.indicator.enabled";
+
+/**
+ * Test various pref and UI properties based on whether the accessibility
+ * indicator is enabled and the accessibility service is initialized.
+ * @param {Object} win browser window to check the indicator in.
+ * @param {Boolean} enabled pref flag for accessibility indicator.
+ * @param {Boolean} active whether accessibility service is started or not.
+ */
+function testIndicatorState(win, enabled, active) {
+ is(
+ Services.prefs.getBoolPref(A11Y_INDICATOR_ENABLED_PREF),
+ enabled,
+ `Indicator is ${enabled ? "enabled" : "disabled"}.`
+ );
+ is(
+ Services.appinfo.accessibilityEnabled,
+ active,
+ `Accessibility service is ${active ? "enabled" : "disabled"}.`
+ );
+
+ let visible = enabled && active;
+ is(
+ win.document.documentElement.hasAttribute("accessibilitymode"),
+ visible,
+ `accessibilitymode flag is ${visible ? "set" : "unset"}.`
+ );
+
+ // Browser UI has 2 indicators in markup for OSX and Windows but only 1 is
+ // shown depending on whether the titlebar is enabled.
+ let expectedVisibleCount = visible ? 1 : 0;
+ let visibleCount = 0;
+ [...win.document.querySelectorAll(".accessibility-indicator")].forEach(
+ indicator =>
+ win.getComputedStyle(indicator).getPropertyValue("display") !== "none" &&
+ visibleCount++
+ );
+ is(
+ expectedVisibleCount,
+ visibleCount,
+ `Indicator is ${visible ? "visible" : "invisible"}.`
+ );
+}
+
+/**
+ * Instantiate accessibility service and wait for event associated with its
+ * startup, if necessary.
+ */
+async function initAccessibilityService() {
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ if (!Services.appinfo.accessibilityEnabled) {
+ await new Promise(resolve => {
+ let observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ // "1" indicates that the accessibility service is initialized.
+ data === "1" && resolve();
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+ }
+
+ return accService;
+}
+
+/**
+ * If accessibility service is not yet disabled, wait for the event associated
+ * with its shutdown.
+ */
+async function shutdownAccessibilityService() {
+ if (Services.appinfo.accessibilityEnabled) {
+ await new Promise(resolve => {
+ let observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ // "1" indicates that the accessibility service is shutdown.
+ data === "0" && resolve();
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+ }
+}
+
+/**
+ * Force garbage collection.
+ */
+function forceGC() {
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+}
+
+add_task(async function test_accessibility_indicator() {
+ info("Preff on accessibility indicator.");
+ Services.prefs.setBoolPref(A11Y_INDICATOR_ENABLED_PREF, true);
+
+ info("Test default accessibility indicator state.");
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ testIndicatorState(window, true, false);
+ testIndicatorState(newWin, true, false);
+
+ info(
+ "Enable accessibility and ensure the indicator is shown in all windows."
+ );
+ let accService = await initAccessibilityService();
+ testIndicatorState(window, true, true);
+ testIndicatorState(newWin, true, true);
+
+ info("Open a new window and ensure the indicator is shown there by default.");
+ let dynamicWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ testIndicatorState(dynamicWin, true, true);
+ await BrowserTestUtils.closeWindow(dynamicWin);
+
+ info("Preff off accessibility indicator.");
+ Services.prefs.setBoolPref(A11Y_INDICATOR_ENABLED_PREF, false);
+ testIndicatorState(window, false, true);
+ testIndicatorState(newWin, false, true);
+ dynamicWin = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ testIndicatorState(dynamicWin, false, true);
+
+ info("Preff on accessibility indicator.");
+ Services.prefs.setBoolPref(A11Y_INDICATOR_ENABLED_PREF, true);
+ testIndicatorState(window, true, true);
+ testIndicatorState(newWin, true, true);
+ testIndicatorState(dynamicWin, true, true);
+
+ info(
+ "Disable accessibility and ensure the indicator is hidden in all windows."
+ );
+ accService = undefined; // eslint-disable-line no-unused-vars
+ forceGC();
+ await shutdownAccessibilityService();
+ testIndicatorState(window, true, false);
+ testIndicatorState(newWin, true, false);
+ testIndicatorState(dynamicWin, true, false);
+
+ Services.prefs.clearUserPref(A11Y_INDICATOR_ENABLED_PREF);
+ await BrowserTestUtils.closeWindow(newWin);
+ await BrowserTestUtils.closeWindow(dynamicWin);
+});
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_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..f91be177a1
--- /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.loadURI(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..99dac9fe4b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_audioTabIcon.js
@@ -0,0 +1,670 @@
+/* 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 = 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) {
+ let tooltip = document.getElementById("tabbrowser-tab-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"
+ );
+ EventUtils.synthesizeMouseAtCenter(toggleMute, {});
+ await popupHiddenPromise;
+ await mutedPromise;
+}
+
+async function test_playing_icon_on_tab(tab, browser, isPinned) {
+ let icon = isPinned ? tab.overlayIcon : tab.soundPlayingIcon;
+ let isActiveTab = tab === gBrowser.selectedTab;
+
+ await play(tab);
+
+ await test_tooltip(icon, "Mute tab", isActiveTab);
+
+ 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);
+
+ 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);
+
+ await test_mute_tab(tab, icon, true);
+
+ await pause(tab);
+
+ ok(
+ tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"),
+ "Tab should still be muted but not playing"
+ );
+ ok(
+ tab.muted && !tab.soundPlaying,
+ "Tab should still be muted but not playing"
+ );
+
+ await test_tooltip(icon, "Unmute tab", isActiveTab);
+
+ await test_mute_tab(tab, icon, false);
+
+ ok(
+ !tab.hasAttribute("muted") && !tab.hasAttribute("soundplaying"),
+ "Tab should not be be muted or playing"
+ );
+ ok(!tab.muted && !tab.soundPlaying, "Tab should not be be muted or playing");
+
+ // Make sure it's possible to mute using the context menu.
+ await test_muting_using_menu(tab, false);
+
+ // Make sure it's possible to unmute using the context menu.
+ await test_muting_using_menu(tab, true);
+}
+
+async function test_playing_icon_on_hidden_tab(tab) {
+ let oldSelectedTab = gBrowser.selectedTab;
+ let otherTabs = [
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
+ ];
+ let tabContainer = tab.container;
+ let alltabsButton = document.getElementById("alltabs-button");
+ let alltabsBadge = alltabsButton.badgeLabel;
+
+ function assertIconShowing() {
+ is(
+ getComputedStyle(alltabsBadge).backgroundImage,
+ 'url("chrome://browser/skin/tabbrowser/badge-audio-playing.svg")',
+ "The audio playing icon is shown"
+ );
+ is(
+ tabContainer.getAttribute("hiddensoundplaying"),
+ "true",
+ "There are hidden audio tabs"
+ );
+ }
+
+ function assertIconHidden() {
+ is(
+ getComputedStyle(alltabsBadge).backgroundImage,
+ "none",
+ "The audio playing icon is hidden"
+ );
+ ok(
+ !tabContainer.hasAttribute("hiddensoundplaying"),
+ "There are no hidden audio tabs"
+ );
+ }
+
+ // Keep the passed in tab selected.
+ gBrowser.selectedTab = tab;
+
+ // Play sound in the other two (visible) tabs.
+ await play(otherTabs[0]);
+ await play(otherTabs[1]);
+ assertIconHidden();
+
+ // Hide one of the noisy tabs, we see the icon.
+ await hide_tab(otherTabs[0]);
+ assertIconShowing();
+
+ // Hiding the other tab keeps the icon.
+ await hide_tab(otherTabs[1]);
+ assertIconShowing();
+
+ // Pausing both tabs will hide the icon.
+ await pause(otherTabs[0]);
+ assertIconShowing();
+ await pause(otherTabs[1]);
+ assertIconHidden();
+
+ // The icon returns when audio starts again.
+ await play(otherTabs[0]);
+ await play(otherTabs[1]);
+ assertIconShowing();
+
+ // There is still an icon after hiding one tab.
+ await show_tab(otherTabs[0]);
+ assertIconShowing();
+
+ // The icon is hidden when both of the tabs are shown.
+ await show_tab(otherTabs[1]);
+ assertIconHidden();
+
+ await BrowserTestUtils.removeTab(otherTabs[0]);
+ await BrowserTestUtils.removeTab(otherTabs[1]);
+
+ // Make sure we didn't change the selected tab.
+ gBrowser.selectedTab = oldSelectedTab;
+}
+
+async function test_swapped_browser_while_playing(oldTab, newBrowser) {
+ // The tab was muted so it won't have soundplaying attribute even it's playing.
+ ok(
+ oldTab.hasAttribute("muted"),
+ "Expected the correct muted attribute on the old tab"
+ );
+ is(
+ oldTab.muteReason,
+ null,
+ "Expected the correct muteReason attribute on the old tab"
+ );
+ ok(
+ !oldTab.hasAttribute("soundplaying"),
+ "Expected the correct soundplaying attribute on the old tab"
+ );
+
+ let newTab = gBrowser.getTabForBrowser(newBrowser);
+ let AttrChangePromise = BrowserTestUtils.waitForEvent(
+ newTab,
+ "TabAttrModified",
+ false,
+ event => {
+ return event.detail.changed.includes("muted");
+ }
+ );
+
+ gBrowser.swapBrowsersAndCloseOther(newTab, oldTab);
+ await AttrChangePromise;
+
+ ok(
+ newTab.hasAttribute("muted"),
+ "Expected the correct muted attribute on the new tab"
+ );
+ is(
+ newTab.muteReason,
+ null,
+ "Expected the correct muteReason property on the new tab"
+ );
+ ok(
+ !newTab.hasAttribute("soundplaying"),
+ "Expected the correct soundplaying attribute on the new tab"
+ );
+
+ await test_tooltip(newTab.soundPlayingIcon, "Unmute tab", true);
+}
+
+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.soundPlayingIcon, "Unmute tab", true);
+}
+
+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.soundPlayingIcon, 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.loadURI(browser, "about:mozilla");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await soundPlayingStoppedPromise;
+
+ ok(
+ !tab.hasAttribute("soundplaying"),
+ "Tab should not be playing sound any more"
+ );
+ ok(!tab.soundPlaying, "Tab should not be playing sound any more");
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ taskFn
+ );
+}
+
+async function test_mute_keybinding() {
+ async function test_muting_using_keyboard(tab) {
+ let mutedPromise = get_wait_for_mute_promise(tab, true);
+ EventUtils.synthesizeKey("m", { ctrlKey: true });
+ await mutedPromise;
+ mutedPromise = get_wait_for_mute_promise(tab, false);
+ EventUtils.synthesizeKey("m", { ctrlKey: true });
+ await mutedPromise;
+ }
+ async function taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Make sure it's possible to mute before the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ // Start playback and wait for it to finish.
+ await play(tab);
+
+ // Make sure it's possible to mute after the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ // Pause playback and wait for it to finish.
+ await pause(tab);
+
+ // Make sure things work if the tab is pinned.
+ gBrowser.pinTab(tab);
+
+ // Make sure it's possible to mute before the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ // Start playback and wait for it to finish.
+ await play(tab);
+
+ // Make sure it's possible to mute after the tab is playing.
+ await test_muting_using_keyboard(tab);
+
+ gBrowser.unpinTab(tab);
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ taskFn
+ );
+}
+
+async function test_on_browser(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Test the icon in a normal tab.
+ await test_playing_icon_on_tab(tab, browser, false);
+
+ gBrowser.pinTab(tab);
+
+ // Test the icon in a pinned tab.
+ await test_playing_icon_on_tab(tab, browser, true);
+
+ gBrowser.unpinTab(tab);
+
+ // Test the sound playing icon for hidden tabs.
+ await test_playing_icon_on_hidden_tab(tab);
+
+ // Retest with another browser in the foreground tab
+ if (gBrowser.selectedBrowser.currentURI.spec == PAGE) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html,test",
+ },
+ () => test_on_browser(browser)
+ );
+ } else {
+ await test_browser_swapping(tab, browser);
+ }
+}
+
+async function test_delayed_tabattr_removal() {
+ async function taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await play(tab);
+
+ // Extend the delay to guarantee the soundplaying attribute
+ // is not removed from the tab when audio is stopped. Without
+ // the extended delay the attribute could be removed in the
+ // same tick and the test wouldn't catch that this broke.
+ await pause(tab, { extendedDelay: true });
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ taskFn
+ );
+}
+
+requestLongerTimeout(2);
+add_task(async function test_page() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ test_on_browser
+ );
+});
+
+add_task(test_click_on_pinned_tab_after_mute);
+
+add_task(test_cross_process_load);
+
+add_task(test_mute_keybinding);
+
+add_task(test_delayed_tabattr_removal);
diff --git a/browser/base/content/test/tabs/browser_bug580956.js b/browser/base/content/test/tabs/browser_bug580956.js
new file mode 100644
index 0000000000..66bc9197ce
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_bug580956.js
@@ -0,0 +1,25 @@
+function numClosedTabs() {
+ return SessionStore.getClosedTabCount(window);
+}
+
+function isUndoCloseEnabled() {
+ updateTabContextMenu();
+ return !document.getElementById("context_undoCloseTab").disabled;
+}
+
+add_task(async function test() {
+ Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", 0);
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ is(numClosedTabs(), 0, "There should be 0 closed tabs.");
+ ok(!isUndoCloseEnabled(), "Undo Close Tab should be disabled.");
+
+ var tab = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/");
+ var browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ ok(isUndoCloseEnabled(), "Undo Close Tab should be enabled.");
+});
diff --git a/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js b/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js
new file mode 100644
index 0000000000..4c9ff7d9b0
--- /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.import(
+ "resource:///modules/sessionstore/TabState.jsm"
+);
+
+/**
+ * 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..293b784f1a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_close_during_beforeunload.js
@@ -0,0 +1,40 @@
+"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 DIALOG_TOPIC = "tabmodal-dialog-loaded";
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let browser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(browser, "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..10649eeb45
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js
@@ -0,0 +1,57 @@
+"use strict";
+
+const example_base =
+ "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.loadURI(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let awaitNewTabOpen = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://example.com/",
+ true
+ );
+
+ info("Click the 'open link in new tab' menu item");
+ let openLinkMenuItem = contextMenu.querySelector("#context-openlinkintab");
+ openLinkMenuItem.click();
+
+ 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,
+ "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..9d231ae9b9
--- /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.loadURI(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.loadURI(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..d69d4ec5c4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js
@@ -0,0 +1,196 @@
+"use strict";
+
+const kChildPage = getRootDirectory(gTestPath) + "file_about_child.html";
+const kParentPage = getRootDirectory(gTestPath) + "file_about_parent.html";
+
+const kAboutPagesRegistered = Promise.all([
+ BrowserTestUtils.registerAboutPage(
+ registerCleanupFunction,
+ "test-about-principal-child",
+ kChildPage,
+ Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | Ci.nsIAboutModule.ALLOW_SCRIPT
+ ),
+ BrowserTestUtils.registerAboutPage(
+ registerCleanupFunction,
+ "test-about-principal-parent",
+ kParentPage,
+ Ci.nsIAboutModule.ALLOW_SCRIPT
+ ),
+]);
+
+add_task(async function test_principal_click() {
+ await kAboutPagesRegistered;
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.skip_about_page_has_csp_assert", true]],
+ });
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-principal-parent",
+ async function(browser) {
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "about:test-about-principal-child"
+ );
+ let myLink = browser.contentDocument.getElementById(
+ "aboutchildprincipal"
+ );
+ myLink.click();
+ await loadPromise;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.originalURI.asciiSpec,
+ "about:test-about-principal-child",
+ "sanity check - make sure we test the principal for the correct URI"
+ );
+
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "loading about: from privileged page must have a triggering of System"
+ );
+
+ let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+ is(
+ contentPolicyType,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "sanity check - loading a top level document"
+ );
+
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(
+ loadingPrincipal,
+ null,
+ "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal"
+ );
+ });
+ }
+ );
+});
+
+add_task(async function test_principal_ctrl_click() {
+ await kAboutPagesRegistered;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.sandbox.content.level", 1],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-principal-parent",
+ async function(browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:test-about-principal-child",
+ true
+ );
+ // simulate ctrl+click
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#aboutchildprincipal",
+ { ctrlKey: true, metaKey: true },
+ gBrowser.selectedBrowser
+ );
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.originalURI.asciiSpec,
+ "about:test-about-principal-child",
+ "sanity check - make sure we test the principal for the correct URI"
+ );
+
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "loading about: from privileged page must have a triggering of System"
+ );
+
+ let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+ is(
+ contentPolicyType,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "sanity check - loading a top level document"
+ );
+
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(
+ loadingPrincipal,
+ null,
+ "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal"
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_principal_right_click_open_link_in_new_tab() {
+ await kAboutPagesRegistered;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.sandbox.content.level", 1],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-principal-parent",
+ async function(browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:test-about-principal-child",
+ true
+ );
+
+ // simulate right-click open link in tab
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-openlinkintab").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#aboutchildprincipal",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+
+ let tab = await loadPromise;
+ gBrowser.selectTabAtIndex(2);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.originalURI.asciiSpec,
+ "about:test-about-principal-child",
+ "sanity check - make sure we test the principal for the correct URI"
+ );
+
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "loading about: from privileged page must have a triggering of System"
+ );
+
+ let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+ is(
+ contentPolicyType,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "sanity check - loading a top level document"
+ );
+
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(
+ loadingPrincipal,
+ null,
+ "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal"
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
diff --git a/browser/base/content/test/tabs/browser_e10s_about_process.js b/browser/base/content/test/tabs/browser_e10s_about_process.js
new file mode 100644
index 0000000000..7776e7c5ac
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_about_process.js
@@ -0,0 +1,181 @@
+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(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return new AboutModule().QueryInterface(aIID);
+ },
+
+ lockFactory(aLock) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+};
+
+add_task(async function init() {
+ 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..1af65621ac
--- /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.loadURI(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_task(async function init() {
+ 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.loadURI(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..d9bb12b966
--- /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..2d0d6de342
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_e10s_switchbrowser.js
@@ -0,0 +1,480 @@
+/* eslint-env mozilla/frame-script */
+
+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(uri) {
+ info("Loading " + uri);
+ // Longwinded but this ensures we don't just shortcut to LoadInNewProcess
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ gBrowser.selectedBrowser.webNavigation.loadURI(uri, loadURIOptions);
+
+ await BrowserTestUtils.browserStopped(gBrowser, uri);
+
+ // 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(
+ uri,
+ flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE
+) {
+ info("Loading " + uri + " flags = " + flags);
+ gBrowser.selectedBrowser.loadURI(uri, {
+ flags,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await BrowserTestUtils.browserStopped(gBrowser, uri);
+ 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;
+ 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
+ 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
+ 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);
+ 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;
+ 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.loadURI(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.loadURI(
+ gBrowser.selectedBrowser,
+ "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(
+ "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(
+ "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(
+ "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..1b4e2094b5
--- /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..cfd989d881
--- /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_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..fe869b50ec
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js
@@ -0,0 +1,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/. */
+
+async function addTab_example_com() {
+ 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..4eb1c409bc
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close.js
@@ -0,0 +1,122 @@
+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");
+
+ // 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);
+ menuItemCloseTab.click();
+ menu.hidePopup();
+ 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);
+});
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..a0d901113e
--- /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({ isLastMultiSelectChange: false });
+ 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_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..65daa229ae
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js
@@ -0,0 +1,48 @@
+add_task(async function test() {
+ let tab0 = gBrowser.selectedTab;
+ let tab1 = await addTab("http://example.com/1");
+ let tab2 = await addTab("http://example.com/2");
+ 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..33a373131b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js
@@ -0,0 +1,75 @@
+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..7497b26c2b
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js
@@ -0,0 +1,112 @@
+add_task(async function test() {
+ let originalTab = gBrowser.selectedTab;
+ let tab1 = await addTab("http://example.com/1");
+ let tab2 = await addTab("http://example.com/2");
+ 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,
+ "http://example.com/3",
+ true
+ );
+ window.TabContextMenu.contextTab = tab3; // Set proper context for command handler
+ menuItemDuplicateTab.click();
+ 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"
+ );
+ window.TabContextMenu.contextTab = tab3; // Set proper context for command handler
+ menuItemDuplicateTabs.click();
+ await newTabsOpened;
+ info("Two tabs opened");
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ getUrl(gBrowser.visibleTabs[4]) == "http://example.com/1" &&
+ 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..dc7f46f76f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js
@@ -0,0 +1,74 @@
+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);
+});
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..293d094480
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js
@@ -0,0 +1,126 @@
+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(
+ 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,
+ `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"),
+ `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..b007616c96
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js
@@ -0,0 +1,355 @@
+const PREF_DELAY_AUTOPLAY = "media.block-autoplay-until-in-foreground";
+const PAGE =
+ "https://example.com/browser/browser/base/content/test/tabs/file_mediaPlayback.html";
+
+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 addMediaTab() {
+ const tab = BrowserTestUtils.addTab(gBrowser, PAGE, { skipAnimation: true });
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return tab;
+}
+
+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.soundPlayingIcon;
+ 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.soundPlayingIcon;
+ 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.
+ // b) media-blocked tabs (tab1, tab2) will get playing. (media not blocked anymore)
+ // However tab4 (muted) which is not multiselected should not be affected.
+ let tab3MuteAudioBtn = tab3.soundPlayingIcon;
+ 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 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 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 become unmuted.
+ // b) unmuted tabs (tab3) will remain unmuted.
+ // b) media-blocked tabs (tab1, tab2) will get playing. (media not blocked anymore)
+ // However tab4 (muted) which is not multiselected should not be affected.
+ let tab2MuteAudioBtn = tab2.soundPlayingIcon;
+ await test_mute_tab(tab2, tab2MuteAudioBtn, 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 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, false);
+ 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 not muted");
+ ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked");
+ ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+ ok(muted(tab2), "Tab2 is muted");
+ ok(!muted(tab3, "Tab3 is not muted"));
+
+ let labels = ["Mute Tabs", "Play Tabs", "Unmute 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.label,
+ labels[i],
+ labels[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..74e6a7dc52
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js
@@ -0,0 +1,144 @@
+add_task(async function test() {
+ let tab1 = await addTab("http://example.com/1");
+ let tab2 = await addTab("http://example.com/2");
+ 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,
+ tab2,
+ "New tab should be opened after tab2 when tab1 and tab2 are multiselected"
+ );
+ is(
+ newTab.nextElementSibling,
+ tab3,
+ "New tab should be opened before tab3 when tab1 and tab2 are multiselected"
+ );
+ 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,
+ tab3,
+ "New tab should be opened after tab3 when tab1 and tab3 are selected"
+ );
+ let next = gBrowser.tabContainer.findNextTab(newTab, { direction: 1 });
+ is(
+ next,
+ null,
+ "New tab should be opened at the end of the tabstrip when tab1 and tab3 are selected"
+ );
+ 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_positional_attrs.js b/browser/base/content/test/tabs/browser_multiselect_tabs_positional_attrs.js
new file mode 100644
index 0000000000..c1f840d4d4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_positional_attrs.js
@@ -0,0 +1,50 @@
+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 checkBeforeMultiselectedAttributes() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let visibleTabs = gBrowser._visibleTabs;
+
+ await triggerClickOn(tab3, { ctrlKey: true });
+
+ is(visibleTabs.indexOf(tab1), 1, "The index of Tab1 is one");
+ is(visibleTabs.indexOf(tab2), 2, "The index of Tab2 is two");
+ is(visibleTabs.indexOf(tab3), 3, "The index of Tab3 is three");
+
+ ok(!tab1.multiselected, "Tab1 is not multi-selected");
+ ok(!tab2.multiselected, "Tab2 is not multi-selected");
+ ok(tab3.multiselected, "Tab3 is multi-selected");
+
+ ok(!tab1.beforeMultiselected, "Tab1 is not before-multiselected");
+ ok(tab2.beforeMultiselected, "Tab2 is before-multiselected");
+
+ info("Close Tab2");
+ let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
+ BrowserTestUtils.removeTab(tab2);
+ await tab2Closing;
+
+ // Cache invalidated, so we need to update the collection
+ visibleTabs = gBrowser._visibleTabs;
+
+ is(visibleTabs.indexOf(tab1), 1, "The index of Tab1 is one");
+ is(visibleTabs.indexOf(tab3), 2, "The index of Tab3 is two");
+ ok(tab1.beforeMultiselected, "Tab1 is before-multiselected");
+
+ // Checking if positional attributes are updated when "active" tab is clicked.
+ info("Click on the active tab to clear multiselect");
+ await triggerClickOn(gBrowser.selectedTab, {});
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+ ok(!tab1.beforeMultiselected, "Tab1 is not before-multiselected anymore");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab3);
+});
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..629b0f8de3
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js
@@ -0,0 +1,137 @@
+"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) {
+ 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");
+ EventUtils.synthesizeMouseAtCenter(
+ reopenItem,
+ { type: "mousemove" },
+ tab.ownerGlobal
+ );
+ 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}"]`
+ );
+ EventUtils.synthesizeMouseAtCenter(menuitem, {}, menuitem.ownerGlobal);
+ 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..3357871b35
--- /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..942bf75b1c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js
@@ -0,0 +1,142 @@
+/* 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_task(async function setup() {
+ // 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(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..8c65437796
--- /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..74b300c13d
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_navigatePinnedTab.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function() {
+ // Test 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,
+ "http://example.com/"
+ );
+ BrowserTestUtils.loadURI(appTab.linkedBrowser, "http://example.com/");
+ info("Started loading example.com");
+ await pageLoadPromise;
+ info("Loaded example.com");
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://example.org/"
+ );
+ await SpecialPowers.spawn(browser, [], async function() {
+ let link = content.document.createElement("a");
+ 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..a34608df47
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js
@@ -0,0 +1,16 @@
+/* -*- 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");
+
+ 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..9688a1d974
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_task(async function setup() {
+ 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],
+ // We want changes to this pref to be reverted at the end of the test
+ ["browser.tabs.remote.useOriginAttributesInRemoteType", false],
+ ],
+ });
+
+ requestLongerTimeout(4);
+
+ add_task(async function testWithOA() {
+ Services.prefs.setBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType",
+ true
+ );
+ await testNavigate();
+ });
+ if (gFissionBrowser) {
+ add_task(async function testWithoutOA() {
+ Services.prefs.setBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType",
+ false
+ );
+ await testNavigate();
+ });
+ }
+});
+
+function setupRemoteTypes() {
+ gPrevRemoteTypeRegularTab = null;
+ gPrevRemoteTypeContainerTab = {};
+ gPrevRemoteTypePrivateTab = null;
+
+ remoteTypes = getExpectedRemoteTypes(
+ gFissionBrowser,
+ NUM_PAGES_OPEN_FOR_EACH_TEST_CASE
+ );
+}
+
+async function testNavigate() {
+ setupRemoteTypes();
+ /**
+ * Open a regular tab, 3 container tabs and a private window, load about:blank
+ * 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("about:blank");
+ gPrevRemoteTypePrivateTab = privatePage.tab.linkedBrowser.remoteType;
+
+ for (const testCase of TEST_CASES) {
+ let uri = testCase.uri;
+
+ await loadURIAndCheckRemoteType(
+ regularPage.tab.linkedBrowser,
+ uri,
+ "regular tab",
+ gPrevRemoteTypeRegularTab
+ );
+ gPrevRemoteTypeRegularTab = regularPage.tab.linkedBrowser.remoteType;
+
+ for (const page of containerPages) {
+ await loadURIAndCheckRemoteType(
+ page.tab.linkedBrowser,
+ uri,
+ `container tab ${page.user_context_id}`,
+ gPrevRemoteTypeContainerTab[page.user_context_id]
+ );
+ gPrevRemoteTypeContainerTab[page.user_context_id] =
+ page.tab.linkedBrowser.remoteType;
+ }
+
+ await loadURIAndCheckRemoteType(
+ privatePage.tab.linkedBrowser,
+ uri,
+ "private tab",
+ gPrevRemoteTypePrivateTab
+ );
+ gPrevRemoteTypePrivateTab = privatePage.tab.linkedBrowser.remoteType;
+ }
+ // Close tabs
+ containerPages.forEach(containerPage => {
+ BrowserTestUtils.removeTab(containerPage.tab);
+ });
+ BrowserTestUtils.removeTab(regularPage.tab);
+ BrowserTestUtils.removeTab(privatePage.tab);
+}
+
+async function loadURIAndCheckRemoteType(
+ aBrowser,
+ aURI,
+ aText,
+ aPrevRemoteType
+) {
+ let expectedCurr = remoteTypes.shift();
+ initXulFrameLoaderCreatedCounter(xulFrameLoaderCreatedCounter);
+ aBrowser.ownerGlobal.gBrowser.addEventListener(
+ "XULFrameLoaderCreated",
+ handleEventLocal
+ );
+ let loaded = BrowserTestUtils.browserLoaded(aBrowser, false, aURI);
+ info(`About to load ${aURI} in ${aText}`);
+ await BrowserTestUtils.loadURI(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 ? 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..f9fe7d3bf9
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.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 TEST_HTTP = "http://example.org/";
+
+// Test for bug 1378377.
+add_task(async function() {
+ // Set prefs to ensure file content process.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.remote.separateFileUriProcess", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(TEST_HTTP, async function(fileBrowser) {
+ ok(
+ E10SUtils.isWebRemoteType(fileBrowser.remoteType),
+ "Check that tab normally has web remote type."
+ );
+ });
+
+ // Set prefs to whitelist TEST_HTTP for file:// URI use.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["capability.policy.policynames", "allowFileURI"],
+ ["capability.policy.allowFileURI.sites", TEST_HTTP],
+ ["capability.policy.allowFileURI.checkloaduri.enabled", "allAccess"],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(TEST_HTTP, async function(fileBrowser) {
+ is(
+ fileBrowser.remoteType,
+ E10SUtils.FILE_REMOTE_TYPE,
+ "Check that tab now has file remote type."
+ );
+ });
+});
diff --git a/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js
new file mode 100644
index 0000000000..404c18ddb6
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js
@@ -0,0 +1,222 @@
+/* -*- 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";
+const TEST_HTTP = "http://example.org/";
+
+add_task(async function setup() {
+ 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.PRIVILEGEDABOUT_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.loadURI(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(browser) {
+ checkBrowserRemoteType(browser, E10SUtils.PRIVILEGEDABOUT_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,
+ ABOUT_NEWTAB,
+ true
+ );
+ await SpecialPowers.spawn(browser, [ABOUT_NEWTAB], 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 about:newtab is loaded in privileged about: content process."
+ );
+
+ // Check that reload does not break the privileged about: content process affinity.
+ BrowserReload();
+ await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB);
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that about:newtab is still in privileged about: content process after reload."
+ );
+
+ // Load http webpage
+ BrowserTestUtils.loadURI(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.waitForEvent(newTab, "SSTabRestored");
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that about:newtab is still in privileged about: content process 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.waitForEvent(newTab, "SSTabRestored");
+ 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."
+ );
+
+ BrowserTestUtils.loadURI(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);
+ is(
+ browser.frameLoader.remoteTab.osPid,
+ privilegedPid,
+ "Check that about:newtab is in privileged about: content process 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..edc1bf147c
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_insert_position.js
@@ -0,0 +1,295 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "SessionStore",
+ "resource:///modules/sessionstore/SessionStore.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "TabStateFlusher",
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "E10SUtils",
+ "resource://gre/modules/E10SUtils.jsm"
+);
+
+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 },
+ ],
+ },
+ ],
+ },
+ ],
+};
+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",
+ "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,
+ "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,
+ "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_newwindow_tabstrip_overflow.js b/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js
new file mode 100644
index 0000000000..cd65bc04ef
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.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 DEFAULT_THEME = "default-theme@mozilla.org";
+
+const { AddonManager } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+async function selectTheme(id) {
+ let theme = await AddonManager.getAddonByID(id || DEFAULT_THEME);
+ await theme.enable();
+}
+
+registerCleanupFunction(() => {
+ return selectTheme(null);
+});
+
+add_task(async function withoutLWT() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ ok(
+ !win.gBrowser.tabContainer.hasAttribute("overflow"),
+ "tab container not overflowing"
+ );
+ ok(
+ !win.gBrowser.tabContainer.arrowScrollbox.hasAttribute("overflowing"),
+ "arrow scrollbox not overflowing"
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function withLWT() {
+ await selectTheme("firefox-compact-light@mozilla.org");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ ok(
+ !win.gBrowser.tabContainer.hasAttribute("overflow"),
+ "tab container not overflowing"
+ );
+ ok(
+ !win.gBrowser.tabContainer.arrowScrollbox.hasAttribute("overflowing"),
+ "arrow scrollbox not overflowing"
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js
new file mode 100644
index 0000000000..cb9fc3c6d7
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js
@@ -0,0 +1,28 @@
+"use strict";
+
+add_task(async function test_browser_open_newtab_start_observer_notification() {
+ let observerFiredPromise = new Promise(resolve => {
+ function observe(subject) {
+ Services.obs.removeObserver(observe, "browser-open-newtab-start");
+ resolve(subject.wrappedJSObject);
+ }
+ Services.obs.addObserver(observe, "browser-open-newtab-start");
+ });
+
+ // We're calling BrowserOpenTab() (rather the using BrowserTestUtils
+ // because we want to be sure that it triggers the event to fire, since
+ // it's very close to where various user-actions are triggered.
+ BrowserOpenTab();
+ const newTabCreatedPromise = await observerFiredPromise;
+ const browser = await newTabCreatedPromise;
+ const tab = gBrowser.selectedTab;
+
+ ok(true, "browser-open-newtab-start observer not called");
+ Assert.deepEqual(
+ browser,
+ tab.linkedBrowser,
+ "browser-open-newtab-start notified with the created browser"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js b/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js
new file mode 100644
index 0000000000..6ba9851965
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js
@@ -0,0 +1,54 @@
+/* -*- 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";
+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.
+ BrowserTestUtils.loadURI(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..6e12d87093
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.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";
+
+/* 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_task(async function setup() {
+ 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],
+ // We want changes to this pref to be reverted at the end of the test
+ ["browser.tabs.remote.useOriginAttributesInRemoteType", 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 });
+
+ add_task(async function testWithOA() {
+ Services.prefs.setBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType",
+ true
+ );
+ await test_user_identity_simple();
+ });
+ if (gFissionBrowser) {
+ add_task(async function testWithoutOA() {
+ Services.prefs.setBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType",
+ false
+ );
+ await test_user_identity_simple();
+ });
+ }
+});
+
+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
+}
+
+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..c63d99f7c5
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_origin_attrs_rel.js
@@ -0,0 +1,325 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_task(async function setup() {
+ 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],
+ // We want changes to this pref to be reverted at the end of the test
+ ["browser.tabs.remote.useOriginAttributesInRemoteType", false],
+ ],
+ });
+ requestLongerTimeout(3);
+
+ add_task(async function testWithOA() {
+ Services.prefs.setBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType",
+ true
+ );
+ await test_user_identity_simple();
+ });
+ if (gFissionBrowser) {
+ add_task(async function testWithoutOA() {
+ Services.prefs.setBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType",
+ false
+ );
+ await test_user_identity_simple();
+ });
+ }
+});
+
+function setupRemoteTypes() {
+ let useOriginAttributesInRemoteType = Services.prefs.getBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType"
+ );
+ if (gFissionBrowser && useOriginAttributesInRemoteType) {
+ 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 if (gFissionBrowser) {
+ remoteTypes = {
+ initial: [
+ ...Array(NUM_NAVIGATIONS).fill("webIsolated=https://example.com"),
+ ...Array(NUM_NAVIGATIONS).fill("webIsolated=https://example.org"),
+ ],
+ 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";
+ remoteTypes["1"][URI_EXAMPLEORG] = "webIsolated=https://example.org";
+ remoteTypes["2"][URI_EXAMPLECOM] = "webIsolated=https://example.com";
+ remoteTypes["2"][URI_EXAMPLEORG] = "webIsolated=https://example.org";
+ remoteTypes["3"][URI_EXAMPLECOM] = "webIsolated=https://example.com";
+ remoteTypes["3"][URI_EXAMPLEORG] = "webIsolated=https://example.org";
+ remoteTypes.private[URI_EXAMPLECOM] = "webIsolated=https://example.com";
+ remoteTypes.private[URI_EXAMPLEORG] = "webIsolated=https://example.org";
+ } 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";
+ }
+}
+
+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_overflowScroll.js b/browser/base/content/test/tabs/browser_overflowScroll.js
new file mode 100644
index 0000000000..91fe50e623
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_overflowScroll.js
@@ -0,0 +1,121 @@
+"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 originalSmoothScroll = arrowScrollbox.smoothScroll;
+ let tabMinWidth = parseInt(
+ getComputedStyle(gBrowser.selectedTab, null).minWidth
+ );
+
+ let rect = ele => ele.getBoundingClientRect();
+ let width = ele => rect(ele).width;
+
+ let tabCountForOverflow = Math.ceil(
+ (width(arrowScrollbox) / tabMinWidth) * 3
+ );
+
+ 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 window.promiseDocumentFlushed(() => {});
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ };
+
+ arrowScrollbox.smoothScroll = false;
+ registerCleanupFunction(() => {
+ arrowScrollbox.smoothScroll = originalSmoothScroll;
+ });
+
+ while (gBrowser.tabs.length < tabCountForOverflow) {
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true });
+ }
+
+ 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();
+ ok(
+ left(scrollbox) <= left(firstScrollable()),
+ "Selecting the first tab scrolls it into view " +
+ "(" +
+ left(scrollbox) +
+ " <= " +
+ left(firstScrollable()) +
+ ")"
+ );
+
+ element = nextRightElement();
+ EventUtils.synthesizeMouseAtCenter(downButton, {});
+ await waitForNextFrame();
+ isRight(element, "Scrolled one tab to the right with a single click");
+
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ await waitForNextFrame();
+ ok(
+ right(gBrowser.selectedTab) <= right(scrollbox),
+ "Selecting the last tab scrolls it into view " +
+ "(" +
+ right(gBrowser.selectedTab) +
+ " <= " +
+ right(scrollbox) +
+ ")"
+ );
+
+ element = nextLeftElement();
+ EventUtils.synthesizeMouseAtCenter(upButton, {});
+ await waitForNextFrame();
+ isLeft(element, "Scrolled one tab to the left with a single click");
+
+ let elementPoint = left(scrollbox) - width(scrollbox);
+ element = elementFromPoint(elementPoint);
+ element = element.nextElementSibling;
+
+ EventUtils.synthesizeMouseAtCenter(upButton, { clickCount: 2 });
+ await waitForNextFrame();
+ await BrowserTestUtils.waitForCondition(
+ () => !gBrowser.tabContainer.arrowScrollbox._isScrolling
+ );
+ isLeft(element, "Scrolled one page of tabs with a double click");
+
+ EventUtils.synthesizeMouseAtCenter(upButton, { clickCount: 3 });
+ await waitForNextFrame();
+ var firstScrollableLeft = left(firstScrollable());
+ ok(
+ left(scrollbox) <= firstScrollableLeft,
+ "Scrolled to the start with a triple click " +
+ "(" +
+ left(scrollbox) +
+ " <= " +
+ firstScrollableLeft +
+ ")"
+ );
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[0]);
+ }
+});
diff --git a/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js b/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js
new file mode 100644
index 0000000000..2c4cb724ee
--- /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",
+ "http://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,
+ "http://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,
+ "http://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,
+ "http://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,
+ "http://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..90d931a7c4
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_pinnedTabs.js
@@ -0,0 +1,93 @@
+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);
+
+ 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..50bdf92748
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_positional_attributes.js
@@ -0,0 +1,140 @@
+/* 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_task(async function setup() {
+ 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, "first-visible-tab", true);
+ testAttrib(4, "last-visible-tab", true);
+ testAttrib(0, "visuallyselected", true);
+ testAttrib(0, "beforeselected-visible", false);
+
+ await switchTab(2);
+
+ testAttrib(2, "visuallyselected", true);
+ testAttrib(1, "beforeselected-visible", true);
+
+ gBrowser.hideTab(gBrowser.tabs[1]);
+
+ testAttrib(0, "beforeselected-visible", true);
+
+ gBrowser.showTab(gBrowser.tabs[1]);
+
+ testAttrib(1, "beforeselected-visible", true);
+ testAttrib(0, "beforeselected-visible", false);
+
+ await switchTab(1);
+
+ testAttrib(0, "beforeselected-visible", true);
+
+ gBrowser.hideTab(gBrowser.tabs[0]);
+
+ testAttrib(0, "first-visible-tab", false);
+ testAttrib(1, "first-visible-tab", true);
+ testAttrib(0, "beforeselected-visible", false);
+
+ gBrowser.showTab(gBrowser.tabs[0]);
+
+ testAttrib(0, "first-visible-tab", true);
+ testAttrib(0, "beforeselected-visible", true);
+
+ gBrowser.moveTabTo(gBrowser.selectedTab, 3);
+
+ testAttrib(2, "beforeselected-visible", true);
+});
+
+add_task(async function test_hoverOne() {
+ await switchTab(0);
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[4], { type: "mousemove" });
+ testAttrib(3, "beforehovered", true);
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[3], { type: "mousemove" });
+ testAttrib(2, "beforehovered", true);
+ testAttrib(2, "afterhovered", false);
+ testAttrib(4, "afterhovered", true);
+ testAttrib(4, "beforehovered", false);
+ testAttrib(0, "beforehovered", false);
+ testAttrib(0, "afterhovered", false);
+ testAttrib(1, "beforehovered", false);
+ testAttrib(1, "afterhovered", false);
+ testAttrib(3, "beforehovered", false);
+ testAttrib(3, "afterhovered", false);
+});
+
+// Test that the afterhovered and beforehovered attributes are still there when
+// a tab is selected and then unselected again. See bug 856107.
+add_task(async function test_hoverStatePersistence() {
+ gBrowser.removeTab(tabs.pop());
+
+ function assertState() {
+ testAttrib(0, "beforehovered", true);
+ testAttrib(0, "afterhovered", false);
+ testAttrib(2, "afterhovered", true);
+ testAttrib(2, "beforehovered", false);
+ testAttrib(1, "beforehovered", false);
+ testAttrib(1, "afterhovered", false);
+ testAttrib(3, "beforehovered", false);
+ testAttrib(3, "afterhovered", false);
+ }
+
+ await switchTab(3);
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[1], { type: "mousemove" });
+ assertState();
+ await switchTab(1);
+ assertState();
+ await switchTab(3);
+ assertState();
+});
+
+add_task(async function test_pinning() {
+ testAttrib(3, "last-visible-tab", true);
+ testAttrib(3, "visuallyselected", true);
+ testAttrib(2, "beforeselected-visible", true);
+ // Causes gBrowser.tabs to change indices
+ gBrowser.pinTab(gBrowser.tabs[3]);
+ testAttrib(3, "last-visible-tab", true);
+ testAttrib(0, "first-visible-tab", true);
+ testAttrib(2, "beforeselected-visible", false);
+ testAttrib(0, "visuallyselected", true);
+ await switchTab(1);
+ testAttrib(0, "beforeselected-visible", 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..58be07b7a0
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js
@@ -0,0 +1,211 @@
+/* -*- 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/";
+const TEST_LOW1 = "http://example.org/";
+const TEST_LOW2 = "https://example.com/";
+
+add_task(async function setup() {
+ 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.loadURI(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.loadURI(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.loadURI(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..3385e8a71a
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kButton = document.getElementById("reload-button");
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.fixup.dns_first_for_single_words", true]],
+ });
+
+ await Services.search.init();
+
+ // Create an engine to use for the test.
+ await Services.search.addEngineWithDetails("MozSearch1", {
+ method: "GET",
+ template: "https://example.com/?q={searchTerms}",
+ });
+
+ let originalEngine = await Services.search.getDefault();
+ let engineDefault = Services.search.getEngineByName("MozSearch1");
+ await Services.search.setDefault(engineDefault);
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engineDefault);
+ });
+});
+
+/*
+ * 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;
+ ok(kButton.hasAttribute("displaystop"), "Should be showing stop");
+
+ await BrowserTestUtils.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,
+ "http://" + kNonExistingHost + "/",
+ true /* want an error page */
+ );
+ gURLBar.value = kNonExistingHost;
+ gURLBar.select();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ await BrowserTestUtils.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_reload_deleted_file.js b/browser/base/content/test/tabs/browser_reload_deleted_file.js
new file mode 100644
index 0000000000..15d4ae5f74
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_reload_deleted_file.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+);
+
+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_tabCloseProbes.js b/browser/base/content/test/tabs/browser_tabCloseProbes.js
new file mode 100644
index 0000000000..3bc547ccb8
--- /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_task(async function setup() {
+ // 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..39f0a2860f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabCloseSpacer.js
@@ -0,0 +1,99 @@
+/* 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;
+
+ await overflowTabs();
+ 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.deepEqual(
+ lastCloseButtonLocation,
+ originalCloseButtonLocation,
+ "Close button hasn't moved"
+ );
+
+ 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");
+});
+
+async function overflowTabs() {
+ let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+ const originalSmoothScroll = arrowScrollbox.smoothScroll;
+ arrowScrollbox.smoothScroll = false;
+ registerCleanupFunction(() => {
+ arrowScrollbox.smoothScroll = originalSmoothScroll;
+ });
+
+ let width = ele => ele.getBoundingClientRect().width;
+ let tabMinWidth = parseInt(
+ getComputedStyle(gBrowser.selectedTab, null).minWidth
+ );
+ let tabCountForOverflow = Math.ceil(
+ (width(arrowScrollbox) / tabMinWidth) * 1.1
+ );
+ while (gBrowser.tabs.length < tabCountForOverflow) {
+ BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ index: 0,
+ });
+ }
+
+ // Make sure scrolling finished.
+ await new Promise(resolve => {
+ arrowScrollbox.addEventListener("scrollend", resolve, { once: true });
+ });
+}
+
+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..81de93e866
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabContextMenu_keyboard.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/. */
+
+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() {
+ // 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..8e6b55119b
--- /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..b328040425
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabReorder_overflow.js
@@ -0,0 +1,60 @@
+/* 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);
+
+ 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 }
+ ));
+
+ while (gBrowser.tabs.length < tabCountForOverflow) {
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true });
+ }
+
+ 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..0a27b6f3ea
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js
@@ -0,0 +1,100 @@
+"use strict";
+
+/**
+ * Tests the FX_TAB_SWITCH_SPINNER_VISIBLE_MS and
+ * FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS telemetry probes
+ */
+const MIN_HANG_TIME = 500; // ms
+const MAX_HANG_TIME = 5 * 1000; // ms
+
+/**
+ * Returns the sum of all values in an array.
+ * @param {Array} aArray An array of integers
+ * @return {Number} The sum of the integers in the array
+ */
+function sum(aArray) {
+ return aArray.reduce(function(previousValue, currentValue) {
+ return previousValue + currentValue;
+ });
+}
+
+/**
+ * Causes the content process for a remote <xul:browser> to run
+ * some busy JS for aMs milliseconds.
+ *
+ * @param {<xul:browser>} browser
+ * The browser that's running in the content process that we're
+ * going to hang.
+ * @param {int} aMs
+ * The amount of time, in milliseconds, to hang the content process.
+ *
+ * @return {Promise}
+ * Resolves once the hang is done.
+ */
+function hangContentProcess(browser, aMs) {
+ return ContentTask.spawn(browser, aMs, function(ms) {
+ let then = Date.now();
+ while (Date.now() - then < ms) {
+ // Let's burn some CPU...
+ }
+ });
+}
+
+/**
+ * A generator intended to be run as a Task. It tests one of the tab spinner
+ * telemetry probes.
+ * @param {String} aProbe The probe to test. Should be one of:
+ * - FX_TAB_SWITCH_SPINNER_VISIBLE_MS
+ * - FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS
+ */
+async function testProbe(aProbe) {
+ info(`Testing probe: ${aProbe}`);
+ let histogram = Services.telemetry.getHistogramById(aProbe);
+ let delayTime = MIN_HANG_TIME + 1; // Pick a bucket arbitrarily
+
+ // The tab spinner does not show up instantly. We need to hang for a little
+ // bit of extra time to account for the tab spinner delay.
+ delayTime += gBrowser.selectedTab.linkedBrowser.getTabBrowser()._getSwitcher()
+ .TAB_SWITCH_TIMEOUT;
+
+ // In order for a spinner to be shown, the tab must have presented before.
+ let origTab = gBrowser.selectedTab;
+ let hangTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let hangBrowser = hangTab.linkedBrowser;
+ ok(hangBrowser.isRemoteBrowser, "New tab should be remote.");
+ ok(hangBrowser.frameLoader.remoteTab.hasPresented, "New tab has presented.");
+
+ // Now switch back to the original tab and set up our hang.
+ await BrowserTestUtils.switchTab(gBrowser, origTab);
+
+ let tabHangPromise = hangContentProcess(hangBrowser, delayTime);
+ histogram.clear();
+ let hangTabSwitch = BrowserTestUtils.switchTab(gBrowser, hangTab);
+ await tabHangPromise;
+ await hangTabSwitch;
+
+ // Now we should have a hang in our histogram.
+ let snapshot = histogram.snapshot();
+ BrowserTestUtils.removeTab(hangTab);
+ ok(
+ sum(Object.values(snapshot.values)) > 0,
+ `Spinner probe should now have a value in some bucket`
+ );
+}
+
+add_task(async function setup() {
+ 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_tabSwitchPrintPreview.js b/browser/base/content/test/tabs/browser_tabSwitchPrintPreview.js
new file mode 100644
index 0000000000..8b9e47461f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSwitchPrintPreview.js
@@ -0,0 +1,58 @@
+const kURL1 = "data:text/html,Should I stay or should I go?";
+const kURL2 = "data:text/html,I shouldn't be here!";
+
+/**
+ * Verify that if we open a new tab and try to make it the selected tab while
+ * print preview is up, that doesn't happen.
+ * Also check that we switch back to the original tab on exiting Print Preview.
+ */
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", false]],
+ });
+ await BrowserTestUtils.withNewTab(kURL1, async function(browser) {
+ let originalTab = gBrowser.selectedTab;
+ let tab = BrowserTestUtils.addTab(gBrowser, kURL2);
+ document.getElementById("cmd_printPreview").doCommand();
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.waitForCondition(
+ () => gInPrintPreviewMode,
+ "should be in print preview mode"
+ );
+ isnot(
+ gBrowser.selectedTab,
+ tab,
+ "Selected tab should not be the tab we added"
+ );
+ is(
+ gBrowser.selectedTab,
+ PrintPreviewListener._printPreviewTab,
+ "Selected tab should be the print preview tab"
+ );
+ gBrowser.selectedTab = tab;
+ isnot(
+ gBrowser.selectedTab,
+ tab,
+ "Selected tab should still not be the tab we added"
+ );
+ is(
+ gBrowser.selectedTab,
+ PrintPreviewListener._printPreviewTab,
+ "Selected tab should still be the print preview tab"
+ );
+ let tabSwitched = BrowserTestUtils.switchTab(gBrowser, () => {
+ PrintUtils.exitPrintPreview();
+ });
+ await BrowserTestUtils.waitForCondition(
+ () => !gInPrintPreviewMode,
+ "should no longer be in print preview mode"
+ );
+ await tabSwitched;
+ is(
+ gBrowser.selectedTab,
+ originalTab,
+ "Selected tab should be back to the original tab that we print previewed"
+ );
+ BrowserTestUtils.removeTab(tab);
+ });
+});
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..88694e5643
--- /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_manager_visibility.js b/browser/base/content/test/tabs/browser_tab_manager_visibility.js
new file mode 100644
index 0000000000..f408419392
--- /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.openNewWindowWithFlushedXULCacheForMozSupports();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: newWindow.gBrowser,
+ url: TEST_HOSTNAME + DUMMY_PAGE_PATH,
+ },
+ async function(browser) {
+ await Assert.ok(
+ BrowserTestUtils.is_visible(
+ newWindow.document.getElementById("alltabs-button")
+ ),
+ "tab manage menu is visible when browser.tabs.tabmanager.enabled preference is set to true"
+ );
+ }
+ );
+ Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled");
+ BrowserTestUtils.closeWindow(newWindow);
+});
+
+add_task(async function tab_manager_visibility_preference_off() {
+ Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", false);
+
+ let newWindow = await BrowserTestUtils.openNewWindowWithFlushedXULCacheForMozSupports();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: newWindow.gBrowser,
+ url: TEST_HOSTNAME + DUMMY_PAGE_PATH,
+ },
+ async function(browser) {
+ await Assert.ok(
+ BrowserTestUtils.is_hidden(
+ newWindow.document.getElementById("alltabs-button")
+ ),
+ "tab manage menu is hidden when browser.tabs.tabmanager.enabled preference is set to true"
+ );
+ }
+ );
+ Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled");
+ BrowserTestUtils.closeWindow(newWindow);
+});
diff --git a/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js b/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js
new file mode 100644
index 0000000000..22613f65ed
--- /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..1b4ff381c8
--- /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..b9359fe880
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_undo_close_tabs.js
@@ -0,0 +1,102 @@
+/* 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, "Two multiselected tabs");
+
+ gBrowser.removeMultiSelectedTabs();
+ 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;
+ 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 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_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..d7f9398e16
--- /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.loadURI(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..819a2ec109
--- /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..501172c984
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_closeOtherTabs").disabled,
+ "Close Other Tabs is enabled 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_closeOtherTabs").disabled,
+ "Close Other Tabs 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_closeOtherTabs").disabled,
+ "Close Other Tabs is enabled on 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/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_anchor_elements.html b/browser/base/content/test/tabs/file_anchor_elements.html
new file mode 100644
index 0000000000..99784fb9b9
--- /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="http://example.com/#a_with_href">anchor element</a></p>
+ <p contenteditable>Here is an <a id="editable_a_with_href" href="http://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="http://example.com/#non-editable_a_with_href">non-editable anchor element</a></span>
+ <p>Here is an <a id="a_with_name" name="a_with_name">anchor element without href</a></p>
+</body>
+</html>
diff --git a/browser/base/content/test/tabs/file_mediaPlayback.html b/browser/base/content/test/tabs/file_mediaPlayback.html
new file mode 100644
index 0000000000..a6979287e2
--- /dev/null
+++ b/browser/base/content/test/tabs/file_mediaPlayback.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<audio src="audio.ogg" controls loop>
diff --git a/browser/base/content/test/tabs/file_new_tab_page.html b/browser/base/content/test/tabs/file_new_tab_page.html
new file mode 100644
index 0000000000..4ef22a8c7c
--- /dev/null
+++ b/browser/base/content/test/tabs/file_new_tab_page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <a href="http://example.com/#linkclick" id="link_to_example_com">go to example.com</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/file_rel_opener_noopener.html b/browser/base/content/test/tabs/file_rel_opener_noopener.html
new file mode 100644
index 0000000000..78e872005c
--- /dev/null
+++ b/browser/base/content/test/tabs/file_rel_opener_noopener.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <a target="_blank" rel="noopener" href="https://example.com/browser/browser/base/content/test/tabs/blank.html" id="link_noopener_examplecom">Go to example.com</a>
+ <a target="_blank" rel="opener" href="https://example.com/browser/browser/base/content/test/tabs/blank.html" id="link_opener_examplecom">Go to example.com</a>
+ <a target="_blank" rel="noopener" href="https://example.org/browser/browser/base/content/test/tabs/blank.html" id="link_noopener_exampleorg">Go to example.org</a>
+ <a target="_blank" rel="opener" href="https://example.org/browser/browser/base/content/test/tabs/blank.html" id="link_opener_exampleorg">Go to example.org</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/tabs/head.js b/browser/base/content/test/tabs/head.js
new file mode 100644
index 0000000000..4897e9d65a
--- /dev/null
+++ b/browser/base/content/test/tabs/head.js
@@ -0,0 +1,514 @@
+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;
+}
+
+async function addTab(url = "http://mochi.test:8888/", params = {}) {
+ params.skipAnimation = true;
+ const tab = BrowserTestUtils.addTab(gBrowser, url, params);
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return tab;
+}
+
+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) {
+ let rect = tab2.getBoundingClientRect();
+ let event = {
+ ctrlKey: copy,
+ altKey: copy,
+ clientX: rect.left + rect.width / 2 + 10,
+ 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..0a97d9d5d9
--- /dev/null
+++ b/browser/base/content/test/tabs/helper_origin_attrs_testing.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/. */
+
+"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} in a private browsing tab`);
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ initXulFrameLoaderListenerInfo();
+ win.gBrowser.addEventListener("XULFrameLoaderCreated", handleEvent);
+
+ const browser = win.gBrowser.selectedTab.linkedBrowser;
+ let prevRemoteType = browser.remoteType;
+ BrowserTestUtils.loadURI(browser, uri);
+ await BrowserTestUtils.browserLoaded(browser, false, uri);
+ let currRemoteType = browser.remoteType;
+
+ info(
+ `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedListenerInfo.numCalledSoFar} time(s) for ${uri} in private tab`
+ );
+
+ 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;
+ let useOriginAttributesInRemoteType = Services.prefs.getBoolPref(
+ "browser.tabs.remote.useOriginAttributesInRemoteType"
+ );
+ if (gFissionBrowser && useOriginAttributesInRemoteType) {
+ 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 if (gFissionBrowser) {
+ remoteTypes = [
+ ...Array(numPagesOpen).fill("webIsolated=https://example.com"),
+ ...Array(numPagesOpen).fill("webIsolated=https://example.org"),
+ ];
+ } 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/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/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/touch/.eslintrc.js b/browser/base/content/test/touch/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/touch/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/touch/browser.ini b/browser/base/content/test/touch/browser.ini
new file mode 100644
index 0000000000..7b14c74211
--- /dev/null
+++ b/browser/base/content/test/touch/browser.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[browser_menu_touch.js]
+skip-if = !(os == 'win' && os_version == '10.0')
diff --git a/browser/base/content/test/touch/browser_menu_touch.js b/browser/base/content/test/touch/browser_menu_touch.js
new file mode 100644
index 0000000000..370c4b0268
--- /dev/null
+++ b/browser/base/content/test/touch/browser_menu_touch.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/. */
+
+/* This test checks that toolbar menus are in touchmode
+ * when opened through a touch event. */
+
+async function openAndCheckMenu(menu, target) {
+ is(menu.state, "closed", `Menu panel (${menu.id}) is initally closed.`);
+
+ let popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeNativeTapAtCenter(target);
+ await popupshown;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ is(
+ menu.getAttribute("touchmode"),
+ "true",
+ `Menu panel (${menu.id}) is in touchmode.`
+ );
+
+ menu.hidePopup();
+
+ popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(target, {});
+ await popupshown;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ ok(
+ !menu.hasAttribute("touchmode"),
+ `Menu panel (${menu.id}) is not in touchmode.`
+ );
+
+ menu.hidePopup();
+}
+
+async function openAndCheckLazyMenu(id, target) {
+ let menu = document.getElementById(id);
+
+ EventUtils.synthesizeNativeTapAtCenter(target);
+ let ev = await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ e => e.target.id == id
+ );
+ menu = ev.target;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ is(
+ menu.getAttribute("touchmode"),
+ "true",
+ `Menu panel (${menu.id}) is in touchmode.`
+ );
+
+ menu.hidePopup();
+
+ let popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(target, {});
+ await popupshown;
+
+ is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
+ ok(
+ !menu.hasAttribute("touchmode"),
+ `Menu panel (${menu.id}) is not in touchmode.`
+ );
+
+ menu.hidePopup();
+}
+
+// The customization UI menu is not attached to the document when it is
+// closed and hence requires special attention.
+async function openAndCheckCustomizationUIMenu(target) {
+ EventUtils.synthesizeNativeTapAtCenter(target);
+
+ await BrowserTestUtils.waitForCondition(
+ () => document.getElementById("customizationui-widget-panel") != null
+ );
+ let menu = document.getElementById("customizationui-widget-panel");
+
+ if (menu.state != "open") {
+ await BrowserTestUtils.waitForEvent(menu, "popupshown");
+ is(menu.state, "open", `Menu for ${target.id} is open`);
+ }
+
+ is(
+ menu.getAttribute("touchmode"),
+ "true",
+ `Menu for ${target.id} is in touchmode.`
+ );
+
+ menu.hidePopup();
+
+ EventUtils.synthesizeMouseAtCenter(target, {});
+
+ await BrowserTestUtils.waitForCondition(
+ () => document.getElementById("customizationui-widget-panel") != null
+ );
+ menu = document.getElementById("customizationui-widget-panel");
+
+ if (menu.state != "open") {
+ await BrowserTestUtils.waitForEvent(menu, "popupshown");
+ is(menu.state, "open", `Menu for ${target.id} is open`);
+ }
+
+ ok(
+ !menu.hasAttribute("touchmode"),
+ `Menu for ${target.id} is not in touchmode.`
+ );
+
+ menu.hidePopup();
+}
+
+// Ensure that we can run touch events properly for windows [10]
+add_task(async function setup() {
+ let isWindows = AppConstants.isPlatformAndVersionAtLeast("win", "10.0");
+ await SpecialPowers.pushPrefEnv({
+ set: [["apz.test.fails_with_native_injection", isWindows]],
+ });
+});
+
+// Test main ("hamburger") menu.
+add_task(async function test_main_menu_touch() {
+ let mainMenu = document.getElementById("appMenu-popup");
+ let target = document.getElementById("PanelUI-menu-button");
+ await openAndCheckMenu(mainMenu, target);
+});
+
+// Test the page action menu.
+add_task(async function test_page_action_panel_touch() {
+ // The page action menu only appears on a web page.
+ await BrowserTestUtils.withNewTab("https://example.com", async function() {
+ 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/webextensions/.eslintrc.js b/browser/base/content/test/webextensions/.eslintrc.js
new file mode 100644
index 0000000000..1f9a607b9e
--- /dev/null
+++ b/browser/base/content/test/webextensions/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+
+ env: {
+ webextensions: true,
+ },
+};
diff --git a/browser/base/content/test/webextensions/browser.ini b/browser/base/content/test/webextensions/browser.ini
new file mode 100644
index 0000000000..435a7c9afc
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files =
+ head.js
+ file_install_extensions.html
+ browser_legacy_webext.xpi
+ browser_webext_permissions.xpi
+ browser_webext_nopermissions.xpi
+ browser_webext_unsigned.xpi
+ browser_webext_update1.xpi
+ browser_webext_update2.xpi
+ browser_webext_update_icon1.xpi
+ browser_webext_update_icon2.xpi
+ browser_webext_update_perms1.xpi
+ browser_webext_update_perms2.xpi
+ browser_webext_update_origins1.xpi
+ browser_webext_update_origins2.xpi
+ browser_webext_update.json
+
+[browser_aboutaddons_blanktab.js]
+[browser_extension_sideloading.js]
+[browser_extension_update_background.js]
+[browser_extension_update_background_noprompt.js]
+[browser_permissions_dismiss.js]
+[browser_permissions_installTrigger.js]
+[browser_permissions_local_file.js]
+[browser_permissions_mozAddonManager.js]
+[browser_permissions_optional.js]
+skip-if = !e10s
+[browser_permissions_pointerevent.js]
+[browser_permissions_unsigned.js]
+skip-if = require_signing
+[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..0df174760b
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js
@@ -0,0 +1,405 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const { AddonManagerPrivate } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+AddonTestUtils.initMochitest(this);
+
+hookExtensionsTelemetry();
+AddonTestUtils.hookAMTelemetryEvents();
+
+async function createWebExtension(details) {
+ let options = {
+ manifest: {
+ applications: { 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) {
+ const { contentDocument: doc } = managerWindow.document.getElementById(
+ "html-view-browser"
+ );
+ return BrowserTestUtils.waitForCondition(
+ () => doc.querySelector(`addon-card[addon-id="${addonId}"]`),
+ `Found entry for sideload extension addon "${addonId}" in HTML about:addons`
+ );
+}
+
+function assertSideloadedAddonElementState(addonElement, checked) {
+ const enableBtn = addonElement.querySelector('[action="toggle-disabled"]');
+ is(
+ enableBtn.checked,
+ checked,
+ `The enable button is ${!checked ? " not " : ""} checked`
+ );
+ is(enableBtn.localName, "input", "The enable button is an input");
+ is(enableBtn.type, "checkbox", "It's a checkbox");
+}
+
+function clickEnableExtension(managerWindow, 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],
+ ["extensions.allowPrivateBrowsingByDefault", false],
+ ],
+ });
+
+ 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.loadURI(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function() {
+ // Return to about:blank when we're done
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ });
+
+ let changePromise = new Promise(resolve => {
+ ExtensionsUI.on("change", function listener() {
+ ExtensionsUI.off("change", listener);
+ resolve();
+ });
+ });
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // Check for the addons badge on the hamburger menu
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ is(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "Should have addon alert badge"
+ );
+
+ // Find the menu entries for sideloaded extensions
+ await gCUITestUtils.openMainMenu();
+
+ let addons = PanelUI.addonNotificationContainer;
+ is(
+ addons.children.length,
+ 3,
+ "Have 3 menu entries for sideloaded extensions"
+ );
+
+ info(
+ "Test disabling sideloaded addon 1 using the permission prompt secondary button"
+ );
+
+ // Click the first sideloaded extension
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addons.children[0].click();
+
+ // The click should hide the main menu. This is currently synchronous.
+ ok(PanelUI.panel.state != "open", "Main menu is closed or closing.");
+
+ // When we get the permissions prompt, we should be at the extensions
+ // list in about:addons
+ let panel = await popupPromise;
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "Foreground tab is at about:addons"
+ );
+
+ const VIEW = "addons://list/extension";
+ let win = gBrowser.selectedBrowser.contentWindow;
+
+ await BrowserTestUtils.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$/, [
+ ["webextPerms.hostDescription.allUrls"],
+ ["webextPerms.description.history"],
+ ]);
+
+ 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);
+
+ if (win.gViewController.isLoading) {
+ await new Promise(resolve =>
+ win.document.addEventListener("ViewChanged", resolve, { once: true })
+ );
+ }
+
+ // XUL or HTML 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(win, addonElement);
+ panel = await popupPromise;
+ checkNotification(panel, DEFAULT_ICON_URL, [
+ ["webextPerms.hostDescription.allUrls"],
+ ]);
+
+ // 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, [
+ ["webextPerms.hostDescription.allUrls"],
+ ]);
+
+ // 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);
+
+ // We should have recorded 1 cancelled followed by 2 accepted sideloads.
+ expectTelemetry(["sideloadRejected", "sideloadAccepted", "sideloadAccepted"]);
+
+ isnot(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "Should no longer have addon alert badge"
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ for (let addon of [addon1, addon2, addon3]) {
+ await addon.uninstall();
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Assert that the expected AddonManager telemetry are being recorded.
+ const expectedExtra = { source: "app-profile", method: "sideload" };
+
+ const baseEvent = { object: "extension", extra: expectedExtra };
+ const createBaseEventAddon = n => ({
+ ...baseEvent,
+ value: `addon${n}@tests.mozilla.org`,
+ });
+ const getEventsForAddonId = (events, addonId) =>
+ events.filter(ev => ev.value === addonId);
+
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+
+ // Test telemetry events for addon1 (1 permission and 1 origin).
+ info("Test telemetry events collected for addon1");
+
+ const baseEventAddon1 = createBaseEventAddon(1);
+ const collectedEventsAddon1 = getEventsForAddonId(
+ amEvents,
+ baseEventAddon1.value
+ );
+ const expectedEventsAddon1 = [
+ {
+ ...baseEventAddon1,
+ method: "sideload_prompt",
+ extra: { ...expectedExtra, num_strings: "2" },
+ },
+ { ...baseEventAddon1, method: "uninstall" },
+ ];
+
+ let i = 0;
+ for (let event of collectedEventsAddon1) {
+ Assert.deepEqual(
+ event,
+ expectedEventsAddon1[i++],
+ "Got the expected telemetry event"
+ );
+ }
+
+ is(
+ collectedEventsAddon1.length,
+ expectedEventsAddon1.length,
+ "Got the expected number of telemetry events for addon1"
+ );
+
+ const baseEventAddon2 = createBaseEventAddon(2);
+ const collectedEventsAddon2 = getEventsForAddonId(
+ amEvents,
+ baseEventAddon2.value
+ );
+ const expectedEventsAddon2 = [
+ {
+ ...baseEventAddon2,
+ method: "sideload_prompt",
+ extra: { ...expectedExtra, num_strings: "1" },
+ },
+ { ...baseEventAddon2, method: "enable" },
+ { ...baseEventAddon2, method: "uninstall" },
+ ];
+
+ i = 0;
+ for (let event of collectedEventsAddon2) {
+ Assert.deepEqual(
+ event,
+ expectedEventsAddon2[i++],
+ "Got the expected telemetry event"
+ );
+ }
+
+ is(
+ collectedEventsAddon2.length,
+ expectedEventsAddon2.length,
+ "Got the expected number of telemetry events for addon2"
+ );
+});
diff --git a/browser/base/content/test/webextensions/browser_extension_update_background.js b/browser/base/content/test/webextensions/browser_extension_update_background.js
new file mode 100644
index 0000000000..c88f74f092
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_update_background.js
@@ -0,0 +1,293 @@
+const { AddonManagerPrivate } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+AddonTestUtils.initMochitest(this);
+
+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 new Promise(resolve => {
+ function listener() {
+ if (win.gViewController.currentViewId != viewid) {
+ return;
+ }
+ win.document.removeEventListener("ViewChanged", listener);
+ resolve();
+ }
+ win.document.addEventListener("ViewChanged", listener);
+ });
+}
+
+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_task(async function setup() {
+ 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.loadURI(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function() {
+ // Return to about:blank when we're done
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ });
+});
+
+hookExtensionsTelemetry();
+AddonTestUtils.hookAMTelemetryEvents();
+
+// Helper function to test background updates.
+async function backgroundUpdateTest(url, id, checkIconFn) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Turn on background updates
+ ["extensions.update.enabled", true],
+
+ // Point updates to the local mochitest server
+ [
+ "extensions.update.background.url",
+ `${BASE}/browser_webext_update.json`,
+ ],
+ ],
+ });
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(url, {
+ source: FAKE_INSTALL_TELEMETRY_SOURCE,
+ });
+ let addonId = addon.id;
+
+ ok(addon, "Addon was installed");
+ is(getBadgeStatus(), "", "Should not start out with an addon alert badge");
+
+ // Trigger an update check and wait for the update for this addon
+ // to be downloaded.
+ let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+ await updatePromise;
+
+ is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
+
+ // Find the menu entry for the update
+ await gCUITestUtils.openMainMenu();
+
+ let addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 1, "Have a menu entry for the update");
+
+ // Click the menu item
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addons.children[0].click();
+
+ // The click should hide the main menu. This is currently synchronous.
+ ok(PanelUI.panel.state != "open", "Main menu is closed or closing.");
+
+ // about:addons should load and go to the list of extensions
+ let tab = await tabPromise;
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ "about:addons",
+ "Browser is at about:addons"
+ );
+
+ const VIEW = "addons://list/extension";
+ await promiseViewLoaded(tab, VIEW);
+ let win = tab.linkedBrowser.contentWindow;
+ ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
+ is(
+ win.gViewController.currentViewId,
+ VIEW,
+ "about:addons is at extensions list"
+ );
+
+ // Wait for the permission prompt, check the contents
+ let panel = await popupPromise;
+ checkIconFn(panel.getAttribute("icon"));
+
+ // The original extension has 1 promptable permission and the new one
+ // has 2 (history and <all_urls>) plus 1 non-promptable permission (cookies).
+ // So we should only see the 1 new promptable permission in the notification.
+ let list = document.getElementById("addon-webext-perm-list");
+ is(list.childElementCount, 1, "Permissions list contains 1 entry");
+
+ // 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");
+
+ // Should have recorded 1 canceled followed by 1 accepted update.
+ expectTelemetry(["updateRejected", "updateAccepted"]);
+
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ // Test that the expected telemetry events have been recorded (and that they include the
+ // permission_prompt event).
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+ const updateEvents = amEvents
+ .filter(evt => evt.method === "update")
+ .map(evt => {
+ delete evt.value;
+ return evt;
+ });
+
+ Assert.deepEqual(
+ updateEvents.map(evt => evt.extra && evt.extra.step),
+ [
+ // First update (cancelled).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "cancelled",
+ // Second update (completed).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "completed",
+ ],
+ "Got the steps from the collected telemetry events"
+ );
+
+ const method = "update";
+ const object = "extension";
+ const baseExtra = {
+ addon_id: addonId,
+ source: FAKE_INSTALL_TELEMETRY_SOURCE,
+ step: "permissions_prompt",
+ updated_from: "app",
+ };
+
+ // Expect the telemetry events to have num_strings set to 1, as only the origin permissions is going
+ // to be listed in the permission prompt.
+ Assert.deepEqual(
+ updateEvents.filter(
+ evt => evt.extra && evt.extra.step === "permissions_prompt"
+ ),
+ [
+ { method, object, extra: { ...baseExtra, num_strings: "1" } },
+ { method, object, extra: { ...baseExtra, num_strings: "1" } },
+ ],
+ "Got the expected permission_prompts events"
+ );
+}
+
+function checkDefaultIcon(icon) {
+ is(
+ icon,
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg",
+ "Popup has the default extension icon"
+ );
+}
+
+add_task(() =>
+ backgroundUpdateTest(
+ `${BASE}/browser_webext_update1.xpi`,
+ ID,
+ checkDefaultIcon
+ )
+);
+function checkNonDefaultIcon(icon) {
+ // The icon should come from the extension, don't bother with the precise
+ // path, just make sure we've got a jar url pointing to the right path
+ // inside the jar.
+ ok(icon.startsWith("jar:file://"), "Icon is a jar url");
+ ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
+}
+
+add_task(() =>
+ backgroundUpdateTest(
+ `${BASE}/browser_webext_update_icon1.xpi`,
+ ID_ICON,
+ checkNonDefaultIcon
+ )
+);
diff --git a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
new file mode 100644
index 0000000000..7a36abb0a6
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
@@ -0,0 +1,124 @@
+const { AddonManagerPrivate } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm",
+ {}
+);
+
+AddonTestUtils.initMochitest(this);
+
+hookExtensionsTelemetry();
+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_task(async function setup() {
+ 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.loadURI(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ registerCleanupFunction(async function() {
+ // Return to about:blank when we're done
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ });
+});
+
+// Helper function to test an upgrade that should not show a prompt
+async function testNoPrompt(origUrl, id) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Turn on background updates
+ ["extensions.update.enabled", true],
+
+ // Point updates to the local mochitest server
+ [
+ "extensions.update.background.url",
+ `${BASE}/browser_webext_update.json`,
+ ],
+ ],
+ });
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(origUrl);
+
+ ok(addon, "Addon was installed");
+
+ let sawPopup = false;
+ PopupNotifications.panel.addEventListener(
+ "popupshown",
+ () => (sawPopup = true),
+ { once: true }
+ );
+
+ // Trigger an update check and wait for the update to be applied.
+ let updatePromise = waitForUpdate(addon);
+ AddonManagerPrivate.backgroundUpdateCheck();
+ await updatePromise;
+
+ // There should be no notifications about the update
+ is(getBadgeStatus(), "", "Should not have addon alert badge");
+
+ await gCUITestUtils.openMainMenu();
+ let addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 0, "Have 0 updates in the PanelUI menu");
+ await gCUITestUtils.hideMainMenu();
+
+ ok(!sawPopup, "Should not have seen permissions notification");
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "2.0", "Update should have applied");
+
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ // Test that the expected telemetry events have been recorded (and that they do not
+ // include the permission_prompt event).
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+ const updateEventsSteps = amEvents
+ .filter(evt => {
+ return evt.method === "update" && evt.extra && evt.extra.addon_id == id;
+ })
+ .map(evt => {
+ return evt.extra.step;
+ });
+
+ // Expect telemetry events related to a completed update with no permissions_prompt event.
+ Assert.deepEqual(
+ updateEventsSteps,
+ ["started", "download_started", "download_completed", "completed"],
+ "Got the steps from the collected telemetry events"
+ );
+}
+
+// Test that an update that adds new non-promptable permissions is just
+// applied without showing a notification dialog.
+add_task(() =>
+ testNoPrompt(`${BASE}/browser_webext_update_perms1.xpi`, ID_PERMS)
+);
+
+// Test that an update that narrows origin permissions is just applied without
+// showing a notification promt
+add_task(() =>
+ testNoPrompt(`${BASE}/browser_webext_update_origins1.xpi`, ID_ORIGINS)
+);
diff --git a/browser/base/content/test/webextensions/browser_legacy_webext.xpi b/browser/base/content/test/webextensions/browser_legacy_webext.xpi
new file mode 100644
index 0000000000..a3bdf6f832
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_legacy_webext.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_permissions_dismiss.js b/browser/base/content/test/webextensions/browser_permissions_dismiss.js
new file mode 100644
index 0000000000..394ab8abe5
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_dismiss.js
@@ -0,0 +1,61 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+const INSTALL_XPI = `${BASE}/browser_webext_permissions.xpi`;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function test_tab_switch_dismiss() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, INSTALL_PAGE);
+
+ let installCanceled = new Promise(resolve => {
+ let listener = {
+ onInstallCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [INSTALL_XPI], function(url) {
+ content.wrappedJSObject.installMozAM(url);
+ });
+
+ await promisePopupNotificationShown("addon-webext-permissions");
+ let permsUL = document.getElementById("addon-webext-perm-list");
+ is(permsUL.childElementCount, 5, `Permissions list has 5 entries`);
+
+ let permsLearnMore = document.getElementById("addon-webext-perm-info");
+ ok(
+ BrowserTestUtils.is_visible(permsLearnMore),
+ "Learn more link is shown on Permission popup"
+ );
+ is(
+ permsLearnMore.href,
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "extension-permissions",
+ "Learn more link has desired URL"
+ );
+
+ // Switching tabs dismisses the notification and cancels the install.
+ let switchTo = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ BrowserTestUtils.removeTab(switchTo);
+ await installCanceled;
+
+ let addon = await AddonManager.getAddonByID("permissions@test.mozilla.org");
+ is(addon, null, "Extension is not installed");
+
+ BrowserTestUtils.removeTab(tab);
+});
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..143d252d11
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js
@@ -0,0 +1,18 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+
+async function installTrigger(filename) {
+ BrowserTestUtils.loadURI(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..438e917cbb
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_local_file.js
@@ -0,0 +1,87 @@
+"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 managerWin = await BrowserOpenAddonsMgr("addons://list/extension");
+
+ // Do the install...
+ await BrowserTestUtils.waitForEvent(managerWin.document, "ViewChanged");
+ let installButton = managerWin
+ .getHtmlBrowser()
+ .contentDocument.querySelector('[action="install-from-file"]');
+ installButton.click();
+}
+
+add_task(async function test_install_extension_from_local_file() {
+ // Clear any telemetry data that might be from a separate test.
+ Services.telemetry.clearEvents();
+
+ // 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"
+ );
+
+ // Check the telemetry.
+ let snapshot = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ );
+
+ // Make sure we got some data.
+ ok(
+ snapshot.parent && !!snapshot.parent.length,
+ "Got parent telemetry events in the snapshot"
+ );
+
+ // Only look at the related events after stripping the timestamp and category.
+ let relatedEvents = snapshot.parent
+ .filter(
+ ([timestamp, category, method, object]) =>
+ category == "addonsManager" &&
+ method == "action" &&
+ object == "aboutAddons"
+ )
+ .map(relatedEvent => relatedEvent.slice(4, 6));
+
+ // testInstallMethod installs the extension three times.
+ Assert.deepEqual(
+ relatedEvents,
+ [
+ [firstInstallId.toString(), { action: "installFromFile", view: "list" }],
+ [
+ (++firstInstallId).toString(),
+ { action: "installFromFile", view: "list" },
+ ],
+ [
+ (++firstInstallId).toString(),
+ { action: "installFromFile", view: "list" },
+ ],
+ ],
+ "The telemetry is recorded correctly"
+ );
+});
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..d66996c5b8
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js
@@ -0,0 +1,18 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+
+async function installMozAM(filename) {
+ BrowserTestUtils.loadURI(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..734cfd9d42
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_pointerevent.js
@@ -0,0 +1,59 @@
+/* -*- 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();
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["dom.w3c_pointer_events.enabled", true]] },
+ resolve
+ );
+ });
+ 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..ecb488f85a
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_permissions_unsigned.js
@@ -0,0 +1,53 @@
+"use strict";
+
+const ID = "permissions@test.mozilla.org";
+const WARNING_ICON = "chrome://browser/skin/warning.svg";
+
+add_task(async function test_unsigned() {
+ 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"));
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ BrowserTestUtils.loadURI(
+ 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);
+ checkPermissionString(
+ document.getElementById("addon-webext-perm-text").textContent,
+ "webextPerms.unsignedWarning",
+ null,
+ "Install notification includes unsigned warning"
+ );
+
+ // cancel the install
+ let promise = promiseInstallEvent({ id: ID }, "onInstallCancelled");
+ panel.secondaryButton.click();
+ await promise;
+
+ let addon = await AddonManager.getAddonByID(ID);
+ is(addon, null, "Extension is not installed");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/webextensions/browser_update_checkForUpdates.js b/browser/base/content/test/webextensions/browser_update_checkForUpdates.js
new file mode 100644
index 0000000000..b902527cae
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_update_checkForUpdates.js
@@ -0,0 +1,17 @@
+// Invoke the "Check for Updates" menu item
+function checkAll(win) {
+ triggerPageOptionsAction(win, "check-for-updates");
+ return new Promise(resolve => {
+ let observer = {
+ observe(subject, topic, data) {
+ Services.obs.removeObserver(observer, "EM-update-check-finished");
+ resolve();
+ },
+ };
+ Services.obs.addObserver(observer, "EM-update-check-finished");
+ });
+}
+
+// Test "Check for Updates" with both auto-update settings
+add_task(() => interactiveUpdateTest(true, checkAll));
+add_task(() => interactiveUpdateTest(false, checkAll));
diff --git a/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
new file mode 100644
index 0000000000..312899e732
--- /dev/null
+++ b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
@@ -0,0 +1,77 @@
+// Set some prefs that apply to all the tests in this file
+add_task(async function setup() {
+ 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.loadURI(gBrowser.selectedBrowser, "about:robots");
+ 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 BrowserTestUtils.waitForEvent(win.document, "ViewChanged");
+
+ 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..0ba36acb8b
--- /dev/null
+++ b/browser/base/content/test/webextensions/head.js
@@ -0,0 +1,689 @@
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonTestUtils",
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+
+var { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+);
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+ // eslint-disable-next-line no-shadow
+ const { Management } = ChromeUtils.import(
+ "resource://gre/modules/Extension.jsm",
+ null
+ );
+ return Management;
+});
+
+ChromeUtils.import(
+ "resource://testing-common/CustomizableUITestUtils.jsm",
+ this
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+/**
+ * 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.import(
+ "resource://gre/modules/AppMenuNotifications.jsm"
+ );
+ 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;
+}
+
+/**
+ * Trigger an action from the page options menu.
+ */
+function triggerPageOptionsAction(win, action) {
+ win
+ .getHtmlBrowser()
+ .contentDocument.querySelector(`#page-options [action="${action}"]`)
+ .click();
+}
+
+function isDefaultIcon(icon) {
+ // These are basically the same icon, but code within webextensions
+ // generates references to the former and generic add-ons manager code
+ // generates referces to the latter.
+ return (
+ icon == "chrome://browser/content/extension.svg" ||
+ icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg"
+ );
+}
+
+/**
+ * Check the contents of an individual permission string.
+ * This function is fairly specific to the use here and probably not
+ * suitable for re-use elsewhere...
+ *
+ * @param {string} string
+ * The string value to check (i.e., pulled from the DOM)
+ * @param {string} key
+ * The key in browser.properties for the localized string to
+ * compare with.
+ * @param {string|null} param
+ * Optional string to substitute for %S in the localized string.
+ * @param {string} msg
+ * The message to be emitted as part of the actual test.
+ */
+function checkPermissionString(string, key, param, msg) {
+ let localizedString = param
+ ? gBrowserBundle.formatStringFromName(key, [param])
+ : gBrowserBundle.GetStringFromName(key);
+
+ // If this is a parameterized string and the parameter isn't given,
+ // just do a simple comparison of the text before and after the %S
+ if (localizedString.includes("%S")) {
+ let i = localizedString.indexOf("%S");
+ ok(string.startsWith(localizedString.slice(0, i)), msg);
+ ok(string.endsWith(localizedString.slice(i + 2)), msg);
+ } else {
+ is(string, localizedString, msg);
+ }
+}
+
+/**
+ * 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., "webextPerms.description.foo") and an
+ * optional formatting parameter.
+ */
+function checkNotification(panel, checkIcon, permissions) {
+ let icon = panel.getAttribute("icon");
+ let ul = document.getElementById("addon-webext-perm-list");
+ let header = document.getElementById("addon-webext-perm-intro");
+ 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");
+ }
+
+ is(
+ ul.childElementCount,
+ permissions.length,
+ `Permissions list has ${permissions.length} entries`
+ );
+ if (!permissions.length) {
+ is(header.getAttribute("hidden"), "true", "Permissions header is hidden");
+ is(
+ learnMoreLink.getAttribute("hidden"),
+ "true",
+ "Permissions learn more is hidden"
+ );
+ } else {
+ is(header.getAttribute("hidden"), "", "Permissions header is visible");
+ is(
+ learnMoreLink.getAttribute("hidden"),
+ "",
+ "Permissions learn more is visible"
+ );
+ }
+
+ for (let i in permissions) {
+ let [key, param] = permissions[i];
+ checkPermissionString(
+ ul.children[i].textContent,
+ key,
+ param,
+ `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],
+ ],
+ });
+
+ if (telemetryBase !== undefined) {
+ hookExtensionsTelemetry();
+ }
+
+ 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$/, [
+ ["webextPerms.hostDescription.wildcard", "wildcard.domain"],
+ ["webextPerms.hostDescription.oneSite", "singlehost.domain"],
+ ["webextPerms.description.nativeMessaging"],
+ // The below permissions are deliberately in this order as permissions
+ // are sorted alphabetically by the permission string to match AMO.
+ ["webextPerms.description.history"],
+ ["webextPerms.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);
+
+ if (telemetryBase !== undefined) {
+ // Should see 2 canceled installs followed by 1 successful install
+ // for this method.
+ expectTelemetry([
+ `${telemetryBase}Rejected`,
+ `${telemetryBase}Rejected`,
+ `${telemetryBase}Accepted`,
+ ]);
+ }
+
+ await SpecialPowers.popPrefEnv();
+}
+
+// Helper function to test a specific scenario for interactive updates.
+// `checkFn` is a callable that triggers a check for updates.
+// `autoUpdate` specifies whether the test should be run with
+// updates applied automatically or not.
+async function interactiveUpdateTest(autoUpdate, checkFn) {
+ AddonTestUtils.initMochitest(this);
+
+ const ID = "update2@tests.mozilla.org";
+ const FAKE_INSTALL_SOURCE = "fake-install-source";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+
+ ["extensions.update.autoUpdateDefault", autoUpdate],
+
+ // Point updates to the local mochitest server
+ ["extensions.update.url", `${BASE}/browser_webext_update.json`],
+ ],
+ });
+
+ AddonTestUtils.hookAMTelemetryEvents();
+
+ // Trigger an update check, manually applying the update if we're testing
+ // without auto-update.
+ async function triggerUpdate(win, addon) {
+ let manualUpdatePromise;
+ if (!autoUpdate) {
+ manualUpdatePromise = new Promise(resolve => {
+ let listener = {
+ onNewInstall() {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+ }
+
+ let promise = checkFn(win, addon);
+
+ if (manualUpdatePromise) {
+ await manualUpdatePromise;
+
+ let doc = win.getHtmlBrowser().contentDocument;
+ 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 = BrowserTestUtils.waitForEvent(
+ win.document,
+ "ViewChanged"
+ );
+ 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.loadURI(gBrowser.selectedBrowser, "about:robots");
+ 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 BrowserTestUtils.waitForEvent(win.document, "ViewChanged");
+
+ // Trigger an update check
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ let { promise: checkPromise } = await triggerUpdate(win, addon);
+ let panel = await popupPromise;
+
+ // Click the cancel button, wait to see the cancel event
+ let cancelPromise = promiseInstallEvent(addon, "onInstallCancelled");
+ panel.secondaryButton.click();
+ await cancelPromise;
+
+ addon = await AddonManager.getAddonByID(ID);
+ is(addon.version, "1.0", "Should still be running the old version");
+
+ // Make sure the update check is completely finished.
+ await checkPromise;
+
+ // Trigger a new update check
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ checkPromise = (await triggerUpdate(win, addon)).promise;
+
+ // This time, accept the upgrade
+ let updatePromise = waitForUpdate(addon);
+ panel = await popupPromise;
+ panel.button.click();
+
+ addon = await updatePromise;
+ is(addon.version, "2.0", "Should have upgraded");
+
+ await checkPromise;
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ const collectedUpdateEvents = AddonTestUtils.getAMTelemetryEvents().filter(
+ evt => {
+ return evt.method === "update";
+ }
+ );
+
+ Assert.deepEqual(
+ collectedUpdateEvents.map(evt => evt.extra.step),
+ [
+ // First update is cancelled on the permission prompt.
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "cancelled",
+ // Second update is expected to be completed.
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "completed",
+ ],
+ "Got the expected sequence on update telemetry events"
+ );
+
+ ok(
+ collectedUpdateEvents.every(evt => evt.extra.addon_id === ID),
+ "Every update telemetry event should have the expected addon_id extra var"
+ );
+
+ ok(
+ collectedUpdateEvents.every(
+ evt => evt.extra.source === FAKE_INSTALL_SOURCE
+ ),
+ "Every update telemetry event should have the expected source extra var"
+ );
+
+ ok(
+ collectedUpdateEvents.every(evt => evt.extra.updated_from === "user"),
+ "Every update telemetry event should have the update_from extra var 'user'"
+ );
+
+ let hasPermissionsExtras = collectedUpdateEvents
+ .filter(evt => {
+ return evt.extra.step === "permissions_prompt";
+ })
+ .every(evt => {
+ return Number.isInteger(parseInt(evt.extra.num_strings, 10));
+ });
+
+ ok(
+ hasPermissionsExtras,
+ "Every 'permissions_prompt' update telemetry event should have the permissions extra vars"
+ );
+
+ let hasDownloadTimeExtras = collectedUpdateEvents
+ .filter(evt => {
+ return evt.extra.step === "download_completed";
+ })
+ .every(evt => {
+ const download_time = parseInt(evt.extra.download_time, 10);
+ return !isNaN(download_time) && download_time > 0;
+ });
+
+ ok(
+ hasDownloadTimeExtras,
+ "Every 'download_completed' update telemetry event should have a download_time extra vars"
+ );
+}
+
+// The tests in this directory install a bunch of extensions but they
+// need to uninstall them before exiting, as a stray leftover extension
+// after one test can foul up subsequent tests.
+// So, add a task to run before any tests that grabs a list of all the
+// add-ons that are pre-installed in the test environment and then checks
+// the list of installed add-ons at the end of the test to make sure no
+// new add-ons have been added.
+// Individual tests can store a cleanup function in the testCleanup global
+// to ensure it gets called before the final check is performed.
+let testCleanup;
+add_task(async function() {
+ 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();
+ }
+ }
+ });
+});
+
+let collectedTelemetry = [];
+function hookExtensionsTelemetry() {
+ let originalHistogram = ExtensionsUI.histogram;
+ ExtensionsUI.histogram = {
+ add(value) {
+ collectedTelemetry.push(value);
+ },
+ };
+ registerCleanupFunction(() => {
+ is(
+ collectedTelemetry.length,
+ 0,
+ "No unexamined telemetry after test is finished"
+ );
+ ExtensionsUI.histogram = originalHistogram;
+ });
+}
+
+function expectTelemetry(values) {
+ Assert.deepEqual(values, collectedTelemetry);
+ collectedTelemetry = [];
+}
diff --git a/browser/base/content/test/webrtc/.eslintrc.js b/browser/base/content/test/webrtc/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/webrtc/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/webrtc/browser.ini b/browser/base/content/test/webrtc/browser.ini
new file mode 100644
index 0000000000..3280e3eb38
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser.ini
@@ -0,0 +1,47 @@
+[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
+ single_peerconnection.html
+
+prefs =
+ privacy.webrtc.allowSilencingNotifications=true
+ privacy.webrtc.legacyGlobalIndicator=false
+ privacy.webrtc.sharedTabWarning=false
+
+[browser_device_controls_menus.js]
+[browser_devices_get_user_media.js]
+skip-if = (os == "linux" && debug) # linux: bug 976544
+[browser_devices_get_user_media_anim.js]
+[browser_devices_get_user_media_default_permissions.js]
+[browser_devices_get_user_media_in_frame.js]
+skip-if = debug # bug 1369731
+[browser_devices_get_user_media_in_xorigin_frame.js]
+skip-if = debug # bug 1369731
+[browser_devices_get_user_media_in_xorigin_frame_chain.js]
+[browser_devices_get_user_media_multi_process.js]
+skip-if = (debug && os == "win") # bug 1393761
+[browser_devices_get_user_media_paused.js]
+skip-if = (os == "win" && !debug) || (os =="linux" && !debug && bits == 64) # Bug 1440900
+[browser_devices_get_user_media_screen.js]
+skip-if = (os == 'linux') # Bug 1503991
+[browser_devices_get_user_media_screen_tab_close.js]
+[browser_devices_get_user_media_tear_off_tab.js]
+[browser_devices_get_user_media_unprompted_access.js]
+[browser_devices_get_user_media_unprompted_access_in_frame.js]
+[browser_devices_get_user_media_unprompted_access_tear_off_tab.js]
+skip-if = (os == "win" && bits == 64) # win8: bug 1334752
+[browser_devices_get_user_media_unprompted_access_queue_request.js]
+[browser_global_mute_toggles.js]
+[browser_indicator_popuphiding.js]
+[browser_notification_silencing.js]
+[browser_stop_sharing_button.js]
+[browser_stop_streams_on_indicator_close.js]
+[browser_tab_switch_warning.js]
+[browser_webrtc_hooks.js]
+[browser_devices_get_user_media_queue_request.js]
+[browser_WebrtcGlobalInformation.js]
+skip-if = (os == "win" && debug) # bug 1651716
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..f017ac05d2
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_WebrtcGlobalInformation.js
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserTestUtils: "resource://testing-common/BrowserTestUtils.jsm",
+});
+
+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");
+ }
+ };
+
+ reports.forEach(sanityCheckReport);
+ return reports;
+};
+
+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 => {
+ 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 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 })
+ );
+ await new Promise(r => (pc.onicecandidate = 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();
+});
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..6b186a142d
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_device_controls_menus.js
@@ -0,0 +1,54 @@
+/* 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;
+ });
+});
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..b78ebf449f
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -0,0 +1,837 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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(true, true);
+ let iconclass = PopupNotifications.panel.firstElementChild.getAttribute(
+ "iconclass"
+ );
+ ok(iconclass.includes("camera-icon"), "panel using devices icon");
+
+ 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(true);
+ let iconclass = PopupNotifications.panel.firstElementChild.getAttribute(
+ "iconclass"
+ );
+ ok(iconclass.includes("microphone-icon"), "panel using microphone icon");
+
+ 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(false, true);
+ let iconclass = PopupNotifications.panel.firstElementChild.getAttribute(
+ "iconclass"
+ );
+ ok(iconclass.includes("camera-icon"), "panel using devices icon");
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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 = [];
+ if (expectedMessage == "ok") {
+ observerPromises.push(expectObserverCalled("getUserMedia:request"));
+ observerPromises.push(
+ expectObserverCalled("getUserMedia:response:allow")
+ );
+ observerPromises.push(
+ expectObserverCalled("recording-device-events")
+ );
+ } else {
+ observerPromises.push(
+ 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 persistent permissions",
+ run: async function checkStopSharingRemovesPersistentPermissions() {
+ async function stopAndCheckPerm(aRequestAudio, 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);
+
+ 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 });
+
+ await stopSharing(aRequestVideo ? "camera" : "microphone");
+
+ // Check that permissions have been removed as expected.
+ let audioPerm = PermissionTestUtils.testExactPermission(
+ uri,
+ "microphone"
+ );
+ if (aRequestAudio) {
+ is(
+ audioPerm,
+ Services.perms.UNKNOWN_ACTION,
+ "microphone permissions removed"
+ );
+ } else {
+ is(
+ audioPerm,
+ Services.perms.ALLOW_ACTION,
+ "microphone permissions untouched"
+ );
+ }
+
+ let videoPerm = PermissionTestUtils.testExactPermission(uri, "camera");
+ if (aRequestVideo) {
+ is(
+ videoPerm,
+ Services.perms.UNKNOWN_ACTION,
+ "camera permissions removed"
+ );
+ } else {
+ is(
+ videoPerm,
+ Services.perms.ALLOW_ACTION,
+ "camera permissions untouched"
+ );
+ }
+
+ // Cleanup.
+ await closeStream(true);
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ }
+
+ info("request audio+video, stop sharing resets both");
+ await stopAndCheckPerm(true, true);
+ info("request audio, stop sharing resets audio only");
+ await stopAndCheckPerm(true, false);
+ info("request video, stop sharing resets video only");
+ await stopAndCheckPerm(false, true);
+ },
+ },
+
+ {
+ desc: "test showControlCenter",
+ run: async function checkShowControlCenter() {
+ if (!USING_LEGACY_INDICATOR) {
+ // The indicator only links to the control center for the
+ // legacy indicator.
+ return;
+ }
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(false, true);
+
+ 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(identityPopupHidden(), "control center should be hidden");
+ if (IS_MAC) {
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ } else {
+ let win = Services.wm.getMostRecentWindow(
+ "Browser:WebRTCGlobalIndicator"
+ );
+
+ let elt = win.document.getElementById("audioVideoButton");
+ EventUtils.synthesizeMouseAtCenter(elt, {}, win);
+ }
+
+ await TestUtils.waitForCondition(
+ () => !identityPopupHidden(),
+ "wait for control center to open"
+ );
+ ok(!identityPopupHidden(), "control center should be open");
+
+ gIdentityHandler._identityPopup.hidePopup();
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "'Always Allow' disabled on http pages",
+ run: async function checkNoAlwaysOnHttp() {
+ // Load an http page instead of the https version.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.devices.insecure.enabled", true],
+ ["media.getusermedia.insecure.enabled", true],
+ ],
+ });
+
+ // Disable while loading a new page
+ await disableObserverVerification();
+
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(
+ browser,
+ 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_default_permissions.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
new file mode 100644
index 0000000000..0390430399
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_default_permissions.js
@@ -0,0 +1,206 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ let observerPromise = expectObserverCalled("recording-window-ended");
+ let promise = promiseMessage(permissionError);
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ await checkNotSharing();
+
+ // Requesting only video shouldn't work.
+ observerPromise = expectObserverCalled("recording-window-ended");
+ promise = promiseMessage(permissionError);
+ await promiseRequestDevice(false, true);
+ await promise;
+ await observerPromise;
+ await checkNotSharing();
+
+ // Requesting audio should work.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ 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(true);
+ let iconclass = PopupNotifications.panel.firstElementChild.getAttribute(
+ "iconclass"
+ );
+ ok(iconclass.includes("microphone-icon"), "panel using microphone icon");
+
+ 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 });
+ 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.
+ let observerPromise = expectObserverCalled("recording-window-ended");
+ let promise = promiseMessage(permissionError);
+ await promiseRequestDevice(true, true);
+ await promise;
+ await observerPromise;
+ await checkNotSharing();
+
+ // Requesting only audio shouldn't work.
+ observerPromise = expectObserverCalled("recording-window-ended");
+ promise = promiseMessage(permissionError);
+ await promiseRequestDevice(true);
+ await promise;
+ await observerPromise;
+ await checkNotSharing();
+
+ // Requesting video should work.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ 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(false, true);
+ let iconclass = PopupNotifications.panel.firstElementChild.getAttribute(
+ "iconclass"
+ );
+ ok(iconclass.includes("camera-icon"), "panel using devices icon");
+
+ 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 });
+ 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_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
new file mode 100644
index 0000000000..83d90ade00
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
@@ -0,0 +1,653 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+SpecialPowers.pushPrefEnv({
+ set: [["permissions.delegation.enabled", true]],
+});
+
+// This test has been seen timing out locally in non-opt debug builds.
+requestLongerTimeout(2);
+
+var gTests = [
+ {
+ desc: "getUserMedia audio+video",
+ run: async function checkAudioVideo(aBrowser, aSubFrames) {
+ let { bc: frame1BC, id: frame1ID, observeBC: frame1ObserveBC } = (
+ await getBrowsingContextsAndFrameIdsForSubFrames(
+ aBrowser.browsingContext,
+ aSubFrames
+ )
+ )[0];
+
+ let observerPromise = expectObserverCalled(
+ "getUserMedia:request",
+ 1,
+ frame1ObserveBC
+ );
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, frame1ID, undefined, frame1BC);
+ await promise;
+ await observerPromise;
+
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon",
+ "anchored to device icon"
+ );
+ checkDeviceSelectors(true, true);
+ 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(true, true);
+
+ 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 });
+
+ 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: 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(false, true);
+
+ 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(false, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ // 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..71dba82989
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame.js
@@ -0,0 +1,793 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 });
+
+ // 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(false, false, true);
+ const notification = PopupNotifications.panel.firstElementChild;
+ const iconclass = notification.getAttribute("iconclass");
+ ok(iconclass.includes("screen-icon"), "panel using screen icon");
+
+ // 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" });
+ 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(true, true);
+
+ // 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(false, false, true);
+ const notification = PopupNotifications.panel.firstElementChild;
+ const iconclass = notification.getAttribute("iconclass");
+ ok(iconclass.includes("screen-icon"), "panel using screen icon");
+
+ // The 'Remember this decision' checkbox is visible.
+ const checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(!checkbox.hidden, "checkbox is visible");
+ ok(!checkbox.checked, "checkbox not checked");
+
+ const menulist = document.getElementById("webRTC-selectWindow-menulist");
+ const count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ const noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(!notification.button.disabled, "Allow button is enabled");
+
+ const indicator = promiseIndicatorWindow();
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () =>
+ EventUtils.synthesizeMouseAtCenter(notification.button, {})
+ );
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+ await closeStream(false, "frame1");
+ },
+ },
+
+ {
+ desc: "getUserMedia use persistent permissions from first party",
+ run: async function checkUsePersistentPermissionsFirstParty() {
+ async function checkPersistentPermission(
+ aPermission,
+ aRequestType,
+ aIframeId,
+ aExpect
+ ) {
+ info(
+ `Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
+ );
+ const uri = gBrowser.selectedBrowser.documentURI;
+ // Persistent allow/deny for first party uri
+ PermissionTestUtils.add(uri, aRequestType, aPermission);
+
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+ if (aExpect == PromptResult.PROMPT) {
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:deny"
+ );
+ const observerPromise2 = expectObserverCalled(
+ "recording-window-ended"
+ );
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(audio, video, aIframeId, screen);
+ await promise;
+ await observerPromise;
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .name,
+ uri.host,
+ "Use first party's origin"
+ );
+
+ // Deny the request to cleanup...
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ await observerPromise1;
+ await observerPromise2;
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ } else if (aExpect == PromptResult.ALLOW) {
+ const observerPromise = expectObserverCalled("getUserMedia:request");
+ const observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ const observerPromise2 = expectObserverCalled(
+ "recording-device-events"
+ );
+ const promise = promiseMessage("ok");
+ await promiseRequestDevice(audio, video, aIframeId, screen);
+ await promise;
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ let expected = {};
+ if (audio) {
+ expected.audio = audio;
+ }
+ if (video) {
+ expected.video = video;
+ }
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ await closeStream(false, aIframeId);
+ } else if (aExpect == PromptResult.DENY) {
+ const observerPromise = expectObserverCalled(
+ "recording-window-ended"
+ );
+ const promise = promiseMessage(permissionError);
+ await promiseRequestDevice(audio, video, aIframeId, screen);
+ await promise;
+ await observerPromise;
+ }
+
+ 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 });
+ await closeStream(true);
+
+ // Check that we get a prompt.
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(true, true, "frame4");
+ await promise;
+ await observerPromise;
+
+ // The 'Remember this decision' checkbox is hidden.
+ notification = PopupNotifications.panel.firstElementChild;
+ checkbox = notification.checkbox;
+ ok(!!checkbox, "checkbox is present");
+ ok(checkbox.hidden, "checkbox is not visible");
+
+ observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
+ observerPromise2 = expectObserverCalled("recording-window-ended");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise1;
+ await observerPromise2;
+ await checkNotSharing();
+
+ // Make sure we are not changing the scope and state of persistent
+ // permission
+ let { state, scope } = SitePermissions.getForPrincipal(
+ principal,
+ "camera",
+ browser
+ );
+ Assert.equal(state, SitePermissions.ALLOW);
+ Assert.equal(scope, SitePermissions.SCOPE_PERSISTENT);
+
+ ({ state, scope } = SitePermissions.getForPrincipal(
+ principal,
+ "microphone",
+ browser
+ ));
+ Assert.equal(state, SitePermissions.ALLOW);
+ Assert.equal(scope, SitePermissions.SCOPE_PERSISTENT);
+
+ PermissionTestUtils.remove(uri, "camera");
+ PermissionTestUtils.remove(uri, "microphone");
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ });
+
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_xorigin_frame.html",
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js
new file mode 100644
index 0000000000..cb1e69bdc4
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_xorigin_frame_chain.js
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const permissionError =
+ "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+const PromptResult = {
+ ALLOW: "allow",
+ DENY: "deny",
+ PROMPT: "prompt",
+};
+
+const Perms = Services.perms;
+
+function expectObserverCalledAncestor(aTopic, browsingContext) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic);
+ }
+
+ return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic);
+}
+
+function enableObserverVerificationAncestor(browsingContext) {
+ // Skip these checks in single process mode as it isn't worth implementing it.
+ if (!gMultiProcessBrowser) {
+ return Promise.resolve();
+ }
+
+ return BrowserTestUtils.startObservingTopics(browsingContext, observerTopics);
+}
+
+function disableObserverVerificationAncestor(browsingContextt) {
+ if (!gMultiProcessBrowser) {
+ return Promise.resolve();
+ }
+
+ return BrowserTestUtils.stopObservingTopics(
+ browsingContextt,
+ observerTopics
+ ).catch(reason => {
+ ok(false, "Failed " + reason);
+ });
+}
+
+function promiseRequestDeviceAncestor(
+ aRequestAudio,
+ aRequestVideo,
+ aType,
+ aBrowser,
+ aBadDevice = false
+) {
+ info("requesting devices");
+ return SpecialPowers.spawn(
+ aBrowser,
+ [{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
+ async function(args) {
+ let global = content.wrappedJSObject.document.getElementById("frame4")
+ .contentWindow;
+ global.requestDevice(
+ args.aRequestAudio,
+ args.aRequestVideo,
+ args.aType,
+ args.aBadDevice
+ );
+ }
+ );
+}
+
+async function closeStreamAncestor(browser) {
+ let observerPromises = [];
+ observerPromises.push(
+ expectObserverCalledAncestor("recording-device-events", browser)
+ );
+ observerPromises.push(
+ expectObserverCalledAncestor("recording-window-ended", browser)
+ );
+
+ info("closing the stream");
+ await SpecialPowers.spawn(browser, [], async () => {
+ let global = content.wrappedJSObject.document.getElementById("frame4")
+ .contentWindow;
+ global.closeStream();
+ });
+
+ await Promise.all(observerPromises);
+
+ await assertWebRTCIndicatorStatus(null);
+}
+
+var gTests = [
+ {
+ desc:
+ "getUserMedia use persistent permissions from first party if third party is explicitly trusted",
+ skipObserverVerification: true,
+ run: async function checkPermissionsAncestorChain() {
+ async function checkPermission(aPermission, aRequestType, aExpect) {
+ info(
+ `Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
+ );
+ const uri = gBrowser.selectedBrowser.documentURI;
+ // Persistent allow/deny for first party uri
+ PermissionTestUtils.add(uri, aRequestType, aPermission);
+
+ let audio = aRequestType == "microphone";
+ let video = aRequestType == "camera";
+ const screen = aRequestType == "screen" ? "screen" : undefined;
+ if (screen) {
+ audio = false;
+ video = true;
+ }
+ const iframeAncestor = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.document.getElementById("frameAncestor")
+ .browsingContext;
+ }
+ );
+
+ if (aExpect == PromptResult.PROMPT) {
+ // Check that we get a prompt.
+ const observerPromise = expectObserverCalledAncestor(
+ "getUserMedia:request",
+ iframeAncestor
+ );
+ const promise = promisePopupNotificationShown("webRTC-shareDevices");
+
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await promise;
+ await observerPromise;
+
+ // Check the label of the notification should be the first party
+ is(
+ PopupNotifications.getNotification("webRTC-shareDevices").options
+ .name,
+ uri.host,
+ "Use first party's origin"
+ );
+ const observerPromise1 = expectObserverCalledAncestor(
+ "getUserMedia:response:deny",
+ iframeAncestor
+ );
+ const observerPromise2 = expectObserverCalledAncestor(
+ "recording-window-ended",
+ iframeAncestor
+ );
+ // Deny the request to cleanup...
+ activateSecondaryAction(kActionDeny);
+ await observerPromise1;
+ await observerPromise2;
+ let browser = gBrowser.selectedBrowser;
+ SitePermissions.removeFromPrincipal(null, aRequestType, browser);
+ } else if (aExpect == PromptResult.ALLOW) {
+ const observerPromise = expectObserverCalledAncestor(
+ "getUserMedia:request",
+ iframeAncestor
+ );
+ const observerPromise1 = expectObserverCalledAncestor(
+ "getUserMedia:response:allow",
+ iframeAncestor
+ );
+ const observerPromise2 = expectObserverCalledAncestor(
+ "recording-device-events",
+ iframeAncestor
+ );
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await observerPromise;
+
+ await promiseNoPopupNotification("webRTC-shareDevices");
+ await observerPromise1;
+ await observerPromise2;
+
+ let expected = {};
+ if (audio) {
+ expected.audio = audio;
+ }
+ if (video) {
+ expected.video = video;
+ }
+
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + Object.keys(expected).join(" and ") + " to be shared"
+ );
+
+ await closeStreamAncestor(iframeAncestor);
+ } else if (aExpect == PromptResult.DENY) {
+ const observerPromise = expectObserverCalledAncestor(
+ "recording-window-ended",
+ iframeAncestor
+ );
+ await promiseRequestDeviceAncestor(
+ audio,
+ video,
+ screen,
+ iframeAncestor
+ );
+ await observerPromise;
+ }
+
+ PermissionTestUtils.remove(uri, aRequestType);
+ }
+
+ await checkPermission(Perms.PROMPT_ACTION, "camera", PromptResult.PROMPT);
+ await checkPermission(Perms.DENY_ACTION, "camera", PromptResult.DENY);
+ await checkPermission(Perms.ALLOW_ACTION, "camera", PromptResult.ALLOW);
+
+ await checkPermission(
+ Perms.PROMPT_ACTION,
+ "microphone",
+ PromptResult.PROMPT
+ );
+ await checkPermission(Perms.DENY_ACTION, "microphone", PromptResult.DENY);
+ await checkPermission(
+ Perms.ALLOW_ACTION,
+ "microphone",
+ PromptResult.ALLOW
+ );
+
+ await checkPermission(Perms.PROMPT_ACTION, "screen", PromptResult.PROMPT);
+ await checkPermission(Perms.DENY_ACTION, "screen", PromptResult.DENY);
+ // Always prompt screen sharing
+ await checkPermission(Perms.ALLOW_ACTION, "screen", PromptResult.PROMPT);
+ },
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ });
+
+ await runTests(gTests, {
+ relativeURI: "get_user_media_in_xorigin_frame_ancestor.html",
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js
new file mode 100644
index 0000000000..7976c0c3ef
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_multi_process.js
@@ -0,0 +1,518 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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(true);
+
+ 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(false, true);
+
+ 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(false, true);
+
+ 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(false, true);
+
+ 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(false, false, true);
+
+ // 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(false, false, true);
+
+ // 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..3fa395bb26
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js
@@ -0,0 +1,1008 @@
+/* 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(true, true);
+
+ 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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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(true, true);
+
+ 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.gIdentityHandler._sharingState.webRTC.camera ==
+ STATE_CAPTURE_DISABLED &&
+ window.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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(false, false, true);
+ let notification = PopupNotifications.panel.firstElementChild;
+ let iconclass = notification.getAttribute("iconclass");
+ ok(iconclass.includes("screen-icon"), "panel using screen icon");
+
+ 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.gIdentityHandler._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.gIdentityHandler._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(true, true);
+
+ 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.gIdentityHandler._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.gIdentityHandler._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(true, true);
+
+ 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.gIdentityHandler._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.gIdentityHandler._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(true, true);
+
+ 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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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(true, true);
+
+ 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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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.gIdentityHandler._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..b70722136e
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_queue_request.js
@@ -0,0 +1,281 @@
+/* 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 queueing deny audio 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 promise;
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ checkDeviceSelectors(false, true);
+ await observerPromise;
+ let indicator = promiseIndicatorWindow();
+
+ observerPromise = expectObserverCalled("getUserMedia:request");
+ 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 });
+
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(true, false);
+
+ observerPromise = expectObserverCalled("getUserMedia:response:deny");
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await observerPromise;
+ SitePermissions.removeFromPrincipal(
+ null,
+ "microphone",
+ gBrowser.selectedBrowser
+ );
+
+ // 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(true, false);
+
+ let observerPromises = [
+ expectObserverCalled("getUserMedia:request"),
+ expectObserverCalled("getUserMedia:response:deny"),
+ ];
+
+ await promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ await Promise.all(observerPromises);
+ checkDeviceSelectors(false, true);
+
+ 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(false, true);
+
+ 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(true, false);
+
+ 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 });
+
+ // 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(true, false);
+
+ 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(true, false);
+
+ 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(true, false);
+
+ 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(false, true);
+
+ 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..9fa3c04746
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
@@ -0,0 +1,922 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", this);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/aborted by the user agent/);
+ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm", this);
+
+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.";
+
+let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+);
+const isHeadless = env.get("MOZ_HEADLESS");
+
+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(false, false, true);
+ let notification = PopupNotifications.panel.firstElementChild;
+ let iconclass = notification.getAttribute("iconclass");
+ ok(iconclass.includes("screen-icon"), "panel using screen icon");
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ let noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ let separator = menulist.getItemAtIndex(1);
+ is(
+ separator.localName,
+ "menuseparator",
+ "the second item is a separator"
+ );
+
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should be hidden while there's no selection"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ let scaryScreenIndex;
+ for (let i = 2; i < count; ++i) {
+ let item = menulist.getItemAtIndex(i);
+ is(
+ parseInt(item.getAttribute("value")),
+ i - 2,
+ "the window/screen item has the correct index"
+ );
+ let type = item.getAttribute("devicetype");
+ ok(
+ ["window", "screen"].includes(type),
+ "the devicetype attribute is set correctly"
+ );
+ if (type == "screen") {
+ ok(item.scary, "the screen item is marked as scary");
+ scaryScreenIndex = i;
+ }
+ }
+ ok(
+ typeof scaryScreenIndex == "number",
+ "there's at least one scary screen, as as all screens are"
+ );
+
+ // Select a screen, a preview with a scary warning should appear.
+ menulist.getItemAtIndex(scaryScreenIndex).doCommand();
+ ok(
+ !document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be visible"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarning").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" });
+
+ // 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(false, false, true);
+
+ 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(false, false, true);
+ let notification = PopupNotifications.panel.firstElementChild;
+ let iconclass = notification.getAttribute("iconclass");
+ ok(iconclass.includes("screen-icon"), "panel using screen icon");
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ let noWindowOrScreenItem = menulist.getItemAtIndex(0);
+ ok(
+ noWindowOrScreenItem.hasAttribute("selected"),
+ "the 'Select Window or Screen' item is selected"
+ );
+ is(
+ menulist.selectedItem,
+ noWindowOrScreenItem,
+ "'Select Window or Screen' is the selected item"
+ );
+ is(menulist.value, "-1", "no window or screen is selected by default");
+ ok(
+ noWindowOrScreenItem.disabled,
+ "'Select Window or Screen' item is disabled"
+ );
+ ok(notification.button.disabled, "Allow button is disabled");
+ ok(
+ notification.hasAttribute("invalidselection"),
+ "Notification is marked as invalid"
+ );
+
+ let separator = menulist.getItemAtIndex(1);
+ is(
+ separator.localName,
+ "menuseparator",
+ "the second item is a separator"
+ );
+
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should be hidden while there's no selection"
+ );
+ ok(
+ document.getElementById("webRTC-preview").hidden,
+ "the preview area is hidden"
+ );
+
+ let scaryWindowIndexes = [],
+ nonScaryWindowIndex,
+ scaryScreenIndex;
+ for (let i = 2; i < count; ++i) {
+ let item = menulist.getItemAtIndex(i);
+ is(
+ parseInt(item.getAttribute("value")),
+ i - 2,
+ "the window/screen item has the correct index"
+ );
+ let type = item.getAttribute("devicetype");
+ ok(
+ ["window", "screen"].includes(type),
+ "the devicetype attribute is set correctly"
+ );
+ if (type == "screen") {
+ ok(item.scary, "the screen item is marked as scary");
+ scaryScreenIndex = i;
+ } else if (item.scary) {
+ scaryWindowIndexes.push(i);
+ } else {
+ nonScaryWindowIndex = i;
+ }
+ }
+ if (isHeadless) {
+ is(
+ scaryWindowIndexes.length,
+ 0,
+ "there are no scary Firefox windows in headless mode"
+ );
+ } else {
+ ok(
+ scaryWindowIndexes.length,
+ "there's at least one scary window, as Firefox is running"
+ );
+ }
+ ok(
+ typeof scaryScreenIndex == "number",
+ "there's at least one scary screen, as all screens are"
+ );
+
+ if (!isHeadless) {
+ // Select one scary window, a preview with a scary warning should appear.
+ let scaryWindowIndex;
+ for (scaryWindowIndex of scaryWindowIndexes) {
+ menulist.getItemAtIndex(scaryWindowIndex).doCommand();
+ ok(
+ document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should still be hidden"
+ );
+ try {
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "",
+ 100,
+ 100
+ );
+ break;
+ } catch (e) {
+ // A "scary window" is Firefox. Multiple Firefox windows have been
+ // observed to come and go during try runs, so we won't know which one
+ // is ours. To avoid intermittents, we ignore preview failing due to
+ // these going away on us, provided it succeeds on one of them.
+ }
+ }
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarning").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-previewWarning").hidden,
+ "the scary warning is hidden"
+ );
+ } else {
+ info("no non-scary window available on this test machine");
+ }
+
+ let indicator = promiseIndicatorWindow();
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Window" },
+ "expected screen to be shared"
+ );
+
+ await indicator;
+ if (sharingNonScaryWindow) {
+ await checkSharingUI({ screen: "Window" });
+ } else {
+ await checkSharingUI({ screen: "Window", browserwindow: true });
+ }
+
+ 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(true, false, true);
+ let iconclass = PopupNotifications.panel.firstElementChild.getAttribute(
+ "iconclass"
+ );
+ ok(iconclass.includes("screen-icon"), "panel using screen icon");
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let count = menulist.itemCount;
+ ok(
+ count >= 4,
+ "There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
+ );
+
+ // Select a screen, a preview with a scary warning should appear.
+ menulist.getItemAtIndex(count - 1).doCommand();
+ ok(
+ !document.getElementById("webRTC-all-windows-shared").hidden,
+ "the 'all windows will be shared' warning should now be visible"
+ );
+ await TestUtils.waitForCondition(
+ () => !document.getElementById("webRTC-preview").hidden,
+ "preview unhide",
+ 100,
+ 100
+ );
+ ok(
+ !document.getElementById("webRTC-preview").hidden,
+ "the preview area is visible"
+ );
+ ok(
+ !document.getElementById("webRTC-previewWarning").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" });
+ 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(false, false, true);
+
+ 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(audio, video, screen) {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ await promiseRequestDevice(
+ audio,
+ video || !!screen,
+ null,
+ screen && "window"
+ );
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(audio, video, screen);
+ 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 = {}) {
+ let shared = Object.keys(expected).join(" and ");
+ if (shared) {
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ expected,
+ "expected " + shared + " to be shared"
+ );
+ await checkSharingUI(expected);
+ } else {
+ await checkNotSharing();
+ }
+ }
+
+ info("Share screen and microphone");
+ let indicator = promiseIndicatorWindow();
+ await share(true, false, true);
+ await indicator;
+ await check({ audio: true, screen: "Screen" });
+
+ info("Share camera");
+ await share(false, true);
+ await check({ video: true, audio: true, screen: "Screen" });
+
+ info("Stop the screen share, mic+cam should continue");
+ await stopSharing("screen", true);
+ await check({ video: true, audio: true });
+
+ info("Stop the camera, everything should stop.");
+ await stopSharing("camera");
+
+ info("Now, share only the screen...");
+ indicator = promiseIndicatorWindow();
+ await share(false, false, true);
+ await indicator;
+ await check({ screen: "Screen" });
+
+ info("... and add camera and microphone in a second request.");
+ await share(true, true);
+ await check({ video: true, audio: true, screen: "Screen" });
+
+ 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(false, false, true);
+ 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" });
+
+ await reloadAndAssertClosedStreams();
+ },
+ },
+
+ {
+ desc: "test showControlCenter from screen icon",
+ run: async function checkShowControlCenter() {
+ if (!USING_LEGACY_INDICATOR) {
+ info(
+ "Skipping since this test doesn't apply to the new global sharing " +
+ "indicator."
+ );
+ return;
+ }
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(false, false, true);
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ menulist.getItemAtIndex(menulist.itemCount - 1).doCommand();
+
+ let observerPromise1 = expectObserverCalled(
+ "getUserMedia:response:allow"
+ );
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ let indicator = promiseIndicatorWindow();
+ await promiseMessage("ok", () => {
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ await observerPromise1;
+ await observerPromise2;
+ Assert.deepEqual(
+ await getMediaCaptureState(),
+ { screen: "Screen" },
+ "expected screen to be shared"
+ );
+ await indicator;
+ await checkSharingUI({ screen: "Screen" });
+
+ ok(identityPopupHidden(), "control center should be hidden");
+ if (IS_MAC) {
+ let activeStreams = webrtcUI.getActiveStreams(false, false, true);
+ webrtcUI.showSharingDoorhanger(activeStreams[0]);
+ } else {
+ let win = Services.wm.getMostRecentWindow(
+ "Browser:WebRTCGlobalIndicator"
+ );
+ let elt = win.document.getElementById("screenShareButton");
+ EventUtils.synthesizeMouseAtCenter(elt, {}, win);
+ }
+ await TestUtils.waitForCondition(
+ () => !identityPopupHidden(),
+ "wait for control center to open"
+ );
+ ok(!identityPopupHidden(), "control center should be open");
+
+ gIdentityHandler._identityPopup.hidePopup();
+
+ await closeStream();
+ },
+ },
+
+ {
+ desc: "Only persistent block is possible for screen sharing",
+ run: async function checkPersistentPermissions() {
+ // This test doesn't apply when the notification silencing
+ // feature is enabled, since the "Remember this decision"
+ // checkbox doesn't exist.
+ if (ALLOW_SILENCING_NOTIFICATIONS) {
+ return;
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ let devicePerms = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "screen",
+ browser
+ );
+ is(
+ devicePerms.state,
+ SitePermissions.UNKNOWN,
+ "starting without screen persistent permissions"
+ );
+
+ let observerPromise = expectObserverCalled("getUserMedia:request");
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+ checkDeviceSelectors(false, false, true);
+ 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.
+ observerPromise = expectObserverCalled("recording-window-ended");
+ promise = promiseMessage(permissionError);
+ await promiseRequestDevice(false, true, null, "screen");
+ await promise;
+ await observerPromise;
+
+ // 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..57d303bf5f
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen_tab_close.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Tests that the given tab is the currently selected tab.
+ * @param {Element} aTab - Tab to test.
+ */
+function testSelected(aTab) {
+ is(aTab, gBrowser.selectedTab, "Tab is gBrowser.selectedTab");
+ is(aTab.getAttribute("selected"), "true", "Tab has property 'selected'");
+ is(
+ aTab.getAttribute("visuallyselected"),
+ "true",
+ "Tab has property 'visuallyselected'"
+ );
+}
+
+/**
+ * Tests that when closing a tab with active screen sharing, the screen sharing
+ * ends and the tab closes properly.
+ */
+add_task(async function testScreenSharingTabClose() {
+ let initialTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://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, byMouse: 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..fa5dcf6d90
--- /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(true, true);
+
+ 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(identityPopupHidden(win), "control center should be hidden");
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0], "Devices");
+ // 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.gIdentityHandler._identityPopup;
+ 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.gIdentityHandler._identityPopup.hidePopup();
+
+ ok(
+ identityPopupHidden(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..721680e2d3
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_unprompted_access.js
@@ -0,0 +1,413 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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(false, false, true);
+
+ 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
+ );
+
+ // 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(true, true);
+
+ 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 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(true, false);
+
+ 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(true, true);
+
+ 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(false, false, true);
+
+ 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(false, true);
+
+ 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(true, true);
+
+ 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..7641df0706
--- /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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+
+ 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(true, true);
+ 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(true, true);
+
+ 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..d792af0dbf
--- /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(false, true);
+ 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..7950344a6e
--- /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(true, true);
+
+ 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_global_mute_toggles.js b/browser/base/content/test/webrtc/browser_global_mute_toggles.js
new file mode 100644
index 0000000000..ce7c319044
--- /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_task(async function setup() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ["privacy.webrtc.globalMuteToggles", true],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Returns a Promise that resolves when the content process for
+ * <browser> fires the right observer notification based on the
+ * value of isMuted for the camera.
+ *
+ * Note: Callers must ensure that they first call
+ * BrowserTestUtils.startObservingTopics to monitor the mute and
+ * unmute observer notifications for this to work properly.
+ *
+ * @param {<xul:browser>} browser - The browser running in the content process
+ * to be monitored.
+ * @param {Boolean} isMuted - True if the muted topic should be fired.
+ * @return {Promise}
+ * @resolves {undefined} When the notification fires.
+ */
+function waitForCameraMuteState(browser, isMuted) {
+ let topic = isMuted ? "getUserMedia:muteVideo" : "getUserMedia:unmuteVideo";
+ return BrowserTestUtils.contentTopicObserved(browser.browsingContext, topic);
+}
+
+/**
+ * Returns a Promise that resolves when the content process for
+ * <browser> fires the right observer notification based on the
+ * value of isMuted for the microphone.
+ *
+ * Note: Callers must ensure that they first call
+ * BrowserTestUtils.startObservingTopics to monitor the mute and
+ * unmute observer notifications for this to work properly.
+ *
+ * @param {<xul:browser>} browser - The browser running in the content process
+ * to be monitored.
+ * @param {Boolean} isMuted - True if the muted topic should be fired.
+ * @return {Promise}
+ * @resolves {undefined} When the notification fires.
+ */
+function waitForMicrophoneMuteState(browser, isMuted) {
+ let topic = isMuted ? "getUserMedia:muteAudio" : "getUserMedia:unmuteAudio";
+ return BrowserTestUtils.contentTopicObserved(browser.browsingContext, topic);
+}
+
+/**
+ * Tests that the global mute toggles fire the right observer
+ * notifications in pre-existing content processes.
+ */
+add_task(async function test_notifications() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ Assert.ok(
+ microphoneMute.checked,
+ "Microphone toggle should now be checked."
+ );
+ Assert.ok(cameraMute.checked, "Camera toggle should now be checked.");
+
+ info("Unmuting microphone...");
+ let microphoneUnmuted = waitForMicrophoneMuteState(browser, false);
+ microphoneMute.click();
+ await microphoneUnmuted;
+ info("Microphone successfully unmuted.");
+
+ info("Unmuting camera...");
+ let cameraUnmuted = waitForCameraMuteState(browser, false);
+ cameraMute.click();
+ await cameraUnmuted;
+ info("Camera successfully unmuted.");
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+ });
+});
+
+/**
+ * Tests that if sharing stops while muted, and the indicator closes,
+ * then the mute state is reset.
+ */
+add_task(async function test_closing_indicator_resets_mute() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ Assert.ok(
+ microphoneMute.checked,
+ "Microphone toggle should now be checked."
+ );
+ Assert.ok(cameraMute.checked, "Camera toggle should now be checked.");
+
+ let allUnmuted = Promise.all([
+ waitForMicrophoneMuteState(browser, false),
+ waitForCameraMuteState(browser, false),
+ ]);
+
+ await closeStream();
+ await allUnmuted;
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser.browsingContext,
+ MUTE_TOPICS
+ );
+ });
+});
+
+/**
+ * Test that if the global mute state is set, then newly created
+ * content processes also have their tracks muted after sending
+ * a getUserMedia request.
+ */
+add_task(async function test_new_processes() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_PAGE,
+ });
+ let browser1 = tab1.linkedBrowser;
+
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(browser1, true /* camera */, true /* microphone */);
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ let microphoneMute = doc.getElementById("microphone-mute-toggle");
+ let cameraMute = doc.getElementById("camera-mute-toggle");
+
+ Assert.ok(
+ !microphoneMute.checked,
+ "Microphone toggle should not start checked."
+ );
+ Assert.ok(!cameraMute.checked, "Camera toggle should not start checked.");
+
+ await BrowserTestUtils.startObservingTopics(
+ browser1.browsingContext,
+ MUTE_TOPICS
+ );
+
+ info("Muting microphone...");
+ let microphoneMuted = waitForMicrophoneMuteState(browser1, true);
+ microphoneMute.click();
+ await microphoneMuted;
+ info("Microphone successfully muted.");
+
+ info("Muting camera...");
+ let cameraMuted = waitForCameraMuteState(browser1, true);
+ cameraMute.click();
+ await cameraMuted;
+ info("Camera successfully muted.");
+
+ // We'll make sure a new process is being launched by observing
+ // for the ipc:content-created notification.
+ let processLaunched = TestUtils.topicObserved("ipc:content-created");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_PAGE,
+ forceNewProcess: true,
+ });
+ let browser2 = tab2.linkedBrowser;
+
+ await processLaunched;
+
+ await BrowserTestUtils.startObservingTopics(
+ browser2.browsingContext,
+ MUTE_TOPICS
+ );
+
+ let microphoneMuted2 = waitForMicrophoneMuteState(browser2, true);
+ let cameraMuted2 = waitForCameraMuteState(browser2, true);
+ info("Sharing the microphone and camera from a new process.");
+ await shareDevices(browser2, true /* camera */, true /* microphone */);
+ await Promise.all([microphoneMuted2, cameraMuted2]);
+
+ info("Unmuting microphone...");
+ let microphoneUnmuted = Promise.all([
+ waitForMicrophoneMuteState(browser1, false),
+ waitForMicrophoneMuteState(browser2, false),
+ ]);
+ microphoneMute.click();
+ await microphoneUnmuted;
+ info("Microphone successfully unmuted.");
+
+ info("Unmuting camera...");
+ let cameraUnmuted = Promise.all([
+ waitForCameraMuteState(browser1, false),
+ waitForCameraMuteState(browser2, false),
+ ]);
+ cameraMute.click();
+ await cameraUnmuted;
+ info("Camera successfully unmuted.");
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser1.browsingContext,
+ MUTE_TOPICS
+ );
+
+ await BrowserTestUtils.stopObservingTopics(
+ browser2.browsingContext,
+ MUTE_TOPICS
+ );
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/base/content/test/webrtc/browser_indicator_popuphiding.js b/browser/base/content/test/webrtc/browser_indicator_popuphiding.js
new file mode 100644
index 0000000000..8d02eb5c70
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_indicator_popuphiding.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_PAGE = TEST_ROOT + "get_user_media.html";
+
+/**
+ * Regression test for bug 1668838 - make sure that a popuphiding
+ * event that fires for any popup not related to the device control
+ * menus is ignored and doesn't cause the targets contents to be all
+ * removed.
+ */
+add_task(async function test_popuphiding() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN
+ );
+
+ let indicator = await indicatorPromise;
+ let doc = indicator.document;
+
+ Assert.ok(doc.body, "Should have a document body in the indicator.");
+
+ let event = new indicator.MouseEvent("popuphiding", { bubbles: true });
+ doc.documentElement.dispatchEvent(event);
+
+ Assert.ok(doc.body, "Should still have a document body in the indicator.");
+ });
+
+ await checkNotSharing();
+});
diff --git a/browser/base/content/test/webrtc/browser_notification_silencing.js b/browser/base/content/test/webrtc/browser_notification_silencing.js
new file mode 100644
index 0000000000..bc3e18904b
--- /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(false, false, true, 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"),
+ "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_task(async function setup() {
+ // 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..2764df61f6
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_stop_sharing_button.js
@@ -0,0 +1,172 @@
+/* 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_task(async function setup() {
+ let prefs = [
+ [PREF_PERMISSION_FAKE, true],
+ [PREF_AUDIO_LOOPBACK, ""],
+ [PREF_VIDEO_LOOPBACK, ""],
+ [PREF_FAKE_STREAMS, true],
+ [PREF_FOCUS_SOURCE, false],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display while
+ * also sharing their microphone or camera, that only the display
+ * stream is stopped.
+ */
+add_task(async function test_stop_sharing() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN
+ );
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = expectObserverCalled("recording-device-events");
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ // Ensure that we're still sharing the other streams.
+ await checkSharingUI({ audio: true, video: true });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+ });
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display while
+ * sharing their display on multiple sites, all of those display sharing
+ * streams are closed.
+ */
+add_task(async function test_stop_sharing_multiple() {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ info("Opening first tab");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera, microphone and screen");
+ await shareDevices(tab1.linkedBrowser, true, true, SHARE_SCREEN);
+
+ info("Opening second tab");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ info("Sharing camera and screen");
+ await shareDevices(tab2.linkedBrowser, true, false, SHARE_SCREEN);
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = TestUtils.waitForCondition(() => {
+ return !webrtcUI.showScreenSharingIndicator;
+ });
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ Assert.equal(gBrowser.selectedTab, tab2, "Should have tab2 selected.");
+ await checkSharingUI({ audio: false, video: true }, window, {
+ audio: true,
+ video: true,
+ });
+ BrowserTestUtils.removeTab(tab2);
+
+ Assert.equal(gBrowser.selectedTab, tab1, "Should have tab1 selected.");
+ await checkSharingUI({ audio: true, video: true }, window, {
+ audio: true,
+ video: true,
+ });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/**
+ * Tests that if the user chooses to "Stop Sharing" a display, persistent
+ * permissions are not removed for camera or microphone devices.
+ */
+add_task(async function test_keep_permissions() {
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => {
+ let indicatorPromise = promiseIndicatorWindow();
+
+ await shareDevices(
+ browser,
+ true /* camera */,
+ true /* microphone */,
+ SHARE_SCREEN,
+ true /* remember */
+ );
+
+ let indicator = await indicatorPromise;
+
+ let stopSharingButton = indicator.document.getElementById("stop-sharing");
+ let stopSharingPromise = expectObserverCalled("recording-device-events");
+ stopSharingButton.click();
+ await stopSharingPromise;
+
+ // Ensure that we're still sharing the other streams.
+ await checkSharingUI({ audio: true, video: true });
+
+ // Ensure that the "display-share" section of the indicator is now hidden
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ indicator.document.getElementById("display-share")
+ ),
+ "The display-share section of the indicator should now be hidden."
+ );
+
+ let { state: micState, scope: micScope } = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+
+ Assert.equal(micState, SitePermissions.ALLOW);
+ Assert.equal(micScope, SitePermissions.SCOPE_PERSISTENT);
+
+ let { state: camState, scope: camScope } = SitePermissions.getForPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ Assert.equal(camState, SitePermissions.ALLOW);
+ Assert.equal(camScope, SitePermissions.SCOPE_PERSISTENT);
+
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "camera",
+ browser
+ );
+ SitePermissions.removeFromPrincipal(
+ browser.contentPrincipal,
+ "microphone",
+ browser
+ );
+ });
+});
diff --git a/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js b/browser/base/content/test/webrtc/browser_stop_streams_on_indicator_close.js
new file mode 100644
index 0000000000..a34df44047
--- /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_task(async function setup() {
+ 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..b677fdeec1
--- /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.recentlyUsedOrder";
+
+/**
+ * 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_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.webrtc.sharedTabWarning", true]],
+ });
+
+ // Loads up NEW_BACKGROUND_TABS_TO_OPEN background tabs at about:blank,
+ // and waits until they're fully open.
+ let uris = new Array(NEW_BACKGROUND_TABS_TO_OPEN).fill("about:blank");
+
+ let loadPromises = Promise.all(
+ uris.map(uri => BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true))
+ );
+
+ gBrowser.loadTabs(uris, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await loadPromises;
+
+ // Switches to the first tab and closes all of the rest.
+ registerCleanupFunction(async () => {
+ await resetDisplaySharingState();
+ gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+ });
+});
+
+/**
+ * Tests that when sharing the window that the first tab switch does _not_ show
+ * the warning. This is because we presume that the first tab switch since
+ * starting display sharing is for a tab that is intentionally being shared.
+ */
+add_task(async function testFirstTabSwitchAllowed() {
+ pretendToShareWindow(window, false);
+
+ let targetTab = gBrowser.tabs[1];
+
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await noWarningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that the second tab switch after sharing is not allowed
+ * without a warning. Also tests that the warning can "allow"
+ * the tab switch to proceed, and that no warning is subsequently
+ * shown for the "allowed" tab. Finally, ensures that if the sharing
+ * session ends and a new session begins, that warnings are shown
+ * again for the allowed tabs.
+ */
+add_task(async function testWarningOnSecondTabSwitch() {
+ pretendToShareWindow(window);
+ let originalTab = gBrowser.selectedTab;
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on the second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ // Not only should we have warned, but we should have prevented
+ // the tab switch from occurring.
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should still be on the original tab."
+ );
+
+ // Now test the "Allow" button in the warning to make sure the tab
+ // switch goes through.
+ let tabSwitchPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "TabSwitchDone"
+ );
+ let allowButton = document.getElementById(ALLOW_BUTTON_ID);
+ allowButton.click();
+ await tabSwitchPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs to the target."
+ );
+
+ // We shouldn't see a warning when switching back to that first
+ // "freebie" tab.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+ await noWarningPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should have switched tabs back to the original tab."
+ );
+
+ // We shouldn't see a warning when switching back to the tab that
+ // we had just allowed.
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await noWarningPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs back to the target tab."
+ );
+
+ // Reset the sharing state, and make sure that warnings can
+ // be displayed again.
+ await resetDisplaySharingState();
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ //
+ // Make sure we get the warning again when switching to the
+ // target tab.
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that warnings can be skipped for a session via the
+ * checkbox in the warning panel. Also checks that once the
+ * session ends and a new one begins that warnings are displayed
+ * again.
+ */
+add_task(async function testDisableWarningForSession() {
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on the second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ // Check the checkbox to suppress warnings for the rest of this session.
+ let checkbox = document.getElementById(
+ DISABLE_WARNING_FOR_SESSION_CHECKBOX_ID
+ );
+ checkbox.checked = true;
+
+ // Now test the "Allow" button in the warning to make sure the tab
+ // switch goes through.
+ let tabSwitchPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "TabSwitchDone"
+ );
+ let allowButton = document.getElementById(ALLOW_BUTTON_ID);
+ allowButton.click();
+ await tabSwitchPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ targetTab,
+ "Should have switched tabs to the target tab."
+ );
+
+ // Switching to the 4th and 5th tabs should now not show warnings.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
+ await noWarningPromise;
+
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
+ await noWarningPromise;
+
+ // Reset the sharing state, and make sure that warnings can
+ // be displayed again.
+ await resetDisplaySharingState();
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch
+ //
+ // Make sure we get the warning again when switching to the
+ // target tab.
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that we don't show a warning when sharing a different
+ * window than the one we're switching tabs in.
+ */
+add_task(async function testOtherWindow() {
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+ await SimpleTest.promiseFocus(window);
+ pretendToShareWindow(otherWin);
+
+ // Switching to the 4th and 5th tabs should now not show warnings.
+ let noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[3]);
+ await noWarningPromise;
+
+ noWarningPromise = ensureNoWarning();
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[4]);
+ await noWarningPromise;
+
+ await BrowserTestUtils.closeWindow(otherWin);
+
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that we show a different label when sharing the screen
+ * vs when sharing a window.
+ */
+add_task(async function testWindowVsScreenLabel() {
+ pretendToShareWindow(window);
+
+ // pretendToShareWindow will have switched us to the second
+ // tab automatically as the first "freebie" tab switch.
+ // Let's now switch to the third tab.
+ let targetTab = gBrowser.tabs[2];
+
+ // Ensure that we show the warning on this second tab switch
+ let warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ let windowHeader = document.getElementById(WINDOW_SHARING_HEADER_ID);
+ let screenHeader = document.getElementById(SCREEN_SHARING_HEADER_ID);
+ Assert.ok(
+ !BrowserTestUtils.is_hidden(windowHeader),
+ "Should be showing window sharing header"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(screenHeader),
+ "Should not be showing screen sharing header"
+ );
+
+ // Reset the sharing state, and then pretend to share the screen.
+ await resetDisplaySharingState();
+ pretendToShareScreen();
+
+ // Ensure that we show the warning on this second tab switch
+ warningPromise = ensureWarning(targetTab);
+ await BrowserTestUtils.switchTab(gBrowser, targetTab);
+ await warningPromise;
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(windowHeader),
+ "Should not be showing window sharing header"
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_hidden(screenHeader),
+ "Should be showing screen sharing header"
+ );
+ await resetDisplaySharingState();
+});
+
+/**
+ * Tests that tab switching via the keyboard can also trigger the
+ * tab switch warnings.
+ */
+add_task(async function testKeyboardTabSwitching() {
+ let pressCtrlTab = async (expectPanel = false) => {
+ let promise;
+ if (expectPanel) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_TAB", {
+ ctrlKey: true,
+ shiftKey: false,
+ });
+ await promise;
+ };
+
+ let releaseCtrl = async () => {
+ let promise;
+ if (ctrlTab.isOpen) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popuphidden");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+ return promise;
+ };
+
+ // Ensure that the (on by default) ctrl-tab switch panel is enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [[CTRL_TAB_RUO_PREF, true]],
+ });
+
+ pretendToShareWindow(window);
+ let originalTab = gBrowser.selectedTab;
+ await pressCtrlTab(true);
+
+ // The Ctrl-Tab MRU list should be:
+ // 0: Second tab (currently selected)
+ // 1: First tab
+ // 2: Last tab
+ //
+ // Having pressed Ctrl-Tab once, 1 (First tab) is selected in the
+ // panel. We want a tab that will warn, so let's hit Ctrl-Tab again
+ // to choose 2 (Last tab).
+ let targetTab = ctrlTab.tabList[2];
+ await pressCtrlTab();
+
+ let warningPromise = ensureWarning(targetTab);
+ await releaseCtrl();
+ await warningPromise;
+
+ // Hide the warning without allowing the tab switch.
+ let panel = document.getElementById(WARNING_PANEL_ID);
+ panel.hidePopup();
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ originalTab,
+ "Should not have changed from the original tab."
+ );
+
+ // Now switch to the in-order tab switching keyboard shortcut mode.
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [[CTRL_TAB_RUO_PREF, false]],
+ });
+
+ // Hitting Ctrl-Tab should choose the _next_ tab over from
+ // the originalTab, which should be the third tab.
+ targetTab = gBrowser.tabs[2];
+
+ warningPromise = ensureWarning(targetTab);
+ await pressCtrlTab();
+ await warningPromise;
+
+ await resetDisplaySharingState();
+});
diff --git a/browser/base/content/test/webrtc/browser_webrtc_hooks.js b/browser/base/content/test/webrtc/browser_webrtc_hooks.js
new file mode 100644
index 0000000000..0fe02d40da
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_webrtc_hooks.js
@@ -0,0 +1,373 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { webrtcUI } = ChromeUtils.import("resource:///modules/webrtcUI.jsm");
+
+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..20b9e67467
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML = m;
+ 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 closeStream() {
+ for (let stream of gStreams) {
+ if (stream) {
+ stream.getTracks().forEach(t => t.stop());
+ stream = null;
+ }
+ }
+ 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..c884a7d51d
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_frame.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML = m;
+ window.top.postMessage(m, "*");
+}
+
+var gStreams = [];
+
+function requestDevice(aAudio, aVideo, aShare) {
+ var opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = {
+ mozMediaSource: aShare,
+ 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 closeStream() {
+ for (let stream of gStreams) {
+ if (stream) {
+ stream.getTracks().forEach(t => t.stop());
+ stream = null;
+ }
+ }
+ 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..1f8407c389
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("message").innerHTML = m;
+ window.parent.postMessage(m, "*");
+}
+
+var gStreams = [];
+
+function requestDevice(aAudio, aVideo, aShare) {
+ var opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = {
+ mozMediaSource: aShare,
+ 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 closeStream() {
+ for (let stream of gStreams) {
+ if (stream) {
+ stream.getTracks().forEach(t => t.stop());
+ stream = null;
+ }
+ }
+ 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/head.js b/browser/base/content/test/webrtc/head.js
new file mode 100644
index 0000000000..b1da09863d
--- /dev/null
+++ b/browser/base/content/test/webrtc/head.js
@@ -0,0 +1,1131 @@
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+var { SitePermissions } = ChromeUtils.import(
+ "resource:///modules/SitePermissions.jsm"
+);
+var { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
+const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev";
+const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev";
+const PREF_FAKE_STREAMS = "media.navigator.streams.fake";
+const PREF_FOCUS_SOURCE = "media.getusermedia.window.focus_source.enabled";
+
+const STATE_CAPTURE_ENABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
+const STATE_CAPTURE_DISABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
+
+const USING_LEGACY_INDICATOR = Services.prefs.getBoolPref(
+ "privacy.webrtc.legacyGlobalIndicator",
+ false
+);
+
+const ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref(
+ "privacy.webrtc.allowSilencingNotifications",
+ false
+);
+
+const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref(
+ "privacy.webrtc.globalMuteToggles",
+ false
+);
+
+const INDICATOR_PATH = USING_LEGACY_INDICATOR
+ ? "chrome://browser/content/webrtcLegacyIndicator.xhtml"
+ : "chrome://browser/content/webrtcIndicator.xhtml";
+
+const IS_MAC = AppConstants.platform == "macosx";
+
+const SHARE_SCREEN = 1;
+const SHARE_WINDOW = 2;
+
+let observerTopics = [
+ "getUserMedia:response:allow",
+ "getUserMedia:revoke",
+ "getUserMedia:response:deny",
+ "getUserMedia:request",
+ "recording-device-events",
+ "recording-window-ended",
+];
+
+// Structured hierarchy of subframes. Keys are frame id:s, The children member
+// contains nested sub frames if any. The noTest member make a frame be ignored
+// for testing if true.
+let gObserveSubFrames = {};
+// Object of subframes to test. Each element contains the members bc and id, for
+// the frames BrowsingContext and id, respectively.
+let gSubFramesToTest = [];
+let gBrowserContextsToObserve = [];
+
+function whenDelayedStartupFinished(aWindow) {
+ return TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == aWindow
+ );
+}
+
+function promiseIndicatorWindow() {
+ // We don't show the legacy indicator window on Mac.
+ if (USING_LEGACY_INDICATOR && IS_MAC) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(win) {
+ win.addEventListener(
+ "load",
+ function() {
+ if (win.location.href !== INDICATOR_PATH) {
+ info("ignoring a window with this url: " + win.location.href);
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "domwindowopened");
+ executeSoon(() => resolve(win));
+ },
+ { once: true }
+ );
+ }, "domwindowopened");
+ });
+}
+
+async function assertWebRTCIndicatorStatus(expected) {
+ let ui = ChromeUtils.import("resource:///modules/webrtcUI.jsm", {}).webrtcUI;
+ let expectedState = expected ? "visible" : "hidden";
+ let msg = "WebRTC indicator " + expectedState;
+ if (!expected && ui.showGlobalIndicator) {
+ // It seems the global indicator is not always removed synchronously
+ // in some cases.
+ await TestUtils.waitForCondition(
+ () => !ui.showGlobalIndicator,
+ "waiting for the global indicator to be hidden"
+ );
+ }
+ is(ui.showGlobalIndicator, !!expected, msg);
+
+ let expectVideo = false,
+ expectAudio = false,
+ expectScreen = "";
+ if (expected) {
+ if (expected.video) {
+ expectVideo = true;
+ }
+ if (expected.audio) {
+ expectAudio = true;
+ }
+ if (expected.screen) {
+ expectScreen = expected.screen;
+ }
+ }
+ is(
+ Boolean(ui.showCameraIndicator),
+ expectVideo,
+ "camera global indicator as expected"
+ );
+ is(
+ Boolean(ui.showMicrophoneIndicator),
+ expectAudio,
+ "microphone global indicator as expected"
+ );
+ is(
+ ui.showScreenSharingIndicator,
+ expectScreen,
+ "screen global indicator as expected"
+ );
+
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ let menu = win.document.getElementById("tabSharingMenu");
+ is(
+ !!menu && !menu.hidden,
+ !!expected,
+ "WebRTC menu should be " + expectedState
+ );
+ }
+
+ if (USING_LEGACY_INDICATOR && IS_MAC) {
+ return;
+ }
+
+ if (!expected) {
+ let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
+ if (win) {
+ await new Promise((resolve, reject) => {
+ win.addEventListener("unload", function listener(e) {
+ if (e.target == win.document) {
+ win.removeEventListener("unload", listener);
+ executeSoon(resolve);
+ }
+ });
+ });
+ }
+ }
+
+ let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator");
+ let hasWindow = indicator.hasMoreElements();
+ is(hasWindow, !!expected, "popup " + msg);
+ if (hasWindow) {
+ let document = indicator.getNext().document;
+ let docElt = document.documentElement;
+
+ if (document.readyState != "complete") {
+ info("Waiting for the sharing indicator's document to load");
+ await new Promise(resolve => {
+ document.addEventListener(
+ "readystatechange",
+ function onReadyStateChange() {
+ if (document.readyState != "complete") {
+ return;
+ }
+ document.removeEventListener(
+ "readystatechange",
+ onReadyStateChange
+ );
+ executeSoon(resolve);
+ }
+ );
+ });
+ }
+
+ if (
+ !USING_LEGACY_INDICATOR &&
+ expected.screen &&
+ expected.screen.startsWith("Window")
+ ) {
+ // These tests were originally written to express window sharing by
+ // having expected.screen start with "Window". This meant that the
+ // legacy indicator is expected to have the "sharingscreen" attribute
+ // set to true when sharing a window.
+ //
+ // The new indicator, however, differentiates between screen, window
+ // and browser window sharing. If we're using the new indicator, we
+ // update the expectations accordingly. This can be removed once we
+ // are able to remove the tests for the legacy indicator.
+ expected.screen = null;
+ expected.window = true;
+ }
+
+ if (!USING_LEGACY_INDICATOR && !SHOW_GLOBAL_MUTE_TOGGLES) {
+ expected.video = false;
+ expected.audio = false;
+
+ let visible = docElt.getAttribute("visible") == "true";
+
+ if (!expected.screen && !expected.window && !expected.browserwindow) {
+ ok(!visible, "Indicator should not be visible in this configuation.");
+ } else {
+ ok(visible, "Indicator should be visible.");
+ }
+ }
+
+ for (let item of ["video", "audio", "screen", "window", "browserwindow"]) {
+ let expectedValue;
+
+ if (USING_LEGACY_INDICATOR) {
+ expectedValue = expected && expected[item] ? "true" : "";
+ } else {
+ expectedValue = expected && expected[item] ? "true" : null;
+ }
+
+ is(
+ docElt.getAttribute("sharing" + item),
+ expectedValue,
+ item + " global indicator attribute as expected"
+ );
+ }
+
+ ok(!indicator.hasMoreElements(), "only one global indicator window");
+ }
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = BrowserTestUtils.waitForPopupEvent(
+ win.PopupNotifications.panel,
+ "shown"
+ );
+ notification.reshow();
+ return panelPromise;
+}
+
+function ignoreEvent(aSubject, aTopic, aData) {
+ // With e10s disabled, our content script receives notifications for the
+ // preview displayed in our screen sharing permission prompt; ignore them.
+ const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
+ const nsIPropertyBag = Ci.nsIPropertyBag;
+ if (
+ aTopic == "recording-device-events" &&
+ aSubject.QueryInterface(nsIPropertyBag).getProperty("requestURL") ==
+ kBrowserURL
+ ) {
+ return true;
+ }
+ if (aTopic == "recording-window-ended") {
+ let win = Services.wm.getOuterWindowWithId(aData).top;
+ if (win.document.documentURI == kBrowserURL) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function expectObserverCalledInProcess(aTopic, aCount = 1) {
+ let promises = [];
+ for (let count = aCount; count > 0; count--) {
+ promises.push(TestUtils.topicObserved(aTopic, ignoreEvent));
+ }
+ return promises;
+}
+
+function expectObserverCalled(
+ aTopic,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic, aCount);
+ }
+
+ let browsingContext = Element.isInstance(browser)
+ ? browser.browsingContext
+ : browser;
+
+ return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic, aCount);
+}
+
+// This is a special version of expectObserverCalled that should only
+// be used when expecting a notification upon closing a window. It uses
+// the per-process message manager instead of actors to send the
+// notifications.
+function expectObserverCalledOnClose(
+ aTopic,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ if (!gMultiProcessBrowser) {
+ return expectObserverCalledInProcess(aTopic, aCount);
+ }
+
+ let browsingContext = Element.isInstance(browser)
+ ? browser.browsingContext
+ : browser;
+
+ return new Promise(resolve => {
+ BrowserTestUtils.sendAsyncMessage(
+ browsingContext,
+ "BrowserTestUtils:ObserveTopic",
+ {
+ topic: aTopic,
+ count: 1,
+ filterFunctionSource: ((subject, topic, data) => {
+ Services.cpmm.sendAsyncMessage("WebRTCTest:ObserverCalled", {
+ topic,
+ });
+ return true;
+ }).toSource(),
+ }
+ );
+
+ function observerCalled(message) {
+ if (message.data.topic == aTopic) {
+ Services.ppmm.removeMessageListener(
+ "WebRTCTest:ObserverCalled",
+ observerCalled
+ );
+ resolve();
+ }
+ }
+ Services.ppmm.addMessageListener(
+ "WebRTCTest:ObserverCalled",
+ observerCalled
+ );
+ });
+}
+
+function promiseMessage(
+ aMessage,
+ aAction,
+ aCount = 1,
+ browser = gBrowser.selectedBrowser
+) {
+ let 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;
+}
+
+function promisePopupNotificationShown(aName, aAction, aWindow = window) {
+ return new Promise(resolve => {
+ // In case the global webrtc indicator has stolen focus (bug 1421724)
+ aWindow.focus();
+
+ 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(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;
+
+function activateSecondaryAction(aAction) {
+ let notification = PopupNotifications.panel.firstElementChild;
+ switch (aAction) {
+ case kActionNever:
+ 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() {
+ function gatherBrowsingContexts(aBrowsingContext) {
+ let list = [aBrowsingContext];
+
+ let children = aBrowsingContext.children;
+ for (let child of children) {
+ list.push(...gatherBrowsingContexts(child));
+ }
+
+ return list;
+ }
+
+ function combine(x, y) {
+ if (
+ x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
+ y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED
+ ) {
+ return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
+ }
+ if (
+ x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED ||
+ y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
+ ) {
+ return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
+ }
+ return Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ }
+
+ let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+ let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
+
+ for (let bc of gatherBrowsingContexts(
+ gBrowser.selectedBrowser.browsingContext
+ )) {
+ let state = await SpecialPowers.spawn(bc, [], async function() {
+ let mediaManagerService = Cc[
+ "@mozilla.org/mediaManagerService;1"
+ ].getService(Ci.nsIMediaManagerService);
+
+ let hasCamera = {};
+ let hasMicrophone = {};
+ let hasScreenShare = {};
+ let hasWindowShare = {};
+ let hasBrowserShare = {};
+ let devices = {};
+ mediaManagerService.mediaCaptureWindowState(
+ content,
+ hasCamera,
+ hasMicrophone,
+ hasScreenShare,
+ hasWindowShare,
+ hasBrowserShare,
+ devices,
+ false
+ );
+
+ return {
+ video: hasCamera.value,
+ audio: hasMicrophone.value,
+ screen: hasScreenShare.value,
+ window: hasWindowShare.value,
+ browser: hasBrowserShare.value,
+ };
+ });
+
+ video = combine(state.video, video);
+ audio = combine(state.audio, audio);
+ screen = combine(state.screen, screen);
+ window = combine(state.window, window);
+ browser = combine(state.browser, browser);
+ }
+
+ let result = {};
+
+ if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.video = true;
+ }
+ if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.audio = true;
+ }
+
+ if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Screen";
+ } else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Window";
+ } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
+ result.screen = "Browser";
+ }
+
+ return result;
+}
+
+async function stopSharing(
+ aType = "camera",
+ aShouldKeepSharing = false,
+ aFrameBC,
+ aWindow = window
+) {
+ let promiseRecordingEvent = expectObserverCalled(
+ "recording-device-events",
+ 1,
+ aFrameBC
+ );
+ aWindow.gIdentityHandler._identityBox.click();
+ let popup = aWindow.gIdentityHandler._identityPopup;
+ // 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("identity-popup-permission-list");
+ let cancelButton = permissions.querySelector(
+ ".identity-popup-permission-icon." +
+ aType +
+ "-icon ~ " +
+ ".identity-popup-permission-remove-button"
+ );
+ 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
+ );
+ }
+
+ cancelButton.click();
+ popup.hidePopup();
+
+ await promiseRecordingEvent;
+ await observerPromise1;
+ await observerPromise2;
+
+ 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 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 reloadAndAssertClosedStreams() {
+ info("reloading the web page");
+
+ // 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();
+
+ await checkNotSharing();
+}
+
+function checkDeviceSelectors(aAudio, aVideo, aScreen, aWindow = window) {
+ let document = aWindow.document;
+ let micSelector = document.getElementById("webRTC-selectMicrophone");
+ if (aAudio) {
+ ok(!micSelector.hidden, "microphone selector visible");
+ } else {
+ ok(micSelector.hidden, "microphone selector hidden");
+ }
+
+ let cameraSelector = document.getElementById("webRTC-selectCamera");
+ if (aVideo) {
+ ok(!cameraSelector.hidden, "camera selector visible");
+ } else {
+ ok(cameraSelector.hidden, "camera selector hidden");
+ }
+
+ let screenSelector = document.getElementById("webRTC-selectWindowOrScreen");
+ if (aScreen) {
+ ok(!screenSelector.hidden, "screen selector visible");
+ } else {
+ ok(screenSelector.hidden, "screen selector hidden");
+ }
+}
+
+// aExpected is for the current tab,
+// aExpectedGlobal is for all tabs.
+async function checkSharingUI(
+ aExpected,
+ aWin = window,
+ aExpectedGlobal = 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 identityBox = doc.getElementById("identity-box");
+ let webrtcSharingIcon = doc.getElementById("webrtc-sharing-icon");
+ ok(webrtcSharingIcon.hasAttribute("sharing"), "sharing attribute is set");
+ let sharing = webrtcSharingIcon.getAttribute("sharing");
+ if (aExpected.screen) {
+ is(sharing, "screen", "showing screen icon in the identity block");
+ } else if (aExpected.video == STATE_CAPTURE_ENABLED) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio == STATE_CAPTURE_ENABLED) {
+ is(sharing, "microphone", "showing mic icon in the identity block");
+ } else if (aExpected.video) {
+ is(sharing, "camera", "showing camera icon in the identity block");
+ } else if (aExpected.audio) {
+ is(sharing, "microphone", "showing mic icon in the identity block");
+ }
+
+ let allStreamsPaused = Object.values(aExpected).every(isPaused);
+ is(
+ webrtcSharingIcon.hasAttribute("paused"),
+ allStreamsPaused,
+ "sharing icon(s) should be in paused state when paused"
+ );
+
+ // Then check the sharing indicators inside the control center panel.
+ identityBox.click();
+ let popup = aWin.gIdentityHandler._identityPopup;
+ // 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("identity-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)];
+ is(
+ !!aWin.gIdentityHandler._sharingState.webRTC[id],
+ !!expected,
+ "sharing state for " + id + " as expected"
+ );
+ let icon = permissions.querySelectorAll(
+ ".identity-popup-permission-icon." + id + "-icon"
+ );
+ if (expected) {
+ is(icon.length, 1, "should show " + id + " icon in control center panel");
+ is(
+ icon[0].classList.contains("in-use"),
+ expected && !isPaused(expected),
+ "icon should have the in-use class, unless paused"
+ );
+ } else if (!icon.length) {
+ ok(true, "should not show " + id + " icon in the control center 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(icon.length, 1, "should not show more than 1 " + id + " icon");
+ }
+ }
+ aWin.gIdentityHandler._identityPopup.hidePopup();
+ await TestUtils.waitForCondition(
+ () => identityPopupHidden(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 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 identityPopupHidden(win = window) {
+ let popup = win.gIdentityHandler._identityPopup;
+ 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(
+ identityPopupHidden(),
+ "should start the test with the control center 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) {
+ 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();
+ }
+ }
+
+ // 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;
+
+ checkDeviceSelectors(mic, camera);
+ 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(false, false, true, window);
+
+ let document = window.document;
+
+ let menulist = document.getElementById("webRTC-selectWindow-menulist");
+ let displayMediaSource;
+
+ if (screenOrWin == SHARE_SCREEN) {
+ displayMediaSource = "screen";
+ } else if (screenOrWin == SHARE_WINDOW) {
+ displayMediaSource = "window";
+ } else {
+ throw new Error("Got an invalid argument to shareDevices.");
+ }
+
+ let menuitem = null;
+ for (let i = 0; i < menulist.itemCount; ++i) {
+ let current = menulist.getItemAtIndex(i);
+ if (current.mediaSource == displayMediaSource) {
+ menuitem = current;
+ break;
+ }
+ }
+
+ Assert.ok(menuitem, "Should have found an appropriate display menuitem");
+ menuitem.doCommand();
+
+ let notification = window.PopupNotifications.panel.firstElementChild;
+
+ let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
+ let observerPromise2 = expectObserverCalled("recording-device-events");
+ await promiseMessage(
+ "ok",
+ () => {
+ notification.button.click();
+ },
+ 1,
+ browser
+ );
+ await observerPromise1;
+ await observerPromise2;
+ }
+}
diff --git a/browser/base/content/test/webrtc/legacyIndicator/browser.ini b/browser/base/content/test/webrtc/legacyIndicator/browser.ini
new file mode 100644
index 0000000000..f9b304b553
--- /dev/null
+++ b/browser/base/content/test/webrtc/legacyIndicator/browser.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+support-files =
+ ../get_user_media.html
+ ../get_user_media_in_frame.html
+ ../get_user_media_in_xorigin_frame.html
+ ../get_user_media_in_xorigin_frame_ancestor.html
+ ../head.js
+prefs =
+ privacy.webrtc.allowSilencingNotifications=false
+ privacy.webrtc.legacyGlobalIndicator=true
+ privacy.webrtc.sharedTabWarning=false
+
+[../browser_devices_get_user_media.js]
+skip-if = (os == "linux" && debug) # linux: bug 976544
+[../browser_devices_get_user_media_anim.js]
+[../browser_devices_get_user_media_default_permissions.js]
+[../browser_devices_get_user_media_in_frame.js]
+skip-if = debug # bug 1369731
+[../browser_devices_get_user_media_in_xorigin_frame.js]
+skip-if = debug # bug 1369731
+[../browser_devices_get_user_media_in_xorigin_frame_chain.js]
+[../browser_devices_get_user_media_multi_process.js]
+skip-if = (debug && os == "win") # bug 1393761
+[../browser_devices_get_user_media_paused.js]
+skip-if = (os == "win" && !debug) || (os =="linux" && !debug && bits == 64) # Bug 1440900
+[../browser_devices_get_user_media_screen.js]
+skip-if = (os == 'linux') # Bug 1503991
+[../browser_devices_get_user_media_tear_off_tab.js]
+[../browser_devices_get_user_media_unprompted_access.js]
+[../browser_devices_get_user_media_unprompted_access_in_frame.js]
+[../browser_devices_get_user_media_unprompted_access_tear_off_tab.js]
+skip-if = (os == "win" && bits == 64) # win8: bug 1334752
+[../browser_devices_get_user_media_unprompted_access_queue_request.js]
+[../browser_webrtc_hooks.js]
+[../browser_devices_get_user_media_queue_request.js]
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..62c63a85c4
--- /dev/null
+++ b/browser/base/content/test/webrtc/single_peerconnection.html
@@ -0,0 +1,14 @@
+<!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();
+ await pc.setLocalDescription(await pc.createOffer({offerToReceiveAudio: true}));
+ };
+ test();
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/zoom/.eslintrc.js b/browser/base/content/test/zoom/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/zoom/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/zoom/browser.ini b/browser/base/content/test/zoom/browser.ini
new file mode 100644
index 0000000000..fdc82c1e65
--- /dev/null
+++ b/browser/base/content/test/zoom/browser.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+support-files =
+ head.js
+ ../general/moz.png
+ zoom_test.html
+
+[browser_background_link_zoom_reset.js]
+[browser_background_zoom.js]
+[browser_default_zoom.js]
+[browser_default_zoom_multitab.js]
+[browser_default_zoom_fission.js]
+[browser_default_zoom_sitespecific.js]
+[browser_image_zoom_tabswitch.js]
+skip-if = (os == "mac") #Bug 1526628
+[browser_mousewheel_zoom.js]
+[browser_sitespecific_background_pref.js]
+[browser_sitespecific_image_zoom.js]
+[browser_sitespecific_video_zoom.js]
+support-files =
+ ../general/video.ogg
+skip-if = os == "win" && debug || (verify && debug && (os == 'linux')) # Bug 1315042
+[browser_subframe_textzoom.js]
+[browser_tabswitch_zoom_flicker.js]
+skip-if = (debug && os == "linux" && bits == 64) || (!debug && webrender && os == "win") # Bug 1652383
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..e577efeac7
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_background_link_zoom_reset.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/. */
+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);
+ 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..3f30b1ab81
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_background_zoom.js
@@ -0,0 +1,113 @@
+var gTestPage =
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+var gTestImage =
+ "http://example.org/browser/browser/base/content/test/general/moz.png";
+var gTab1, gTab2, gTab3;
+var gLevel;
+
+function test() {
+ waitForExplicitFinish();
+ registerCleanupFunction(async () => {
+ await new Promise(resolve => {
+ ContentPrefService2.removeByName(FullZoom.name, Cu.createLoadContext(), {
+ handleCompletion: resolve,
+ });
+ });
+ });
+
+ (async function() {
+ gTab1 = BrowserTestUtils.addTab(gBrowser, gTestPage);
+ gTab2 = BrowserTestUtils.addTab(gBrowser);
+ gTab3 = BrowserTestUtils.addTab(gBrowser);
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.load(gTab1, gTestPage);
+ await FullZoomHelper.load(gTab2, gTestPage);
+ })().then(secondPageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function secondPageLoaded() {
+ (async function() {
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+ FullZoomHelper.zoomTest(gTab3, 1, "Initial zoom of tab 3 should be 1");
+
+ // Now have three tabs, two with the test page, one blank. Tab 1 is selected
+ // Zoom tab 1
+ FullZoom.enlarge();
+ gLevel = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1));
+
+ ok(gLevel > 1, "New zoom for tab 1 should be greater than 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2");
+ FullZoomHelper.zoomTest(gTab3, 1, "Zooming tab 1 should not affect tab 3");
+
+ await FullZoomHelper.load(gTab3, gTestPage);
+ })().then(thirdPageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function thirdPageLoaded() {
+ (async function() {
+ FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed");
+ FullZoomHelper.zoomTest(gTab2, 1, "Tab 2 should still not be affected");
+ FullZoomHelper.zoomTest(
+ gTab3,
+ gLevel,
+ "Tab 3 should have zoomed as it was loading in the background"
+ );
+
+ // Switching to tab 2 should update its zoom setting.
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed");
+ FullZoomHelper.zoomTest(gTab2, gLevel, "Tab 2 should be zoomed now");
+ FullZoomHelper.zoomTest(gTab3, gLevel, "Tab 3 should still be zoomed");
+
+ await FullZoomHelper.load(gTab1, gTestImage);
+ })().then(imageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function imageLoaded() {
+ (async function() {
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should be 1 when image was loaded in the background"
+ );
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should still be 1 when tab with image is selected"
+ );
+ })().then(imageZoomSwitch, FullZoomHelper.failAndContinue(finish));
+}
+
+function imageZoomSwitch() {
+ (async function() {
+ await FullZoomHelper.navigate(FullZoomHelper.BACK);
+ await FullZoomHelper.navigate(FullZoomHelper.FORWARD);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Tab 1 should not be zoomed when an image loads"
+ );
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Tab 1 should still not be zoomed when deselected"
+ );
+ })().then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+var finishTestStarted = false;
+function finishTest() {
+ (async function() {
+ ok(!finishTestStarted, "finishTest called more than once");
+ finishTestStarted = true;
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab3);
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_default_zoom.js b/browser/base/content/test/zoom/browser_default_zoom.js
new file mode 100644
index 0000000000..457d009da2
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom.js
@@ -0,0 +1,148 @@
+/* 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();
+});
+
+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..1e379976a7
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom_multitab.js
@@ -0,0 +1,186 @@
+/* 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() {
+ const TEST_PAGE_URL_1 = "http://example.com/";
+ 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() {
+ const TEST_PAGE_URL_1 = "http://example.net/";
+ 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..c0c4a0480e
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_default_zoom_multitab_002.js
@@ -0,0 +1,91 @@
+/* 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() {
+ const TEST_PAGE_URL_1 = "http://example.net";
+ 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..90ce4cd604
--- /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 FullZoomHelper.refreshTab(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..3b2b935163
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_image_zoom_tabswitch.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 =
+ "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..1630a4f540
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_mousewheel_zoom.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const TEST_PAGE =
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+
+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_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..7bf909c4b7
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_sitespecific_background_pref.js
@@ -0,0 +1,34 @@
+function test() {
+ waitForExplicitFinish();
+
+ (async function() {
+ let testPage =
+ "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..0985f67357
--- /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..9f09a9e8ac
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_sitespecific_video_zoom.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const TEST_PAGE =
+ "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html";
+const TEST_VIDEO =
+ "http://example.org/browser/browser/base/content/test/general/video.ogg";
+
+var gTab1, gTab2, gLevel1;
+
+function test() {
+ waitForExplicitFinish();
+
+ (async function() {
+ gTab1 = BrowserTestUtils.addTab(gBrowser);
+ gTab2 = BrowserTestUtils.addTab(gBrowser);
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.load(gTab1, TEST_PAGE);
+ await FullZoomHelper.load(gTab2, TEST_VIDEO);
+ })().then(zoomTab1, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab1() {
+ (async function() {
+ is(gBrowser.selectedTab, gTab1, "Tab 1 is selected");
+
+ // Reset zoom level if we run this test > 1 time in same browser session.
+ var level1 = ZoomManager.getZoomForBrowser(
+ gBrowser.getBrowserForTab(gTab1)
+ );
+ if (level1 > 1) {
+ FullZoom.reduce();
+ }
+
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+
+ FullZoom.enlarge();
+ gLevel1 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1));
+
+ ok(gLevel1 > 1, "New zoom for tab 1 should be greater than 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2");
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(
+ gTab2,
+ 1,
+ "Tab 2 is still unzoomed after it is selected"
+ );
+ FullZoomHelper.zoomTest(gTab1, gLevel1, "Tab 1 is still zoomed");
+ })().then(zoomTab2, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab2() {
+ (async function() {
+ is(gBrowser.selectedTab, gTab2, "Tab 2 is selected");
+
+ FullZoom.reduce();
+ let level2 = ZoomManager.getZoomForBrowser(
+ gBrowser.getBrowserForTab(gTab2)
+ );
+
+ ok(level2 < 1, "New zoom for tab 2 should be less than 1");
+ FullZoomHelper.zoomTest(
+ gTab1,
+ gLevel1,
+ "Zooming tab 2 should not affect tab 1"
+ );
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ gLevel1,
+ "Tab 1 should have the same zoom after it's selected"
+ );
+ })().then(testNavigation, FullZoomHelper.failAndContinue(finish));
+}
+
+function testNavigation() {
+ (async function() {
+ await FullZoomHelper.load(gTab1, TEST_VIDEO);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should be 1 when a video was loaded"
+ );
+ await waitForNextTurn(); // trying to fix orange bug 806046
+ await FullZoomHelper.navigate(FullZoomHelper.BACK);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ gLevel1,
+ "Zoom should be restored when a page is loaded"
+ );
+ await waitForNextTurn(); // trying to fix orange bug 806046
+ await FullZoomHelper.navigate(FullZoomHelper.FORWARD);
+ FullZoomHelper.zoomTest(
+ gTab1,
+ 1,
+ "Zoom should be 1 again when navigating back to a video"
+ );
+ })().then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+function waitForNextTurn() {
+ return new Promise(resolve => {
+ setTimeout(() => resolve(), 0);
+ });
+}
+
+var finishTestStarted = false;
+function finishTest() {
+ (async function() {
+ ok(!finishTestStarted, "finishTest called more than once");
+ finishTestStarted = true;
+
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ await FullZoom.reset();
+ await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ })().then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/zoom/browser_subframe_textzoom.js b/browser/base/content/test/zoom/browser_subframe_textzoom.js
new file mode 100644
index 0000000000..1c7cf9c3be
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_subframe_textzoom.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * 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>';
+ 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..f87eb46363
--- /dev/null
+++ b/browser/base/content/test/zoom/browser_tabswitch_zoom_flicker.js
@@ -0,0 +1,44 @@
+var tab;
+
+function test() {
+ // ----------
+ // Test setup
+
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("browser.zoom.updateBackgroundTabs", true);
+ Services.prefs.setBoolPref("browser.zoom.siteSpecific", true);
+
+ let uri =
+ "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/head.js b/browser/base/content/test/zoom/head.js
new file mode 100644
index 0000000000..0e8f13a938
--- /dev/null
+++ b/browser/base/content/test/zoom/head.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { BrowserTestUtils } = ChromeUtils.import(
+ "resource://testing-common/BrowserTestUtils.jsm"
+);
+
+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();
+ },
+ }
+ );
+ });
+ },
+
+ 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) {
+ Cu.reportError(error);
+ },
+ });
+ });
+ },
+
+ async refreshTab(tab = gBrowser.selectedTab) {
+ info("Refreshing tab.");
+ const finished = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.reloadTab(tab);
+ await finished;
+ info("Tab finished refreshing.");
+ },
+
+ 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) {
+ Cu.reportError(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.loadURI(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>
diff --git a/browser/base/content/titlebar-items.inc.xhtml b/browser/base/content/titlebar-items.inc.xhtml
new file mode 100644
index 0000000000..3c8a1a4fc2
--- /dev/null
+++ b/browser/base/content/titlebar-items.inc.xhtml
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<hbox class="titlebar-buttonbox-container" skipintoolbarset="true">
+ <hbox class="titlebar-buttonbox titlebar-color">
+ <toolbarbutton class="titlebar-button titlebar-min"
+ oncommand="window.minimize();"
+ data-l10n-id="browser-window-minimize-button"
+ />
+ <toolbarbutton class="titlebar-button titlebar-max"
+ oncommand="window.maximize();"
+ data-l10n-id="browser-window-maximize-button"
+ />
+ <toolbarbutton class="titlebar-button titlebar-restore"
+ oncommand="window.restore();"
+ data-l10n-id="browser-window-restore-down-button"
+ />
+ <toolbarbutton class="titlebar-button titlebar-close"
+ command="cmd_closeWindow"
+ data-l10n-id="browser-window-close-button"
+ />
+ </hbox>
+</hbox>
diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js
new file mode 100644
index 0000000000..7989f58aee
--- /dev/null
+++ b/browser/base/content/utilityOverlay.js
@@ -0,0 +1,1162 @@
+/* -*- 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/. */
+
+// Services = object with smart getters for common XPCOM services
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.jsm",
+ ExtensionSettingsStore: "resource://gre/modules/ExtensionSettingsStore.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ ShellService: "resource:///modules/ShellService.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "ReferrerInfo", () =>
+ Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ )
+);
+
+Object.defineProperty(this, "BROWSER_NEW_TAB_URL", {
+ enumerable: true,
+ get() {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ if (
+ !PrivateBrowsingUtils.permanentPrivateBrowsing &&
+ !AboutNewTab.newTabURLOverridden
+ ) {
+ return "about:privatebrowsing";
+ }
+ // If an extension controls the setting and does not have private
+ // browsing permission, use the default setting.
+ let extensionControlled = Services.prefs.getBoolPref(
+ "browser.newtab.extensionControlled",
+ false
+ );
+ let privateAllowed = Services.prefs.getBoolPref(
+ "browser.newtab.privateAllowed",
+ false
+ );
+ // There is a potential on upgrade that the prefs are not set yet, so we double check
+ // for moz-extension.
+ if (
+ !privateAllowed &&
+ (extensionControlled ||
+ AboutNewTab.newTabURL.startsWith("moz-extension://"))
+ ) {
+ return "about:privatebrowsing";
+ }
+ }
+ return AboutNewTab.newTabURL;
+ },
+});
+
+var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
+
+var gBidiUI = false;
+
+/**
+ * Determines whether the given url is considered a special URL for new tabs.
+ */
+function isBlankPageURL(aURL) {
+ return (
+ aURL == "about:blank" ||
+ aURL == "about:home" ||
+ aURL == "about:welcome" ||
+ aURL == BROWSER_NEW_TAB_URL
+ );
+}
+
+function getTopWin(skipPopups) {
+ // If this is called in a browser window, use that window regardless of
+ // whether it's the frontmost window, since commands can be executed in
+ // background windows (bug 626148).
+ if (
+ top.document.documentElement.getAttribute("windowtype") ==
+ "navigator:browser" &&
+ (!skipPopups || top.toolbar.visible)
+ ) {
+ return top;
+ }
+
+ return BrowserWindowTracker.getTopWindow({
+ private: PrivateBrowsingUtils.isWindowPrivate(window),
+ allowPopups: !skipPopups,
+ });
+}
+
+function doGetProtocolFlags(aURI) {
+ let handler = Services.io.getProtocolHandler(aURI.scheme);
+ // see DoGetProtocolFlags in nsIProtocolHandler.idl
+ return handler instanceof Ci.nsIProtocolHandlerWithDynamicFlags
+ ? handler
+ .QueryInterface(Ci.nsIProtocolHandlerWithDynamicFlags)
+ .getFlagsForURI(aURI)
+ : handler.protocolFlags;
+}
+
+/**
+ * openUILink handles clicks on UI elements that cause URLs to load.
+ *
+ * @param {string} url
+ * @param {Event | Object} event Event or JSON object representing an Event
+ * @param {Boolean | Object} aIgnoreButton
+ * Boolean or object with the same properties as
+ * accepted by openUILinkIn, plus "ignoreButton"
+ * and "ignoreAlt".
+ * @param {Boolean} aIgnoreAlt
+ * @param {Boolean} aAllowThirdPartyFixup
+ * @param {Object} aPostData
+ * @param {Object} aReferrerInfo
+ */
+function openUILink(
+ url,
+ event,
+ aIgnoreButton,
+ aIgnoreAlt,
+ aAllowThirdPartyFixup,
+ aPostData,
+ aReferrerInfo
+) {
+ event = getRootEvent(event);
+ let params;
+
+ if (aIgnoreButton && typeof aIgnoreButton == "object") {
+ params = aIgnoreButton;
+
+ // don't forward "ignoreButton" and "ignoreAlt" to openUILinkIn
+ aIgnoreButton = params.ignoreButton;
+ aIgnoreAlt = params.ignoreAlt;
+ delete params.ignoreButton;
+ delete params.ignoreAlt;
+ } else {
+ params = {
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ postData: aPostData,
+ referrerInfo: aReferrerInfo,
+ initiatingDoc: event ? event.target.ownerDocument : null,
+ };
+ }
+
+ if (!params.triggeringPrincipal) {
+ throw new Error(
+ "Required argument triggeringPrincipal missing within openUILink"
+ );
+ }
+
+ let where = whereToOpenLink(event, aIgnoreButton, aIgnoreAlt);
+ openUILinkIn(url, where, params);
+}
+
+// Utility function to check command events for potential middle-click events
+// from checkForMiddleClick and unwrap them.
+function getRootEvent(aEvent) {
+ // Part of the fix for Bug 1523813.
+ // Middle-click events arrive here wrapped in different numbers (1-2) of
+ // command events, depending on the button originally clicked.
+ if (!aEvent) {
+ return aEvent;
+ }
+ let tempEvent = aEvent;
+ while (tempEvent.sourceEvent) {
+ if (tempEvent.sourceEvent.button == 1) {
+ aEvent = tempEvent.sourceEvent;
+ break;
+ }
+ tempEvent = tempEvent.sourceEvent;
+ }
+ return aEvent;
+}
+
+/**
+ * whereToOpenLink() looks at an event to decide where to open a link.
+ *
+ * The event may be a mouse event (click, double-click, middle-click) or keypress event (enter).
+ *
+ * On Windows, the modifiers are:
+ * Ctrl new tab, selected
+ * Shift new window
+ * Ctrl+Shift new tab, in background
+ * Alt save
+ *
+ * Middle-clicking is the same as Ctrl+clicking (it opens a new tab).
+ *
+ * Exceptions:
+ * - Alt is ignored for menu items selected using the keyboard so you don't accidentally save stuff.
+ * (Currently, the Alt isn't sent here at all for menu items, but that will change in bug 126189.)
+ * - Alt is hard to use in context menus, because pressing Alt closes the menu.
+ * - Alt can't be used on the bookmarks toolbar because Alt is used for "treat this as something draggable".
+ * - The button is ignored for the middle-click-paste-URL feature, since it's always a middle-click.
+ *
+ * @param e {Event|Object} Event or JSON Object
+ * @param ignoreButton {Boolean}
+ * @param ignoreAlt {Boolean}
+ * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
+ */
+function whereToOpenLink(e, ignoreButton, ignoreAlt) {
+ // This method must treat a null event like a left click without modifier keys (i.e.
+ // e = { shiftKey:false, ctrlKey:false, metaKey:false, altKey:false, button:0 })
+ // for compatibility purposes.
+ if (!e) {
+ return "current";
+ }
+
+ e = getRootEvent(e);
+
+ var shift = e.shiftKey;
+ var ctrl = e.ctrlKey;
+ var meta = e.metaKey;
+ var alt = e.altKey && !ignoreAlt;
+
+ // ignoreButton allows "middle-click paste" to use function without always opening in a new window.
+ let middle = !ignoreButton && e.button == 1;
+ let middleUsesTabs = Services.prefs.getBoolPref(
+ "browser.tabs.opentabfor.middleclick",
+ true
+ );
+ let middleUsesNewWindow = Services.prefs.getBoolPref(
+ "middlemouse.openNewWindow",
+ false
+ );
+
+ // Don't do anything special with right-mouse clicks. They're probably clicks on context menu items.
+
+ var metaKey = AppConstants.platform == "macosx" ? meta : ctrl;
+ if (metaKey || (middle && middleUsesTabs)) {
+ return shift ? "tabshifted" : "tab";
+ }
+
+ if (alt && Services.prefs.getBoolPref("browser.altClickSave", false)) {
+ return "save";
+ }
+
+ if (shift || (middle && !middleUsesTabs && middleUsesNewWindow)) {
+ return "window";
+ }
+
+ return "current";
+}
+
+/* openTrustedLinkIn will attempt to open the given URI using the SystemPrincipal
+ * as the trigeringPrincipal, unless a more specific Principal is provided.
+ *
+ * See openUILinkIn for a discussion of parameters
+ */
+function openTrustedLinkIn(url, where, aParams) {
+ var params = aParams;
+
+ if (!params) {
+ params = {};
+ }
+
+ if (!params.triggeringPrincipal) {
+ params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ }
+
+ openUILinkIn(url, where, params);
+}
+
+/* openWebLinkIn will attempt to open the given URI using the NullPrincipal
+ * as the triggeringPrincipal, unless a more specific Principal is provided.
+ *
+ * See openUILinkIn for a discussion of parameters
+ */
+function openWebLinkIn(url, where, params) {
+ if (!params) {
+ params = {};
+ }
+
+ if (!params.triggeringPrincipal) {
+ params.triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ );
+ }
+ if (params.triggeringPrincipal.isSystemPrincipal) {
+ throw new Error(
+ "System principal should never be passed into openWebLinkIn()"
+ );
+ }
+
+ openUILinkIn(url, where, params);
+}
+
+/* openUILinkIn opens a URL in a place specified by the parameter |where|.
+ *
+ * |where| can be:
+ * "current" current tab (if there aren't any browser windows, then in a new window instead)
+ * "tab" new tab (if there aren't any browser windows, then in a new window instead)
+ * "tabshifted" same as "tab" but in background if default is to select new tabs, and vice versa
+ * "window" new window
+ * "save" save to disk (with no filename hint!)
+ *
+ * DEPRECATION WARNING:
+ * USE -> openTrustedLinkIn(url, where, aParams) if the source is always
+ * a user event on a user- or product-specified URL (as
+ * opposed to URLs provided by a webpage)
+ * USE -> openWebLinkIn(url, where, aParams) if the URI should be loaded
+ * with a specific triggeringPrincipal, for instance, if
+ * the url was supplied by web content.
+ * DEPRECATED -> openUILinkIn(url, where, AllowThirdPartyFixup, aPostData, ...)
+ *
+ *
+ * allowThirdPartyFixup controls whether third party services such as Google's
+ * I Feel Lucky are allowed to interpret this URL. This parameter may be
+ * undefined, which is treated as false.
+ *
+ * Instead of aAllowThirdPartyFixup, you may also pass an object with any of
+ * these properties:
+ * allowThirdPartyFixup (boolean)
+ * fromChrome (boolean)
+ * postData (nsIInputStream)
+ * referrerInfo (nsIReferrerInfo)
+ * relatedToCurrent (boolean)
+ * skipTabAnimation (boolean)
+ * allowPinnedTabHostChange (boolean)
+ * allowPopups (boolean)
+ * userContextId (unsigned int)
+ * targetBrowser (XUL browser)
+ */
+function openUILinkIn(
+ url,
+ where,
+ aAllowThirdPartyFixup,
+ aPostData,
+ aReferrerInfo
+) {
+ var params;
+
+ if (arguments.length == 3 && typeof arguments[2] == "object") {
+ params = aAllowThirdPartyFixup;
+ }
+ if (!params || !params.triggeringPrincipal) {
+ throw new Error(
+ "Required argument triggeringPrincipal missing within openUILinkIn"
+ );
+ }
+
+ params.fromChrome = params.fromChrome ?? true;
+
+ openLinkIn(url, where, params);
+}
+
+/* eslint-disable complexity */
+function openLinkIn(url, where, params) {
+ if (!where || !url) {
+ return;
+ }
+
+ var aFromChrome = params.fromChrome;
+ var aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ var aPostData = params.postData;
+ var aCharset = params.charset;
+ var aReferrerInfo = params.referrerInfo
+ ? params.referrerInfo
+ : new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, null);
+ var aRelatedToCurrent = params.relatedToCurrent;
+ var aAllowInheritPrincipal = !!params.allowInheritPrincipal;
+ var aAllowMixedContent = params.allowMixedContent;
+ var aForceAllowDataURI = params.forceAllowDataURI;
+ var aInBackground = params.inBackground;
+ var aInitiatingDoc = params.initiatingDoc;
+ var aIsPrivate = params.private;
+ var aSkipTabAnimation = params.skipTabAnimation;
+ var aAllowPinnedTabHostChange = !!params.allowPinnedTabHostChange;
+ var aAllowPopups = !!params.allowPopups;
+ var aUserContextId = params.userContextId;
+ var aIndicateErrorPageLoad = params.indicateErrorPageLoad;
+ var aPrincipal = params.originPrincipal;
+ var aStoragePrincipal = params.originStoragePrincipal;
+ var aTriggeringPrincipal = params.triggeringPrincipal;
+ var aCsp = params.csp;
+ var aForceAboutBlankViewerInCurrent = params.forceAboutBlankViewerInCurrent;
+ var aResolveOnNewTabCreated = params.resolveOnNewTabCreated;
+
+ if (!aTriggeringPrincipal) {
+ throw new Error("Must load with a triggering Principal");
+ }
+
+ if (where == "save") {
+ if ("isContentWindowPrivate" in params) {
+ saveURL(
+ url,
+ null,
+ null,
+ true,
+ true,
+ aReferrerInfo,
+ null,
+ null,
+ params.isContentWindowPrivate,
+ aPrincipal
+ );
+ } else {
+ if (!aInitiatingDoc) {
+ Cu.reportError(
+ "openUILink/openLinkIn was called with " +
+ "where == 'save' but without initiatingDoc. See bug 814264."
+ );
+ return;
+ }
+ saveURL(url, null, null, true, true, aReferrerInfo, null, aInitiatingDoc);
+ }
+ return;
+ }
+
+ // Establish which window we'll load the link in.
+ let w;
+ if (where == "current" && params.targetBrowser) {
+ w = params.targetBrowser.ownerGlobal;
+ } else {
+ w = getTopWin();
+ }
+ // We don't want to open tabs in popups, so try to find a non-popup window in
+ // that case.
+ if ((where == "tab" || where == "tabshifted") && w && !w.toolbar.visible) {
+ w = getTopWin(true);
+ aRelatedToCurrent = false;
+ }
+
+ // Teach the principal about the right OA to use, e.g. in case when
+ // opening a link in a new private window, or in a new container tab.
+ // Please note we do not have to do that for SystemPrincipals and we
+ // can not do it for NullPrincipals since NullPrincipals are only
+ // identical if they actually are the same object (See Bug: 1346759)
+ function useOAForPrincipal(principal) {
+ if (principal && principal.isContentPrincipal) {
+ let attrs = {
+ userContextId: aUserContextId,
+ privateBrowsingId:
+ aIsPrivate || (w && PrivateBrowsingUtils.isWindowPrivate(w)),
+ firstPartyDomain: principal.originAttributes.firstPartyDomain,
+ };
+ return Services.scriptSecurityManager.principalWithOA(principal, attrs);
+ }
+ return principal;
+ }
+ aPrincipal = useOAForPrincipal(aPrincipal);
+ aStoragePrincipal = useOAForPrincipal(aStoragePrincipal);
+ aTriggeringPrincipal = useOAForPrincipal(aTriggeringPrincipal);
+
+ if (!w || where == "window") {
+ let features = "chrome,dialog=no,all";
+ if (aIsPrivate) {
+ features += ",private";
+ // To prevent regular browsing data from leaking to private browsing sites,
+ // strip the referrer when opening a new private window. (See Bug: 1409226)
+ aReferrerInfo = new ReferrerInfo(
+ aReferrerInfo.referrerPolicy,
+ false,
+ aReferrerInfo.originalReferrer
+ );
+ }
+
+ // This propagates to window.arguments.
+ var sa = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+
+ var wuri = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ wuri.data = url;
+
+ let charset = null;
+ if (aCharset) {
+ charset = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ charset.data = "charset=" + aCharset;
+ }
+
+ var allowThirdPartyFixupSupports = Cc[
+ "@mozilla.org/supports-PRBool;1"
+ ].createInstance(Ci.nsISupportsPRBool);
+ allowThirdPartyFixupSupports.data = aAllowThirdPartyFixup;
+
+ var userContextIdSupports = Cc[
+ "@mozilla.org/supports-PRUint32;1"
+ ].createInstance(Ci.nsISupportsPRUint32);
+ userContextIdSupports.data = aUserContextId;
+
+ sa.appendElement(wuri);
+ sa.appendElement(charset);
+ sa.appendElement(aReferrerInfo);
+ sa.appendElement(aPostData);
+ sa.appendElement(allowThirdPartyFixupSupports);
+ sa.appendElement(userContextIdSupports);
+ sa.appendElement(aPrincipal);
+ sa.appendElement(aStoragePrincipal);
+ sa.appendElement(aTriggeringPrincipal);
+ sa.appendElement(null); // allowInheritPrincipal
+ sa.appendElement(aCsp);
+
+ const sourceWindow = w || window;
+ let win;
+ if (params.frameID != undefined && sourceWindow) {
+ // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
+ // event if it contains the expected frameID params.
+ // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
+ // opening a new window using the keyboard shortcut).
+ const sourceTabBrowser = sourceWindow.gBrowser.selectedBrowser;
+ let delayedStartupObserver = aSubject => {
+ if (aSubject == win) {
+ Services.obs.removeObserver(
+ delayedStartupObserver,
+ "browser-delayed-startup-finished"
+ );
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ url,
+ createdTabBrowser: win.gBrowser.selectedBrowser,
+ sourceTabBrowser,
+ sourceFrameID: params.frameID,
+ },
+ },
+ "webNavigation-createdNavigationTarget"
+ );
+ }
+ };
+ Services.obs.addObserver(
+ delayedStartupObserver,
+ "browser-delayed-startup-finished"
+ );
+ }
+ win = Services.ww.openWindow(
+ sourceWindow,
+ AppConstants.BROWSER_CHROME_URL,
+ null,
+ features,
+ sa
+ );
+ return;
+ }
+
+ // We're now committed to loading the link in an existing browser window.
+
+ // Raise the target window before loading the URI, since loading it may
+ // result in a new frontmost window (e.g. "javascript:window.open('');").
+ w.focus();
+
+ let targetBrowser;
+ let loadInBackground;
+ let uriObj;
+
+ if (where == "current") {
+ targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser;
+ loadInBackground = false;
+
+ try {
+ uriObj = Services.io.newURI(url);
+ } catch (e) {}
+
+ if (
+ !aAllowPinnedTabHostChange &&
+ w.gBrowser.getTabForBrowser(targetBrowser).pinned &&
+ url != "about:crashcontent"
+ ) {
+ try {
+ // nsIURI.host can throw for non-nsStandardURL nsIURIs.
+ if (
+ !uriObj ||
+ (!uriObj.schemeIs("javascript") &&
+ targetBrowser.currentURI.host != uriObj.host)
+ ) {
+ where = "tab";
+ loadInBackground = false;
+ }
+ } catch (err) {
+ where = "tab";
+ loadInBackground = false;
+ }
+ }
+ } else {
+ // 'where' is "tab" or "tabshifted", so we'll load the link in a new tab.
+ loadInBackground = aInBackground;
+ if (loadInBackground == null) {
+ loadInBackground = aFromChrome
+ ? false
+ : Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+ }
+ }
+
+ let focusUrlBar = false;
+
+ switch (where) {
+ case "current":
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+
+ if (aAllowThirdPartyFixup) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL isn't supported for javascript URIs,
+ // i.e. it causes them not to load at all. Callers should strip
+ // "javascript:" from pasted strings to prevent blank tabs
+ if (!aAllowInheritPrincipal) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+
+ if (aAllowPopups) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS;
+ }
+ if (aIndicateErrorPageLoad) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV;
+ }
+ if (aForceAllowDataURI) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
+ }
+
+ let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
+ if (
+ aForceAboutBlankViewerInCurrent &&
+ (!uriObj || doGetProtocolFlags(uriObj) & URI_INHERITS_SECURITY_CONTEXT)
+ ) {
+ // Unless we know for sure we're not inheriting principals,
+ // force the about:blank viewer to have the right principal:
+ targetBrowser.createAboutBlankContentViewer(
+ aPrincipal,
+ aStoragePrincipal
+ );
+ }
+
+ targetBrowser.loadURI(url, {
+ triggeringPrincipal: aTriggeringPrincipal,
+ csp: aCsp,
+ flags,
+ referrerInfo: aReferrerInfo,
+ postData: aPostData,
+ userContextId: aUserContextId,
+ });
+
+ // Don't focus the content area if focus is in the address bar and we're
+ // loading the New Tab page.
+ focusUrlBar =
+ w.document.activeElement == w.gURLBar.inputField &&
+ w.isBlankPageURL(url);
+ break;
+ case "tabshifted":
+ loadInBackground = !loadInBackground;
+ // fall through
+ case "tab":
+ focusUrlBar =
+ !loadInBackground &&
+ w.isBlankPageURL(url) &&
+ !AboutNewTab.willNotifyUser;
+
+ let tabUsedForLoad = w.gBrowser.loadOneTab(url, {
+ referrerInfo: aReferrerInfo,
+ charset: aCharset,
+ postData: aPostData,
+ inBackground: loadInBackground,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ relatedToCurrent: aRelatedToCurrent,
+ skipAnimation: aSkipTabAnimation,
+ allowMixedContent: aAllowMixedContent,
+ userContextId: aUserContextId,
+ originPrincipal: aPrincipal,
+ originStoragePrincipal: aStoragePrincipal,
+ triggeringPrincipal: aTriggeringPrincipal,
+ allowInheritPrincipal: aAllowInheritPrincipal,
+ csp: aCsp,
+ focusUrlBar,
+ openerBrowser: params.openerBrowser,
+ });
+ targetBrowser = tabUsedForLoad.linkedBrowser;
+
+ if (aResolveOnNewTabCreated) {
+ aResolveOnNewTabCreated(targetBrowser);
+ }
+
+ if (params.frameID != undefined && w) {
+ // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
+ // event if it contains the expected frameID params.
+ // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
+ // opening a new tab using the keyboard shortcut).
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ url,
+ createdTabBrowser: targetBrowser,
+ sourceTabBrowser: w.gBrowser.selectedBrowser,
+ sourceFrameID: params.frameID,
+ },
+ },
+ "webNavigation-createdNavigationTarget"
+ );
+ }
+ break;
+ }
+
+ if (
+ !params.avoidBrowserFocus &&
+ !focusUrlBar &&
+ targetBrowser == w.gBrowser.selectedBrowser
+ ) {
+ // Focus the content, but only if the browser used for the load is selected.
+ targetBrowser.focus();
+ }
+}
+
+// Used as an onclick handler for UI elements with link-like behavior.
+// e.g. onclick="checkForMiddleClick(this, event);"
+function checkForMiddleClick(node, event) {
+ // We should be using the disabled property here instead of the attribute,
+ // but some elements that this function is used with don't support it (e.g.
+ // menuitem).
+ if (node.getAttribute("disabled") == "true") {
+ return;
+ } // Do nothing
+
+ if (event.button == 1) {
+ /* Execute the node's oncommand or command.
+ */
+
+ let cmdEvent = document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ window,
+ 0,
+ event.ctrlKey,
+ event.altKey,
+ event.shiftKey,
+ event.metaKey,
+ event,
+ event.mozInputSource
+ );
+ node.dispatchEvent(cmdEvent);
+
+ // Stop the propagation of the click event, to prevent the event from being
+ // handled more than once.
+ // E.g. see https://bugzilla.mozilla.org/show_bug.cgi?id=1657992#c4
+ event.stopPropagation();
+
+ // If the middle-click was on part of a menu, close the menu.
+ // (Menus close automatically with left-click but not with middle-click.)
+ closeMenus(event.target);
+ }
+}
+
+// Populate a menu with user-context menu items. This method should be called
+// by onpopupshowing passing the event as first argument.
+function createUserContextMenu(
+ event,
+ {
+ isContextMenu = false,
+ excludeUserContextId = 0,
+ showDefaultTab = false,
+ useAccessKeys = true,
+ } = {}
+) {
+ while (event.target.hasChildNodes()) {
+ event.target.firstChild.remove();
+ }
+
+ let bundle = Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ );
+ let docfrag = document.createDocumentFragment();
+
+ // If we are excluding a userContextId, we want to add a 'no-container' item.
+ if (excludeUserContextId || showDefaultTab) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("data-usercontextid", "0");
+ menuitem.setAttribute(
+ "label",
+ bundle.GetStringFromName("userContextNone.label")
+ );
+ menuitem.setAttribute(
+ "accesskey",
+ bundle.GetStringFromName("userContextNone.accesskey")
+ );
+
+ // We don't set an oncommand/command attribute because if we have
+ // to exclude a userContextId we are generating the contextMenu and
+ // isContextMenu will be true.
+
+ docfrag.appendChild(menuitem);
+
+ let menuseparator = document.createXULElement("menuseparator");
+ docfrag.appendChild(menuseparator);
+ }
+
+ ContextualIdentityService.getPublicIdentities().forEach(identity => {
+ if (identity.userContextId == excludeUserContextId) {
+ return;
+ }
+
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("data-usercontextid", identity.userContextId);
+ menuitem.setAttribute(
+ "label",
+ ContextualIdentityService.getUserContextLabel(identity.userContextId)
+ );
+
+ if (identity.accessKey && useAccessKeys) {
+ menuitem.setAttribute(
+ "accesskey",
+ bundle.GetStringFromName(identity.accessKey)
+ );
+ }
+
+ menuitem.classList.add("menuitem-iconic");
+ menuitem.classList.add("identity-color-" + identity.color);
+
+ if (!isContextMenu) {
+ menuitem.setAttribute("command", "Browser:NewUserContextTab");
+ }
+
+ menuitem.classList.add("identity-icon-" + identity.icon);
+
+ docfrag.appendChild(menuitem);
+ });
+
+ if (!isContextMenu) {
+ docfrag.appendChild(document.createXULElement("menuseparator"));
+
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute(
+ "label",
+ bundle.GetStringFromName("userContext.aboutPage.label")
+ );
+ if (useAccessKeys) {
+ menuitem.setAttribute(
+ "accesskey",
+ bundle.GetStringFromName("userContext.aboutPage.accesskey")
+ );
+ }
+ menuitem.setAttribute("command", "Browser:OpenAboutContainers");
+ docfrag.appendChild(menuitem);
+ }
+
+ event.target.appendChild(docfrag);
+ return true;
+}
+
+// Closes all popups that are ancestors of the node.
+function closeMenus(node) {
+ if ("tagName" in node) {
+ if (
+ node.namespaceURI ==
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" &&
+ (node.tagName == "menupopup" || node.tagName == "popup")
+ ) {
+ node.hidePopup();
+ }
+
+ closeMenus(node.parentNode);
+ }
+}
+
+/** This function takes in a key element and compares it to the keys pressed during an event.
+ *
+ * @param aEvent
+ * The KeyboardEvent event you want to compare against your key.
+ *
+ * @param aKey
+ * The <key> element checked to see if it was called in aEvent.
+ * For example, aKey can be a variable set to document.getElementById("key_close")
+ * to check if the close command key was pressed in aEvent.
+ */
+function eventMatchesKey(aEvent, aKey) {
+ let keyPressed = aKey.getAttribute("key").toLowerCase();
+ let keyModifiers = aKey.getAttribute("modifiers");
+ let modifiers = ["Alt", "Control", "Meta", "Shift"];
+
+ if (aEvent.key != keyPressed) {
+ return false;
+ }
+ let eventModifiers = modifiers.filter(modifier =>
+ aEvent.getModifierState(modifier)
+ );
+ // Check if aEvent has a modifier and aKey doesn't
+ if (eventModifiers.length && !keyModifiers.length) {
+ return false;
+ }
+ // Check whether aKey's modifiers match aEvent's modifiers
+ if (keyModifiers) {
+ keyModifiers = keyModifiers.split(/[\s,]+/);
+ // Capitalize first letter of aKey's modifers to compare to aEvent's modifier
+ keyModifiers.forEach(function(modifier, index) {
+ if (modifier == "accel") {
+ keyModifiers[index] =
+ AppConstants.platform == "macosx" ? "Meta" : "Control";
+ } else {
+ keyModifiers[index] = modifier[0].toUpperCase() + modifier.slice(1);
+ }
+ });
+ return modifiers.every(
+ modifier =>
+ keyModifiers.includes(modifier) == aEvent.getModifierState(modifier)
+ );
+ }
+ return true;
+}
+
+// Gather all descendent text under given document node.
+function gatherTextUnder(root) {
+ var text = "";
+ var node = root.firstChild;
+ var depth = 1;
+ while (node && depth > 0) {
+ // See if this node is text.
+ if (node.nodeType == Node.TEXT_NODE) {
+ // Add this text to our collection.
+ text += " " + node.data;
+ } else if (node instanceof HTMLImageElement) {
+ // If it has an "alt" attribute, add that.
+ var altText = node.getAttribute("alt");
+ if (altText && altText != "") {
+ text += " " + altText;
+ }
+ }
+ // Find next node to test.
+ // First, see if this node has children.
+ if (node.hasChildNodes()) {
+ // Go to first child.
+ node = node.firstChild;
+ depth++;
+ } else {
+ // No children, try next sibling (or parent next sibling).
+ while (depth > 0 && !node.nextSibling) {
+ node = node.parentNode;
+ depth--;
+ }
+ if (node.nextSibling) {
+ node = node.nextSibling;
+ }
+ }
+ }
+ // Strip leading and tailing whitespace.
+ text = text.trim();
+ // Compress remaining whitespace.
+ text = text.replace(/\s+/g, " ");
+ return text;
+}
+
+// This function exists for legacy reasons.
+function getShellService() {
+ return ShellService;
+}
+
+function isBidiEnabled() {
+ // first check the pref.
+ if (Services.prefs.getBoolPref("bidi.browser.ui", false)) {
+ return true;
+ }
+
+ // now see if the app locale is an RTL one.
+ const isRTL = Services.locale.isAppLocaleRTL;
+
+ if (isRTL) {
+ Services.prefs.setBoolPref("bidi.browser.ui", true);
+ }
+ return isRTL;
+}
+
+function openAboutDialog() {
+ for (let win of Services.wm.getEnumerator("Browser:About")) {
+ // Only open one about window (Bug 599573)
+ if (win.closed) {
+ continue;
+ }
+ win.focus();
+ return;
+ }
+
+ var features = "chrome,";
+ if (AppConstants.platform == "win") {
+ features += "centerscreen,dependent";
+ } else if (AppConstants.platform == "macosx") {
+ features += "resizable=no,minimizable=no";
+ } else {
+ features += "centerscreen,dependent,dialog=no";
+ }
+
+ window.openDialog("chrome://browser/content/aboutDialog.xhtml", "", features);
+}
+
+function openPreferences(paneID, extraArgs) {
+ // This function is duplicated from preferences.js.
+ function internalPrefCategoryNameToFriendlyName(aName) {
+ return (aName || "").replace(/^pane./, function(toReplace) {
+ return toReplace[4].toLowerCase();
+ });
+ }
+
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let friendlyCategoryName = internalPrefCategoryNameToFriendlyName(paneID);
+ let params;
+ if (extraArgs && extraArgs.urlParams) {
+ params = new URLSearchParams();
+ let urlParams = extraArgs.urlParams;
+ for (let name in urlParams) {
+ if (urlParams[name] !== undefined) {
+ params.set(name, urlParams[name]);
+ }
+ }
+ }
+ let preferencesURL =
+ "about:preferences" +
+ (params ? "?" + params : "") +
+ (friendlyCategoryName ? "#" + friendlyCategoryName : "");
+ let newLoad = true;
+ let browser = null;
+ if (!win) {
+ let windowArguments = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ let supportsStringPrefURL = Cc[
+ "@mozilla.org/supports-string;1"
+ ].createInstance(Ci.nsISupportsString);
+ supportsStringPrefURL.data = preferencesURL;
+ windowArguments.appendElement(supportsStringPrefURL);
+
+ win = Services.ww.openWindow(
+ null,
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,dialog=no,all",
+ windowArguments
+ );
+ } else {
+ let shouldReplaceFragment = friendlyCategoryName
+ ? "whenComparingAndReplace"
+ : "whenComparing";
+ newLoad = !win.switchToTabHavingURI(preferencesURL, true, {
+ ignoreFragment: shouldReplaceFragment,
+ replaceQueryString: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ browser = win.gBrowser.selectedBrowser;
+ }
+
+ if (newLoad) {
+ Services.obs.addObserver(function panesLoadedObs(prefWin, topic, data) {
+ if (!browser) {
+ browser = win.gBrowser.selectedBrowser;
+ }
+ if (prefWin != browser.contentWindow) {
+ return;
+ }
+ Services.obs.removeObserver(panesLoadedObs, "sync-pane-loaded");
+ }, "sync-pane-loaded");
+ } else if (paneID) {
+ browser.contentWindow.gotoPref(paneID);
+ }
+}
+
+/**
+ * Opens the troubleshooting information (about:support) page for this version
+ * of the application.
+ */
+function openTroubleshootingPage() {
+ openTrustedLinkIn("about:support", "tab");
+}
+
+/**
+ * Opens the feedback page for this version of the application.
+ */
+function openFeedbackPage() {
+ var url = Services.urlFormatter.formatURLPref("app.feedback.baseURL");
+ openTrustedLinkIn(url, "tab");
+}
+
+function openTourPage() {
+ let scope = {};
+ ChromeUtils.import("resource:///modules/UITour.jsm", scope);
+ openTrustedLinkIn(scope.UITour.url, "tab");
+}
+
+function buildHelpMenu() {
+ document.getElementById(
+ "feedbackPage"
+ ).disabled = !Services.policies.isAllowed("feedbackCommands");
+
+ document.getElementById(
+ "helpSafeMode"
+ ).disabled = !Services.policies.isAllowed("safeMode");
+
+ let supportMenu = Services.policies.getSupportMenu();
+ if (supportMenu) {
+ let menuitem = document.getElementById("helpPolicySupport");
+ menuitem.hidden = false;
+ menuitem.setAttribute("label", supportMenu.Title);
+ if ("AccessKey" in supportMenu) {
+ menuitem.setAttribute("accesskey", supportMenu.AccessKey);
+ }
+ document.getElementById("helpPolicySeparator").hidden = false;
+ }
+
+ // Enable/disable the "Report Web Forgery" menu item.
+ if (typeof gSafeBrowsing != "undefined") {
+ gSafeBrowsing.setReportPhishingMenu();
+ }
+
+ updateImportCommandEnabledState();
+}
+
+function isElementVisible(aElement) {
+ if (!aElement) {
+ return false;
+ }
+
+ // If aElement or a direct or indirect parent is hidden or collapsed,
+ // height, width or both will be 0.
+ var rect = aElement.getBoundingClientRect();
+ return rect.height > 0 && rect.width > 0;
+}
+
+function makeURLAbsolute(aBase, aUrl) {
+ // Note: makeURI() will throw if aUri is not a valid URI
+ return makeURI(aUrl, null, makeURI(aBase)).spec;
+}
+
+function getHelpLinkURL(aHelpTopic) {
+ var url = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ return url + aHelpTopic;
+}
+
+// aCalledFromModal is optional
+function openHelpLink(aHelpTopic, aCalledFromModal, aWhere) {
+ var url = getHelpLinkURL(aHelpTopic);
+ var where = aWhere;
+ if (!aWhere) {
+ where = aCalledFromModal ? "window" : "tab";
+ }
+
+ openTrustedLinkIn(url, where);
+}
+
+function openPrefsHelp(aEvent) {
+ let helpTopic = aEvent.target.getAttribute("helpTopic");
+ openHelpLink(helpTopic);
+}
+
+/**
+ * Updates the enabled state of the "Import From Another Browser" command
+ * depending on the DisableProfileImport policy.
+ */
+function updateImportCommandEnabledState() {
+ if (!Services.policies.isAllowed("profileImport")) {
+ document
+ .getElementById("cmd_file_importFromAnotherBrowser")
+ .setAttribute("disabled", "true");
+ document
+ .getElementById("cmd_help_importFromAnotherBrowser")
+ .setAttribute("disabled", "true");
+ }
+}
diff --git a/browser/base/content/webext-panels.js b/browser/base/content/webext-panels.js
new file mode 100644
index 0000000000..c5e535b4fc
--- /dev/null
+++ b/browser/base/content/webext-panels.js
@@ -0,0 +1,184 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Via webext-panels.xhtml
+/* import-globals-from browser.js */
+/* import-globals-from nsContextMenu.js */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm"
+);
+
+const { ExtensionUtils } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionUtils.jsm"
+);
+
+var { promiseEvent } = ExtensionUtils;
+
+function getBrowser(panel) {
+ let browser = document.getElementById("webext-panels-browser");
+ if (browser) {
+ return Promise.resolve(browser);
+ }
+
+ let stack = document.getElementById("webext-panels-stack");
+ if (!stack) {
+ stack = document.createXULElement("stack");
+ stack.setAttribute("flex", "1");
+ stack.setAttribute("id", "webext-panels-stack");
+ document.documentElement.appendChild(stack);
+ }
+
+ browser = document.createXULElement("browser");
+ browser.setAttribute("id", "webext-panels-browser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("disableglobalhistory", "true");
+ browser.setAttribute("messagemanagergroup", "webext-browsers");
+ browser.setAttribute("webextension-view-type", panel.viewType);
+ browser.setAttribute("context", "contentAreaContextMenu");
+ browser.setAttribute("tooltip", "aHTMLTooltip");
+ browser.setAttribute("autocompletepopup", "PopupAutoComplete");
+ browser.setAttribute("selectmenulist", "ContentSelectDropdown");
+
+ // Ensure that the browser is going to run in the same bc group as the other
+ // extension pages from the same addon.
+ browser.setAttribute(
+ "initialBrowsingContextGroupId",
+ panel.extension.policy.browsingContextGroupId
+ );
+
+ let readyPromise;
+ if (panel.extension.remote) {
+ browser.setAttribute("remote", "true");
+ let oa = E10SUtils.predictOriginAttributes({ browser });
+ browser.setAttribute(
+ "remoteType",
+ E10SUtils.getRemoteTypeForURI(
+ panel.uri,
+ /* remote */ true,
+ /* fission */ false,
+ E10SUtils.EXTENSION_REMOTE_TYPE,
+ null,
+ oa
+ )
+ );
+ readyPromise = promiseEvent(browser, "XULFrameLoaderCreated");
+ } else {
+ readyPromise = Promise.resolve();
+ }
+
+ stack.appendChild(browser);
+
+ browser.addEventListener(
+ "DoZoomEnlargeBy10",
+ () => {
+ let { ZoomManager } = browser.ownerGlobal;
+ let zoom = browser.fullZoom;
+ zoom += 0.1;
+ if (zoom > ZoomManager.MAX) {
+ zoom = ZoomManager.MAX;
+ }
+ browser.fullZoom = zoom;
+ },
+ true
+ );
+ browser.addEventListener(
+ "DoZoomReduceBy10",
+ () => {
+ let { ZoomManager } = browser.ownerGlobal;
+ let zoom = browser.fullZoom;
+ zoom -= 0.1;
+ if (zoom < ZoomManager.MIN) {
+ zoom = ZoomManager.MIN;
+ }
+ browser.fullZoom = zoom;
+ },
+ true
+ );
+
+ return readyPromise.then(() => {
+ ExtensionParent.apiManager.emit(
+ "extension-browser-inserted",
+ browser,
+ panel.browserInsertedData
+ );
+
+ browser.messageManager.loadFrameScript(
+ "chrome://extensions/content/ext-browser-content.js",
+ false,
+ true
+ );
+
+ let options =
+ panel.browserStyle !== false
+ ? { stylesheets: ExtensionParent.extensionStylesheets }
+ : {};
+ browser.messageManager.sendAsyncMessage("Extension:InitBrowser", options);
+ return browser;
+ });
+}
+
+// Stub tabbrowser implementation for use by the tab-modal alert code.
+var gBrowser = {
+ get selectedBrowser() {
+ return document.getElementById("webext-panels-browser");
+ },
+
+ getTabForBrowser(browser) {
+ return null;
+ },
+
+ getTabModalPromptBox(browser) {
+ if (!browser.tabModalPromptBox) {
+ browser.tabModalPromptBox = new TabModalPromptBox(browser);
+ }
+ return browser.tabModalPromptBox;
+ },
+};
+
+function updatePosition() {
+ // We need both of these to make sure we update the position
+ // after any lower level updates have finished.
+ requestAnimationFrame(() =>
+ setTimeout(() => {
+ let browser = document.getElementById("webext-panels-browser");
+ if (browser && browser.isRemoteBrowser) {
+ browser.frameLoader.requestUpdatePosition();
+ }
+ }, 0)
+ );
+}
+
+function loadPanel(extensionId, extensionUrl, browserStyle) {
+ let browserEl = document.getElementById("webext-panels-browser");
+ if (browserEl) {
+ if (browserEl.currentURI.spec === extensionUrl) {
+ return;
+ }
+ // Forces runtime disconnect. Remove the stack (parent).
+ browserEl.parentNode.remove();
+ }
+
+ let policy = WebExtensionPolicy.getByID(extensionId);
+
+ let sidebar = {
+ uri: extensionUrl,
+ extension: policy.extension,
+ browserStyle,
+ viewType: "sidebar",
+ };
+
+ getBrowser(sidebar).then(browser => {
+ let uri = Services.io.newURI(policy.getURL());
+ let triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ browser.loadURI(extensionUrl, { triggeringPrincipal });
+ });
+}
diff --git a/browser/base/content/webext-panels.xhtml b/browser/base/content/webext-panels.xhtml
new file mode 100644
index 0000000000..0a566783e5
--- /dev/null
+++ b/browser/base/content/webext-panels.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/usercontext/usercontext.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+%browserDTD;
+<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
+%textcontextDTD;
+]>
+
+<window id="webextpanels-window"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://global/content/contentAreaUtils.js"/>
+ <script src="chrome://browser/content/browser.js"/>
+ <script src="chrome://browser/content/browser-places.js"/>
+ <script src="chrome://browser/content/webext-panels.js"/>
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://browser/content/utilityOverlay.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+
+ <linkset>
+ <html:link rel="localization" href="browser/branding/brandings.ftl"/>
+ <html:link rel="localization" href="toolkit/global/textActions.ftl"/>
+ <html:link rel="localization" href="browser/browserContext.ftl"/>
+ </linkset>
+
+ <commandset id="mainCommandset">
+ <command id="Browser:Back"
+ oncommand="getPanelBrowser().webNavigation.goBack();"
+ disabled="true"/>
+ <command id="Browser:Forward"
+ oncommand="getPanelBrowser().webNavigation.goForward();"
+ disabled="true"/>
+ <command id="Browser:Stop" oncommand="PanelBrowserStop();"/>
+ <command id="Browser:Reload" oncommand="PanelBrowserReload();"/>
+ </commandset>
+
+ <popupset id="mainPopupSet">
+ <tooltip id="aHTMLTooltip" page="true"/>
+
+ <panel is="autocomplete-richlistbox-popup"
+ type="autocomplete-richlistbox"
+ id="PopupAutoComplete"
+ noautofocus="true"
+ hidden="true"
+ overflowpadding="4"
+ norolluponanchor="true" />
+
+ <menupopup id="contentAreaContextMenu" pagemenu="start"
+ onpopupshowing="if (event.target != this)
+ return true;
+ gContextMenu = new nsContextMenu(this, event.shiftKey);
+ if (gContextMenu.shouldDisplay)
+ document.popupNode = this.triggerNode;
+ return gContextMenu.shouldDisplay;"
+ onpopuphiding="if (event.target != this)
+ return;
+ gContextMenu.hiding();
+ gContextMenu = null;">
+#include browser-context.inc
+ </menupopup>
+
+ <!-- for select dropdowns. The menupopup is what shows the list of options,
+ and the popuponly menulist makes things like the menuactive attributes
+ work correctly on the menupopup. ContentSelectDropdown expects the
+ popuponly menulist to be its immediate parent. -->
+ <menulist popuponly="true" id="ContentSelectDropdown" hidden="true">
+ <menupopup rolluponmousewheel="true"
+ activateontab="true" position="after_start"
+ level="parent"
+#ifdef XP_WIN
+ consumeoutsideclicks="false" ignorekeys="shortcuts"
+#endif
+ />
+ </menulist>
+ </popupset>
+</window>
diff --git a/browser/base/content/webrtcIndicator.js b/browser/base/content/webrtcIndicator.js
new file mode 100644
index 0000000000..ccde6436f4
--- /dev/null
+++ b/browser/base/content/webrtcIndicator.js
@@ -0,0 +1,682 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { webrtcUI } = ChromeUtils.import("resource:///modules/webrtcUI.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "MacOSWebRTCStatusbarIndicator",
+ "resource:///modules/webrtcUI.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "BrowserWindowTracker",
+ "resource:///modules/BrowserWindowTracker.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PluralForm",
+ "resource://gre/modules/PluralForm.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gScreenManager",
+ "@mozilla.org/gfx/screenmanager;1",
+ "nsIScreenManager"
+);
+
+/**
+ * Public function called by webrtcUI to update the indicator
+ * display when the active streams change.
+ */
+function updateIndicatorState() {
+ WebRTCIndicator.updateIndicatorState();
+}
+
+/**
+ * Public function called by webrtcUI to indicate that webrtcUI
+ * is about to close the indicator. This is so that we can differentiate
+ * between closes that are caused by webrtcUI, and closes that are
+ * caused by other reasons (like the user closing the window via the
+ * OS window controls somehow).
+ *
+ * If the window is closed without having called this method first, the
+ * indicator will ask webrtcUI to shutdown any remaining streams and then
+ * select and focus the most recent browser tab that a stream was shared
+ * with.
+ */
+function closingInternally() {
+ WebRTCIndicator.closingInternally();
+}
+
+/**
+ * Main control object for the WebRTC global indicator
+ */
+const WebRTCIndicator = {
+ init(event) {
+ addEventListener("load", this);
+ addEventListener("unload", this);
+
+ // If the user customizes the position of the indicator, we will
+ // not try to re-center it on the primary display after indicator
+ // state updates.
+ this.positionCustomized = false;
+
+ this.updatingIndicatorState = false;
+ this.loaded = false;
+ this.isClosingInternally = false;
+
+ this.statusBar = null;
+ this.statusBarMenus = new Set();
+
+ this.showGlobalMuteToggles = Services.prefs.getBoolPref(
+ "privacy.webrtc.globalMuteToggles",
+ false
+ );
+
+ this.hideGlobalIndicator = Services.prefs.getBoolPref(
+ "privacy.webrtc.hideGlobalIndicator",
+ false
+ );
+
+ if (this.hideGlobalIndicator) {
+ this.setVisibility(false);
+ }
+ },
+
+ /**
+ * Controls the visibility of the global indicator. Also sets the value of
+ * a "visible" attribute on the document element to "true" or "false".
+ *
+ * @param isVisible (boolean)
+ * Whether or not the global indicator should be visible.
+ */
+ setVisibility(isVisible) {
+ let baseWin = window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
+ baseWin.visibility = isVisible;
+ // AppWindow::GetVisibility _always_ returns true (see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=306245), so we'll set an
+ // attribute on the document to make it easier for tests to know that the
+ // indicator is not visible.
+ document.documentElement.setAttribute("visible", isVisible);
+ // This will hide the indicator from the Window menu on macOS when
+ // not visible.
+ document.documentElement.setAttribute("inwindowmenu", isVisible);
+ },
+
+ /**
+ * Exposed externally so that webrtcUI can alert the indicator to
+ * update itself when sharing states have changed.
+ */
+ updateIndicatorState() {
+ // It's possible that we were called externally before the indicator
+ // finished loading. If so, then bail out - we're going to call
+ // updateIndicatorState ourselves automatically once the load
+ // event fires.
+ if (!this.loaded) {
+ return;
+ }
+
+ // We've started to update the indicator state. We set this flag so
+ // that the MozUpdateWindowPos event handler doesn't interpret indicator
+ // state updates as window movement caused by the user.
+ this.updatingIndicatorState = true;
+
+ let showCameraIndicator = webrtcUI.showCameraIndicator;
+ let showMicrophoneIndicator = webrtcUI.showMicrophoneIndicator;
+ let showScreenSharingIndicator = webrtcUI.showScreenSharingIndicator;
+ if (this.statusBar) {
+ let statusMenus = new Map([
+ ["Camera", showCameraIndicator],
+ ["Microphone", showMicrophoneIndicator],
+ ["Screen", showScreenSharingIndicator],
+ ]);
+
+ for (let [name, shouldShow] of statusMenus) {
+ let menu = document.getElementById(`webRTC-sharing${name}-menu`);
+ if (shouldShow && !this.statusBarMenus.has(menu)) {
+ this.statusBar.addItem(menu);
+ this.statusBarMenus.add(menu);
+ } else if (!shouldShow && this.statusBarMenus.has(menu)) {
+ this.statusBar.removeItem(menu);
+ this.statusBarMenus.delete(menu);
+ }
+ }
+ }
+
+ if (!this.showGlobalMuteToggles && !webrtcUI.showScreenSharingIndicator) {
+ this.setVisibility(false);
+ } else if (!this.hideGlobalIndicator) {
+ this.setVisibility(true);
+ }
+
+ if (this.showGlobalMuteToggles) {
+ this.updateWindowAttr("sharingvideo", showCameraIndicator);
+ this.updateWindowAttr("sharingaudio", showMicrophoneIndicator);
+ }
+
+ let sharingScreen = showScreenSharingIndicator.startsWith("Screen");
+ this.updateWindowAttr("sharingscreen", sharingScreen);
+
+ // We don't currently support the browser-tab sharing case, so we don't
+ // check if the screen sharing indicator starts with "Browser".
+
+ // We special-case sharing a window, because we want to have a slightly
+ // different UI if we're sharing a browser window.
+ let sharingWindow = showScreenSharingIndicator.startsWith("Window");
+ this.updateWindowAttr("sharingwindow", sharingWindow);
+
+ if (sharingWindow) {
+ // Get the active window streams and see if any of them are "scary".
+ // If so, then we're sharing a browser window.
+ let activeStreams = webrtcUI.getActiveStreams(
+ false /* camera */,
+ false /* microphone */,
+ false /* screen */,
+ true /* window */
+ );
+ let hasBrowserWindow = activeStreams.some(stream => {
+ return stream.devices.some(device => device.scary);
+ });
+
+ this.updateWindowAttr("sharingbrowserwindow", hasBrowserWindow);
+ this.sharingBrowserWindow = hasBrowserWindow;
+ } else {
+ this.updateWindowAttr("sharingbrowserwindow");
+ this.sharingBrowserWindow = false;
+ }
+
+ // The label that's displayed when sharing a display followed a priority.
+ // The more "risky" we deem the display is for sharing, the higher priority.
+ // This gives us the following priorities, from highest to lowest.
+ //
+ // 1. Screen
+ // 2. Browser window
+ // 3. Other application window
+ // 4. Browser tab (unimplemented)
+ //
+ // The CSS for the indicator does the work of showing or hiding these labels
+ // for us, but we need to update the aria-labelledby attribute on the container
+ // of those labels to make it clearer for screenreaders which one the user cares
+ // about.
+ let displayShare = document.getElementById("display-share");
+ let labelledBy;
+ if (sharingScreen) {
+ labelledBy = "screen-share-info";
+ } else if (this.sharingBrowserWindow) {
+ labelledBy = "browser-window-share-info";
+ } else if (sharingWindow) {
+ labelledBy = "window-share-info";
+ }
+ displayShare.setAttribute("aria-labelledby", labelledBy);
+
+ if (window.windowState != window.STATE_MINIMIZED) {
+ // Resize and ensure the window position is correct
+ // (sizeToContent messes with our position).
+ let docElStyle = document.documentElement.style;
+ docElStyle.minWidth = docElStyle.maxWidth = "unset";
+ docElStyle.minHeight = docElStyle.maxHeight = "unset";
+ window.sizeToContent();
+
+ // On Linux GTK, the style of window we're using by default is resizable. We
+ // workaround this by setting explicit limits on the height and width of the
+ // window.
+ if (AppConstants.platform == "linux") {
+ let { width, height } = window.windowUtils.getBoundsWithoutFlushing(
+ document.documentElement
+ );
+
+ docElStyle.minWidth = docElStyle.maxWidth = `${width}px`;
+ docElStyle.minHeight = docElStyle.maxHeight = `${height}px`;
+ }
+
+ this.ensureOnScreen();
+
+ if (!this.positionCustomized) {
+ this.centerOnLatestBrowser();
+ }
+ }
+
+ this.updatingIndicatorState = false;
+ },
+
+ /**
+ * After the indicator has been updated, checks to see if it has expanded
+ * such that part of the indicator is now outside of the screen. If so,
+ * it then adjusts the position to put the entire indicator on screen.
+ */
+ ensureOnScreen() {
+ let desiredX = Math.max(window.screenX, screen.availLeft);
+ let maxX =
+ screen.availLeft +
+ screen.availWidth -
+ document.documentElement.clientWidth;
+ window.moveTo(Math.min(desiredX, maxX), window.screenY);
+ },
+
+ /**
+ * If the indicator is first being opened, we'll find the browser window
+ * associated with the most recent share, and pin the indicator to the
+ * very top of the content area.
+ */
+ centerOnLatestBrowser() {
+ let activeStreams = webrtcUI.getActiveStreams(
+ true /* camera */,
+ true /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+
+ if (!activeStreams.length) {
+ return;
+ }
+
+ let browser = activeStreams[activeStreams.length - 1].browser;
+ let browserWindow = browser.ownerGlobal;
+ let browserRect = browserWindow.windowUtils.getBoundsWithoutFlushing(
+ browser
+ );
+
+ // This should be called in initialize right after we've just called
+ // updateIndicatorState. Since updateIndicatorState uses
+ // window.sizeToContent, the layout information should be up to date,
+ // and so the numbers that we get without flushing should be sufficient.
+ let { width: windowWidth } = window.windowUtils.getBoundsWithoutFlushing(
+ document.documentElement
+ );
+
+ window.moveTo(
+ browserWindow.mozInnerScreenX +
+ browserRect.left +
+ (browserRect.width - windowWidth) / 2,
+ browserWindow.mozInnerScreenY + browserRect.top
+ );
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "load": {
+ this.onLoad();
+ break;
+ }
+ case "unload": {
+ this.onUnload();
+ break;
+ }
+ case "click": {
+ this.onClick(event);
+ break;
+ }
+ case "change": {
+ this.onChange(event);
+ break;
+ }
+ case "MozUpdateWindowPos": {
+ if (!this.updatingIndicatorState) {
+ // The window moved while not updating the indicator state,
+ // so the user probably moved it.
+ this.positionCustomized = true;
+ }
+ break;
+ }
+ case "sizemodechange": {
+ if (window.windowState != window.STATE_MINIMIZED) {
+ this.updateIndicatorState();
+ }
+ break;
+ }
+ case "popupshowing": {
+ this.onPopupShowing(event);
+ break;
+ }
+ case "popuphiding": {
+ this.onPopupHiding(event);
+ break;
+ }
+ case "command": {
+ this.onCommand(event);
+ break;
+ }
+ case "DOMWindowClose":
+ case "close": {
+ this.onClose(event);
+ break;
+ }
+ }
+ },
+
+ onLoad() {
+ this.loaded = true;
+
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "win") {
+ this.statusBar = Cc["@mozilla.org/widget/systemstatusbar;1"].getService(
+ Ci.nsISystemStatusBar
+ );
+ }
+
+ this.updateIndicatorState();
+
+ window.addEventListener("click", this);
+ window.addEventListener("change", this);
+ window.addEventListener("sizemodechange", this);
+
+ // There are two ways that the dialog can close - either via the
+ // .close() window method, or via the OS. We handle both of those
+ // cases here.
+ window.addEventListener("DOMWindowClose", this);
+ window.addEventListener("close", this);
+
+ if (this.statusBar) {
+ // We only want these events for the system status bar menus.
+ window.addEventListener("popupshowing", this);
+ window.addEventListener("popuphiding", this);
+ window.addEventListener("command", this);
+ }
+
+ window.windowRoot.addEventListener("MozUpdateWindowPos", this);
+
+ // Alert accessibility implementations stuff just changed. We only need to do
+ // this initially, because changes after this will automatically fire alert
+ // events if things change materially.
+ let ev = new CustomEvent("AlertActive", {
+ bubbles: true,
+ cancelable: true,
+ });
+ document.documentElement.dispatchEvent(ev);
+
+ this.loaded = true;
+ },
+
+ onClose(event) {
+ // This event is fired from when the indicator window tries to be closed.
+ // If we preventDefault() the event, we are able to cancel that close
+ // attempt.
+ //
+ // We want to do that if we're not showing the global mute toggles
+ // and we're still sharing a camera or a microphone so that we can
+ // keep the status bar indicators present (since those status bar
+ // indicators are bound to this window).
+ if (
+ !this.showGlobalMuteToggles &&
+ (webrtcUI.showCameraIndicator || webrtcUI.showMicrophoneIndicator)
+ ) {
+ event.preventDefault();
+ this.setVisibility(false);
+ }
+
+ if (!this.isClosingInternally) {
+ // Something has tried to close the indicator, but it wasn't webrtcUI.
+ // This means we might still have some streams being shared. To protect
+ // the user from unknowingly sharing streams, we shut those streams
+ // down.
+ //
+ // This only includes the camera and microphone streams if the user
+ // has the global mute toggles enabled, since these toggles visually
+ // associate the indicator with those streams.
+ let activeStreams = webrtcUI.getActiveStreams(
+ this.showGlobalMuteToggles /* camera */,
+ this.showGlobalMuteToggles /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+ webrtcUI.stopSharingStreams(
+ activeStreams,
+ this.showGlobalMuteToggles /* camera */,
+ this.showGlobalMuteToggles /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+ }
+ },
+
+ onUnload() {
+ Services.ppmm.sharedData.set("WebRTC:GlobalCameraMute", false);
+ Services.ppmm.sharedData.set("WebRTC:GlobalMicrophoneMute", false);
+ Services.ppmm.sharedData.flush();
+
+ if (this.statusBar) {
+ for (let menu of this.statusBarMenus) {
+ this.statusBar.removeItem(menu);
+ }
+ }
+ },
+
+ onClick(event) {
+ switch (event.target.id) {
+ case "stop-sharing": {
+ let activeStreams = webrtcUI.getActiveStreams(
+ false /* camera */,
+ false /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+
+ if (!activeStreams.length) {
+ return;
+ }
+
+ // getActiveStreams is filtering for streams that have screen
+ // sharing, but those streams might _also_ be sharing other
+ // devices like camera or microphone. This is why we need to
+ // tell stopSharingStreams explicitly which device type we want
+ // to stop.
+ webrtcUI.stopSharingStreams(
+ activeStreams,
+ false /* camera */,
+ false /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+ break;
+ }
+ case "minimize": {
+ window.minimize();
+ break;
+ }
+ }
+ },
+
+ onChange(event) {
+ switch (event.target.id) {
+ case "microphone-mute-toggle": {
+ this.toggleMicrophoneMute(event.target);
+ break;
+ }
+ case "camera-mute-toggle": {
+ this.toggleCameraMute(event.target);
+ break;
+ }
+ }
+ },
+
+ onPopupShowing(event) {
+ if (!this.eventIsForDeviceMenuPopup(event)) {
+ return;
+ }
+
+ let menupopup = event.target;
+ let type = menupopup.getAttribute("type");
+
+ // When the indicator is hidden by default, opening the menu from the
+ // system tray _might_ cause the indicator to try to become visible again.
+ // We work around this by re-hiding it if it wasn't already visible.
+ if (document.documentElement.getAttribute("visible") != "true") {
+ let baseWin = window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
+ baseWin.visibility = false;
+ }
+
+ let activeStreams;
+ if (type == "Camera") {
+ activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ } else if (type == "Microphone") {
+ activeStreams = webrtcUI.getActiveStreams(false, true, false);
+ } else if (type == "Screen") {
+ activeStreams = webrtcUI.getActiveStreams(false, false, true, true);
+ type = webrtcUI.showScreenSharingIndicator;
+ }
+
+ let bundle = Services.strings.createBundle(
+ "chrome://browser/locale/webrtcIndicator.properties"
+ );
+
+ if (!activeStreams.length) {
+ event.preventDefault();
+ return;
+ }
+
+ if (activeStreams.length == 1) {
+ let stream = activeStreams[0];
+
+ let menuitem = document.createXULElement("menuitem");
+ let labelId = `webrtcIndicator.sharing${type}With.menuitem`;
+ let label = stream.browser.contentTitle || stream.uri;
+ menuitem.setAttribute(
+ "label",
+ bundle.formatStringFromName(labelId, [label])
+ );
+ menuitem.setAttribute("disabled", "true");
+ menupopup.appendChild(menuitem);
+
+ menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute(
+ "label",
+ bundle.GetStringFromName("webrtcIndicator.controlSharing.menuitem")
+ );
+ menuitem.stream = stream;
+ menuitem.addEventListener("command", this);
+
+ menupopup.appendChild(menuitem);
+ return;
+ }
+
+ // We show a different menu when there are several active streams.
+ let menuitem = document.createXULElement("menuitem");
+ let labelId = `webrtcIndicator.sharing${type}WithNTabs.menuitem`;
+ let count = activeStreams.length;
+ let label = PluralForm.get(
+ count,
+ bundle.GetStringFromName(labelId)
+ ).replace("#1", count);
+ menuitem.setAttribute("label", label);
+ menuitem.setAttribute("disabled", "true");
+ menupopup.appendChild(menuitem);
+
+ for (let stream of activeStreams) {
+ let item = document.createXULElement("menuitem");
+ labelId = "webrtcIndicator.controlSharingOn.menuitem";
+ label = stream.browser.contentTitle || stream.uri;
+ item.setAttribute("label", bundle.formatStringFromName(labelId, [label]));
+ item.stream = stream;
+ item.addEventListener("command", this);
+ menupopup.appendChild(item);
+ }
+ },
+
+ onPopupHiding(event) {
+ if (!this.eventIsForDeviceMenuPopup(event)) {
+ return;
+ }
+
+ let menu = event.target;
+ while (menu.firstChild) {
+ menu.firstChild.remove();
+ }
+ },
+
+ onCommand(event) {
+ webrtcUI.showSharingDoorhanger(event.target.stream);
+ },
+
+ /**
+ * Returns true if an event was fired for one of the shared device
+ * menupopups.
+ *
+ * @param event (Event)
+ * The event to check.
+ * @returns True if the event was for one of the shared device
+ * menupopups.
+ */
+ eventIsForDeviceMenuPopup(event) {
+ let menupopup = event.target;
+ let type = menupopup.getAttribute("type");
+
+ return ["Camera", "Microphone", "Screen"].includes(type);
+ },
+
+ /**
+ * Mutes or unmutes the microphone globally based on the checked
+ * state of toggleEl. Also updates the tooltip of toggleEl once
+ * the state change is done.
+ *
+ * @param toggleEl (Element)
+ * The input[type="checkbox"] for toggling the microphone mute
+ * state.
+ */
+ toggleMicrophoneMute(toggleEl) {
+ Services.ppmm.sharedData.set(
+ "WebRTC:GlobalMicrophoneMute",
+ toggleEl.checked
+ );
+ Services.ppmm.sharedData.flush();
+ let l10nId =
+ "webrtc-microphone-" + (toggleEl.checked ? "muted" : "unmuted");
+ document.l10n.setAttributes(toggleEl, l10nId);
+ },
+
+ /**
+ * Mutes or unmutes the camera globally based on the checked
+ * state of toggleEl. Also updates the tooltip of toggleEl once
+ * the state change is done.
+ *
+ * @param toggleEl (Element)
+ * The input[type="checkbox"] for toggling the camera mute
+ * state.
+ */
+ toggleCameraMute(toggleEl) {
+ Services.ppmm.sharedData.set("WebRTC:GlobalCameraMute", toggleEl.checked);
+ Services.ppmm.sharedData.flush();
+ let l10nId = "webrtc-camera-" + (toggleEl.checked ? "muted" : "unmuted");
+ document.l10n.setAttributes(toggleEl, l10nId);
+ },
+
+ /**
+ * Updates an attribute on the <window> element.
+ *
+ * @param attr (String)
+ * The name of the attribute to update.
+ * @param value (String?)
+ * A string to set the attribute to. If the value is false-y,
+ * the attribute is removed.
+ */
+ updateWindowAttr(attr, value) {
+ let docEl = document.documentElement;
+ if (value) {
+ docEl.setAttribute(attr, "true");
+ } else {
+ docEl.removeAttribute(attr);
+ }
+ },
+
+ /**
+ * See the documentation on the script global closingInternally() function.
+ */
+ closingInternally() {
+ this.isClosingInternally = true;
+ },
+};
+
+WebRTCIndicator.init();
diff --git a/browser/base/content/webrtcIndicator.xhtml b/browser/base/content/webrtcIndicator.xhtml
new file mode 100644
index 0000000000..0d14150daa
--- /dev/null
+++ b/browser/base/content/webrtcIndicator.xhtml
@@ -0,0 +1,58 @@
+<?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://browser/skin/webRTC-indicator.css" type="text/css"?>
+
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="webrtcIndicator"
+ windowtype="Browser:WebRTCGlobalIndicator"
+ chromemargin="0,0,0,0">
+
+ <head>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="browser/webrtcIndicator.ftl"/>
+ <title data-l10n-id="webrtc-indicator-title"></title>
+ <script src="chrome://global/content/customElements.js"/>
+ <script src="chrome://browser/content/webrtcIndicator.js"></script>
+ </head>
+
+ <xul:menu id="webRTC-sharingCamera-menu" data-l10n-id="webrtc-camera-system-menu">
+ <xul:menupopup type="Camera">
+ </xul:menupopup>
+ </xul:menu>
+ <xul:menu id="webRTC-sharingMicrophone-menu" data-l10n-id="webrtc-microphone-system-menu">
+ <xul:menupopup type="Microphone">
+ </xul:menupopup>
+ </xul:menu>
+ <xul:menu id="webRTC-sharingScreen-menu" data-l10n-id="webrtc-screen-system-menu">
+ <xul:menupopup type="Screen">
+ </xul:menupopup>
+ </xul:menu>
+
+ <body role="alert">
+ <div id="drag-indicator" />
+ <div id="display-share" class="row-item" role="group" aria-labelledby="">
+ <image id="display-share-icon" />
+
+ <span id="window-share-info" data-l10n-id="webrtc-sharing-window"/>
+ <span id="browser-window-share-info" data-l10n-id="webrtc-sharing-browser-window"/>
+ <span id="screen-share-info" data-l10n-id="webrtc-sharing-screen"/>
+ <button id="stop-sharing" class="stop-button" data-l10n-id="webrtc-stop-sharing-button"/>
+ </div>
+ <div class="row-item separator" />
+ <div id="device-share" class="row-item">
+ <input type="checkbox" id="microphone-mute-toggle" class="control-icon" data-l10n-id="webrtc-microphone-unmuted"/>
+ <input type="checkbox" id="camera-mute-toggle" class="control-icon" data-l10n-id="webrtc-camera-unmuted"/>
+ </div>
+ <div class="row-item separator" />
+ <div id="window-controls" class="row-item">
+ <button id="minimize" class="control-icon" data-l10n-id="webrtc-minimize"/>
+ </div>
+ </body>
+</html>
diff --git a/browser/base/content/webrtcLegacyIndicator.js b/browser/base/content/webrtcLegacyIndicator.js
new file mode 100644
index 0000000000..2a359a8a4c
--- /dev/null
+++ b/browser/base/content/webrtcLegacyIndicator.js
@@ -0,0 +1,213 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { webrtcUI } = ChromeUtils.import("resource:///modules/webrtcUI.jsm");
+
+const BUNDLE_URL = "chrome://browser/locale/webrtcIndicator.properties";
+var gStringBundle;
+
+function init(event) {
+ gStringBundle = Services.strings.createBundle(BUNDLE_URL);
+
+ let brand = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+ let brandShortName = brand.GetStringFromName("brandShortName");
+ document.title = gStringBundle.formatStringFromName(
+ "webrtcIndicator.windowtitle",
+ [brandShortName]
+ );
+
+ for (let id of ["audioVideoButton", "screenSharePopup"]) {
+ let popup = document.getElementById(id);
+ popup.addEventListener("popupshowing", onPopupMenuShowing);
+ popup.addEventListener("popuphiding", onPopupMenuHiding);
+ popup.addEventListener("command", onPopupMenuCommand);
+ }
+
+ let fxButton = document.getElementById("firefoxButton");
+ fxButton.addEventListener("click", onFirefoxButtonClick);
+ fxButton.addEventListener("mousedown", PositionHandler);
+
+ updateIndicatorState();
+
+ // Alert accessibility implementations stuff just changed. We only need to do
+ // this initially, because changes after this will automatically fire alert
+ // events if things change materially.
+ let ev = new CustomEvent("AlertActive", { bubbles: true, cancelable: true });
+ document.documentElement.dispatchEvent(ev);
+}
+
+function updateIndicatorState() {
+ // If gStringBundle isn't set, the window hasn't finished loading.
+ if (!gStringBundle) {
+ return;
+ }
+
+ updateWindowAttr("sharingvideo", webrtcUI.showCameraIndicator);
+ updateWindowAttr("sharingaudio", webrtcUI.showMicrophoneIndicator);
+ updateWindowAttr("sharingscreen", webrtcUI.showScreenSharingIndicator);
+
+ // Camera and microphone button tooltip.
+ let shareTypes = [];
+ if (webrtcUI.showCameraIndicator) {
+ shareTypes.push("Camera");
+ }
+ if (webrtcUI.showMicrophoneIndicator) {
+ shareTypes.push("Microphone");
+ }
+
+ let audioVideoButton = document.getElementById("audioVideoButton");
+ if (shareTypes.length) {
+ let stringId =
+ "webrtcIndicator.sharing" + shareTypes.join("And") + ".tooltip";
+ audioVideoButton.setAttribute(
+ "tooltiptext",
+ gStringBundle.GetStringFromName(stringId)
+ );
+ } else {
+ audioVideoButton.removeAttribute("tooltiptext");
+ }
+
+ // Screen sharing button tooltip.
+ let screenShareButton = document.getElementById("screenShareButton");
+ if (webrtcUI.showScreenSharingIndicator) {
+ let stringId =
+ "webrtcIndicator.sharing" +
+ webrtcUI.showScreenSharingIndicator +
+ ".tooltip";
+ screenShareButton.setAttribute(
+ "tooltiptext",
+ gStringBundle.GetStringFromName(stringId)
+ );
+ } else {
+ screenShareButton.removeAttribute("tooltiptext");
+ }
+
+ // Resize and ensure the window position is correct
+ // (sizeToContent messes with our position).
+ window.sizeToContent();
+ PositionHandler.adjustPosition();
+}
+
+function updateWindowAttr(attr, value) {
+ let docEl = document.documentElement;
+ if (value) {
+ docEl.setAttribute(attr, "true");
+ } else {
+ docEl.removeAttribute(attr);
+ }
+}
+
+function onPopupMenuShowing(event) {
+ let popup = event.target;
+
+ let activeStreams;
+ if (popup.getAttribute("type") == "Devices") {
+ activeStreams = webrtcUI.getActiveStreams(true, true, false);
+ } else {
+ activeStreams = webrtcUI.getActiveStreams(false, false, true, true);
+ }
+ if (activeStreams.length) {
+ let index = activeStreams.length - 1;
+ webrtcUI.showSharingDoorhanger(activeStreams[index]);
+ event.preventDefault();
+ return;
+ }
+
+ for (let stream of activeStreams) {
+ let item = document.createElement("menuitem");
+ item.setAttribute("label", stream.browser.contentTitle || stream.uri);
+ item.setAttribute("tooltiptext", stream.uri);
+ item.stream = stream;
+ popup.appendChild(item);
+ }
+}
+
+function onPopupMenuHiding(event) {
+ let popup = event.target;
+ while (popup.firstChild) {
+ popup.firstChild.remove();
+ }
+}
+
+function onPopupMenuCommand(event) {
+ webrtcUI.showSharingDoorhanger(event.target.stream);
+}
+
+function onFirefoxButtonClick(event) {
+ event.target.blur();
+ let activeStreams = webrtcUI.getActiveStreams(true, true, true, true);
+ activeStreams[0].browser.ownerGlobal.focus();
+}
+
+var PositionHandler = {
+ positionCustomized: false,
+ threshold: 10,
+ adjustPosition() {
+ if (!this.positionCustomized) {
+ // Center the window horizontally on the screen (not the available area).
+ // Until we have moved the window to y=0, 'screen.width' may give a value
+ // for a secondary screen, so use values from the screen manager instead.
+ let primaryScreen = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
+ Ci.nsIScreenManager
+ ).primaryScreen;
+ let widthDevPix = {};
+ primaryScreen.GetRect({}, {}, widthDevPix, {});
+ let availTopDevPix = {};
+ primaryScreen.GetAvailRect({}, availTopDevPix, {}, {});
+ let scaleFactor = primaryScreen.defaultCSSScaleFactor;
+ let widthCss = widthDevPix.value / scaleFactor;
+ window.moveTo(
+ (widthCss - document.documentElement.clientWidth) / 2,
+ availTopDevPix.value / scaleFactor
+ );
+ } else {
+ // This will ensure we're at y=0.
+ this.setXPosition(window.screenX);
+ }
+ },
+ setXPosition(desiredX) {
+ // Ensure the indicator isn't moved outside the available area of the screen.
+ desiredX = Math.max(desiredX, screen.availLeft);
+ let maxX =
+ screen.availLeft +
+ screen.availWidth -
+ document.documentElement.clientWidth;
+ window.moveTo(Math.min(desiredX, maxX), screen.availTop);
+ },
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mousedown":
+ if (aEvent.button != 0 || aEvent.defaultPrevented) {
+ return;
+ }
+
+ this._startMouseX = aEvent.screenX;
+ this._startWindowX = window.screenX;
+ this._deltaX = this._startMouseX - this._startWindowX;
+
+ window.addEventListener("mousemove", this);
+ window.addEventListener("mouseup", this);
+ break;
+
+ case "mousemove":
+ let moveOffset = Math.abs(aEvent.screenX - this._startMouseX);
+ if (this._dragFullyStarted || moveOffset > this.threshold) {
+ this.setXPosition(aEvent.screenX - this._deltaX);
+ this._dragFullyStarted = true;
+ }
+ break;
+
+ case "mouseup":
+ this._dragFullyStarted = false;
+ window.removeEventListener("mousemove", this);
+ window.removeEventListener("mouseup", this);
+ this.positionCustomized =
+ Math.abs(this._startWindowX - window.screenX) >= this.threshold;
+ break;
+ }
+ },
+};
diff --git a/browser/base/content/webrtcLegacyIndicator.xhtml b/browser/base/content/webrtcLegacyIndicator.xhtml
new file mode 100644
index 0000000000..491b5d2c76
--- /dev/null
+++ b/browser/base/content/webrtcLegacyIndicator.xhtml
@@ -0,0 +1,35 @@
+<?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://browser/skin/webRTC-legacy-indicator.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="webrtcIndicator"
+ role="alert"
+ windowtype="Browser:WebRTCGlobalIndicator"
+ onload="init(event);"
+#ifdef XP_MACOSX
+ inwindowmenu="false"
+#endif
+ sizemode="normal"
+ hidechrome="true"
+ orient="horizontal"
+ >
+ <script src="chrome://browser/content/webrtcLegacyIndicator.js"/>
+
+ <button id="firefoxButton"/>
+ <button id="audioVideoButton" type="menu">
+ <menupopup id="audioVideoPopup" type="Devices"/>
+ </button>
+ <separator id="shareSeparator"/>
+ <button id="screenShareButton" type="menu">
+ <menupopup id="screenSharePopup" type="Screen"/>
+ </button>
+</window>