summaryrefslogtreecommitdiffstats
path: root/toolkit/content/tests/browser
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/content/tests/browser
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--toolkit/content/tests/browser/audio.oggbin0 -> 14290 bytes
-rw-r--r--toolkit/content/tests/browser/audio_file.txt1
-rw-r--r--toolkit/content/tests/browser/browser.ini149
-rw-r--r--toolkit/content/tests/browser/browser_about_logging.js464
-rw-r--r--toolkit/content/tests/browser/browser_about_networking.js47
-rw-r--r--toolkit/content/tests/browser/browser_autoscroll_disabled.js82
-rw-r--r--toolkit/content/tests/browser/browser_autoscroll_disabled_on_editable_content.js306
-rw-r--r--toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js125
-rw-r--r--toolkit/content/tests/browser/browser_bug1170531.js139
-rw-r--r--toolkit/content/tests/browser/browser_bug1198465.js76
-rw-r--r--toolkit/content/tests/browser/browser_bug1572798.js29
-rw-r--r--toolkit/content/tests/browser/browser_bug1693577.js49
-rw-r--r--toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js389
-rw-r--r--toolkit/content/tests/browser/browser_bug451286.js166
-rw-r--r--toolkit/content/tests/browser/browser_bug594509.js15
-rw-r--r--toolkit/content/tests/browser/browser_bug982298.js77
-rw-r--r--toolkit/content/tests/browser/browser_cancel_starting_autoscrolling_requested_by_background_tab.js156
-rw-r--r--toolkit/content/tests/browser/browser_charsetMenu_disable_on_ascii.js18
-rw-r--r--toolkit/content/tests/browser/browser_charsetMenu_swapBrowsers.js39
-rw-r--r--toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js577
-rw-r--r--toolkit/content/tests/browser/browser_contentTitle.js17
-rw-r--r--toolkit/content/tests/browser/browser_content_url_annotation.js78
-rw-r--r--toolkit/content/tests/browser/browser_crash_previous_frameloader.js131
-rw-r--r--toolkit/content/tests/browser/browser_default_audio_filename.js98
-rw-r--r--toolkit/content/tests/browser/browser_default_image_filename_redirect.js53
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_iframe.js91
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_navigation.js65
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_media.js145
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_media_pausedAfterPlay.js121
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_multipleMedia.js77
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_notInTreeAudio.js66
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_playAfterTabVisible.js68
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_playMediaInMuteTab.js97
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_silentAudioTrack_media.js63
-rw-r--r--toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js42
-rw-r--r--toolkit/content/tests/browser/browser_f7_caret_browsing.js367
-rw-r--r--toolkit/content/tests/browser/browser_findbar.js560
-rw-r--r--toolkit/content/tests/browser/browser_findbar_disabled_manual.js33
-rw-r--r--toolkit/content/tests/browser/browser_findbar_hiddenframes.js59
-rw-r--r--toolkit/content/tests/browser/browser_findbar_marks.js263
-rw-r--r--toolkit/content/tests/browser/browser_isSynthetic.js69
-rw-r--r--toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js129
-rw-r--r--toolkit/content/tests/browser/browser_label_textlink.js65
-rw-r--r--toolkit/content/tests/browser/browser_license_links.js27
-rw-r--r--toolkit/content/tests/browser/browser_mediaStreamPlayback.html24
-rw-r--r--toolkit/content/tests/browser/browser_mediaStreamPlaybackWithoutAudio.html17
-rw-r--r--toolkit/content/tests/browser/browser_media_wakelock.js159
-rw-r--r--toolkit/content/tests/browser/browser_media_wakelock_PIP.js155
-rw-r--r--toolkit/content/tests/browser/browser_media_wakelock_webaudio.js127
-rw-r--r--toolkit/content/tests/browser/browser_moz_support_link_open_links_in_chrome.js85
-rw-r--r--toolkit/content/tests/browser/browser_quickfind_editable.js59
-rw-r--r--toolkit/content/tests/browser/browser_remoteness_change_listeners.js39
-rw-r--r--toolkit/content/tests/browser/browser_resume_bkg_video_on_tab_hover.js154
-rw-r--r--toolkit/content/tests/browser/browser_richlistbox_keyboard.js81
-rw-r--r--toolkit/content/tests/browser/browser_saveImageURL.js76
-rw-r--r--toolkit/content/tests/browser/browser_save_folder_standalone_image.js81
-rw-r--r--toolkit/content/tests/browser/browser_save_resend_postdata.js169
-rw-r--r--toolkit/content/tests/browser/browser_starting_autoscroll_in_about_content.js74
-rw-r--r--toolkit/content/tests/browser/browser_suspend_videos_outside_viewport.js39
-rw-r--r--toolkit/content/tests/browser/common/mockTransfer.js85
-rw-r--r--toolkit/content/tests/browser/data/post_form_inner.sjs33
-rw-r--r--toolkit/content/tests/browser/data/post_form_outer.sjs36
-rw-r--r--toolkit/content/tests/browser/datetime/browser.ini74
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_blur.js265
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js369
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js56
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.js191
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js576
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js483
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js405
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js209
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js201
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js534
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_showPicker.js52
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js27
-rw-r--r--toolkit/content/tests/browser/datetime/browser_spinner.js180
-rw-r--r--toolkit/content/tests/browser/datetime/browser_spinner_keynav.js622
-rw-r--r--toolkit/content/tests/browser/datetime/head.js441
-rw-r--r--toolkit/content/tests/browser/doggy.pngbin0 -> 46876 bytes
-rw-r--r--toolkit/content/tests/browser/empty.pngbin0 -> 14528 bytes
-rw-r--r--toolkit/content/tests/browser/file_contentTitle.html14
-rw-r--r--toolkit/content/tests/browser/file_document_open_audio.html11
-rw-r--r--toolkit/content/tests/browser/file_empty.html8
-rw-r--r--toolkit/content/tests/browser/file_findinframe.html5
-rw-r--r--toolkit/content/tests/browser/file_iframe_media.html14
-rw-r--r--toolkit/content/tests/browser/file_mediaPlayback2.html14
-rw-r--r--toolkit/content/tests/browser/file_multipleAudio.html19
-rw-r--r--toolkit/content/tests/browser/file_multiplePlayingAudio.html23
-rw-r--r--toolkit/content/tests/browser/file_nonAutoplayAudio.html7
-rw-r--r--toolkit/content/tests/browser/file_outside_viewport_videos.html41
-rw-r--r--toolkit/content/tests/browser/file_redirect.html13
-rw-r--r--toolkit/content/tests/browser/file_redirect_to.html15
-rw-r--r--toolkit/content/tests/browser/file_silentAudioTrack.html18
-rw-r--r--toolkit/content/tests/browser/file_video.html9
-rw-r--r--toolkit/content/tests/browser/file_videoWithAudioOnly.html9
-rw-r--r--toolkit/content/tests/browser/file_videoWithoutAudioTrack.html9
-rw-r--r--toolkit/content/tests/browser/file_webAudio.html29
-rw-r--r--toolkit/content/tests/browser/firebird.pngbin0 -> 16179 bytes
-rw-r--r--toolkit/content/tests/browser/firebird.png^headers^2
-rw-r--r--toolkit/content/tests/browser/gizmo-noaudio.webmbin0 -> 112663 bytes
-rw-r--r--toolkit/content/tests/browser/gizmo.mp4bin0 -> 455255 bytes
-rw-r--r--toolkit/content/tests/browser/head.js244
-rw-r--r--toolkit/content/tests/browser/image.jpgbin0 -> 24204 bytes
-rw-r--r--toolkit/content/tests/browser/image_page.html9
-rw-r--r--toolkit/content/tests/browser/silentAudioTrack.webmbin0 -> 224800 bytes
105 files changed, 12379 insertions, 0 deletions
diff --git a/toolkit/content/tests/browser/audio.ogg b/toolkit/content/tests/browser/audio.ogg
new file mode 100644
index 0000000000..7f1833508a
--- /dev/null
+++ b/toolkit/content/tests/browser/audio.ogg
Binary files differ
diff --git a/toolkit/content/tests/browser/audio_file.txt b/toolkit/content/tests/browser/audio_file.txt
new file mode 100644
index 0000000000..2e4af84485
--- /dev/null
+++ b/toolkit/content/tests/browser/audio_file.txt
@@ -0,0 +1 @@
+data:audio/mpeg;base64,//tAxAAABvhjIgSxKQEFCi009CDgFQlHhw2YJxQXGTI1QVJ8WRkDAPBHKiG0yuPRcfEiOvayEXHyQnQLqJrMnjbEwTBMIN//8279Bv86QAAiCXbbgbotwRxCSFkvPxUgvmJ3iYWrqAoCs5buniYp/chXmj6wI6c0ILg/qAbENxV4RiApBAEHKD+U//1vkMWgAAAAXJJQ7CSK1lN3jhQIGtJsc/1mY13o/H2DFvCa0JLC4koaBRX/+0LEFAAIDElpp6TGwQaN7LWDFSAAAsKziRUMG0Cx+ZO03eBVJIS/r/6er/r1sAFAAAFtyCPInCApfqXtMafCH4l5Fws6rufUiy3yiYaLKcWLMKq9V7lbuMlIxZUkD72tRFvqJj3pNhu+h+j/fxSJIAAAA13i4R47FCkjw6iOeU4w7xSnggQI/gnyAAdDlwl1nf3n97uZvzrEglCsOSx+cz+bek4VOBxDVMr+xnuqyJApAAEpxtj/+0LEJAAIBG1lh5jJAQuLLfT2JOB+X+EE+bxPBYTSF7q9o1vZw7vGjRKI0Y9vCyA8lcPsdvfSq4nUmcFdATAwuoTD31EhITjEFy4gC79/662wGQAAm44x1TRwFXrpaMzgUkWiscPpglgQqnkQcIO1BsF4eYZhB9ImQfeQuAL18MtHMItQSYpptaRYHVK/17UUVJgAAABJyRiUWqCUx7PyXq4iDjYrwsbhUu6YgwjKohzhF2UAwNX/+0LEM4AIJENzrDzGwQeIbnT3mNgbYEDQBNsCYsfsOHaWAScIIROCc4OrKu5043XorYAAAACbcjGwHQyi9K43TKUqNLAaDqdkydra8PkT02zR1MqFidt+xldbdTutkmokxovBUiODVB0DChhYdFNFkm1P7EL5wgAAAJyS2BeANnooRJTfRqRLAEjxW02wzR3HuGxpXcX6TZNClIxQQqggFTwrOrvcHCCHsaLJKP72NBwRrYVe5///+0LEQwAIjF9xp6UpAQ6I7nT2JNjmKdIgAQAA5LLBXKpWsOvDcUSkizwwE0CdTJHWaauyBA8mcGEoETaBsLfsz0e0yeIZHs8jFc77f1VOzFVNNGQo7aqO3V/6v7GwAAAAW24w/EMgl0V0AfR7rpsLgwgRrUbyKCJhD4tZUBDyMLYrOzA729DKJOsYIlwUJj4wAt3eRaIEpRf/+a2J82AAAAMtt1FosIgV8ncVuRgT6NqhhhhJNvf/+0LEUAAIoMNxrBhLQQQM7fT2CSAy5LAbACMhIoJxLGYc8k6gOBcfila8BhBhyMFlViQkKgobrCwr/+dP11I0pXy72ToeA7iSDuIKtGklHAuaRLme5MHgIgPIZcDg4EAmi8jpFi9uF9+pkABYHxYqHh9O//6lFfrUAAAC7dtRMp7DyrdBD8xCLBzQ6P/MY7PyYUOCFHEHtbnip+g9mgeLlXhkOCxE21c+AAo0iZFTA46HhZs7qvb/+0LEXgAIXE13rCRnAOSKLQD3sODZcq6n/DopgAAABGX/8Rl0EEV2CuYAWUNfDLaqDFZ29QiFaINCRJfstMgA7mY7SpzOVC3ZMj/6yiHfUlMgLuJlLEq6zZ9DobFDYomGhgAAAAAKu34fnsIsqCCgDJY0OfbYkG7BvOyaziJgSmULsSCNV9v/6d5KpUzue6/7KKDntX///czor2Rv/v1faoyXo//9PrUAAAAATLaIdoIeJCZF72r/+0DEcQAIlD95rDDGwRmRbrz0iShVWuISCBERzVXmVBBAAw2bTOo1SwgxXWikcx7SMx3UrX/7xzE//xoADX4teiaLuYjRpAAAAkl20NbeqlKE12FO1N1HQ74MRwX8KB8wTrmR3yJVaM9rksd3LMq0/0Kv////op7G5cizTlZy1c6KOijRH+mv1YdkMAIAAgF7/iRMHQStYUxGWWMdwFziArfGT+crrIpN7TvFk9TircKdVnUSLP/7QsR8AAjU/3XnmEuBAZFuNPSJKPfYnsf//1YEwff2fUoVJmmjbmi54r3xyIhWRCAAAAKb/iblyeogNZlgiKryrKhAikqTbZ/3SzmGMiLgs4OWp2qnmqiHEwnBprQMDLzVyQygOo1sLVEq5SxSHdVnocvxBoYgIABASslDGHeLQG2ys5niY00HUNtOT/NT205Jmzg+V5SLDgDKI3FD4uBkKh1vAyASb0iQDGzQsGyBG7uv////lP/7QsSJgAiBB3usDEuRFBHuvPSNKK2wAAAAApHBcuqDHuxoQXwAXTewW95oai1hSrVtt6o8Orh+wV8vKCJ0UHdyE1MSOP9+If+P6jKQm0bgzb4HJ9mKD2p/e/7/bezhZAAADctg23vQdqPWlIG242YwgobqIp9AwhUwari28fdpfEg/mA7uAzUQ+5LN1ainiQfeUVF0YVF7PuIvuCsBCgNmDKjhAB0fr/nESQQSmU5gn2zNM8KRY//7QsSWAAjMWXPsYGcBDoyufPYJJPLYfMP7tpQqOutKsva1Wm26aAKqcNnI870+dkZ9UNXq0hBRQBF/9P0l2IhhmUeg8nrrJvYtXTzKOIAACRBgTkEvuqkMrpj8AAxMKkU+Wndjikqc2f/uVEzc5XzvQqtaadjOt93+X6OWoQeVHuVveqt2maqs/5lOQcgUCliUbZmnVBEiIBIceuwm2XkqjMeDnYxYbu+gFkUIkSuhEo6llUWHzP/7QsSiAAkEjWunvQlJK5XuNPOVaFeUOBw6SH+jxv78/zNqYprSj3pg0de1CVREReeDp1oWPWGKWXuUpiPv1fg1QQAAAAS3d+CKLeNIvTpCiZF0eMFB1EImoZQwMTAri7LrJM3vdlQXgVWr8Ln9iX+Oehn4O0fU0246plNhIFnIiRQOrkmqscr9qtoiAAAAG5ZQtsY9IKlgVJmCKxoAO0Qsj9RVlURLFEzZl4pStZMjhk0Wp/y2c//7QsSpgAi0mXusNElRIZ0ufZMJbM81+0uCzojepOsw0eXOJF2IDAssCoNjFo//2UdEAAAWvNTZLaojCgjqCEEbrowepo6vm5pM8ogjTLlSQn8uFEQpOS1ISBETd3P/ry+hVzU//dH3T4CEFahAFpk+cD8wt4frnOkUESEAwqbRfybHIRvBIkNQYNfiVX5q1fKz7rWa/z8+mu70BrjYaVmBz6aEPj/3fz3UWUgESOC7CL2ixoSOkf/7QsSzgAmwmW/sDGlhKhPsvPGNKDJa+/ArdPs1/8Cq29sa8iBkMIN15EeU8891iQ82889abDrEKnIphACguPLE9kFo7Sky8Jt1S2srdElD4IMgsnVEexo+j3B8PmSYkQvovu3rkiaTSTIBQyYXB6IGJBdLpKV2iVliMt2rgsi5S3GjxRb3sVccA8DQ0PNPUWa7lIXXRjVNaOZ6u2W7ZqkhypGsBMWFix1Y12VvIVMGXr/ts9ammv/7QMS4gAkYl2GnmGlBGR4sZJGNcACA4EWjYrNMogcaxHMOSm0EL9OuruosVWKV4BgYJTotwMSCcHRyA6YxMZMBlBFIslyRQOBseSOrSg9VfmOokWQ1kYmIh1RlFBQASVwfTC+q87J6RJ7VIbkzxezub6PuG2C4+s8UgDWi3WuHxZoSzpz/85eEzDHMj/BUyqm2CDCyAVVLwZpO33ePnB3m78Lv/lVDuG/p+viFYzVCMSAmjKK1//tCxMGACQzBYgSMy0kemu2gkKFoIYFkIzVCxx56guNZBpzGTuM3IUJxOxq328x0RURjjH1K+znvuSMEjmLVzlys6y3dyHcUhUUqNCUyvit/VfhTxq19v9Te/9IY/dSIeEVEEQBCaUwfMB3CsyQhIB9bDWzFpSFhG6bjY2VCXrt/6M5gZXeuiqk9Dl/fSSVE3VFT+ywbIBOs+KFC9+N874u8xfT4TL1dX6pzfm71/KiIZGMxERSy//tCxMqACaTDb4MNC0Efja50kY0gXjaKH+h5blWWpO3motURFA8k9rK2ZyHv90Oq/8+8WKk5M/dsj8/VSIlQbLypd9pkpNSLWkfNoxrP8GJJXRBsX5eup8Nd/75ibyWuNEgAAN/mnaeojh2QEBOOE1lwFg+KsfMGKriIPlpSdOex7tcw5E3+C6hp1YbSQ82Jdz8umR/pDx0PhPc7SqgxENH4blDXI8z8oXtfcSdq1f26kJAFZri2//tCxNEACnDBd+YYa0lRl668xBVpMpQuZm4zpPTak0GAyH5eM1jryx85tQzjcDoMqb8pUstnjlHZabmPqYOTER0r/9vKvKEfTcF7fTbCP/+HWp5LTGF3DJDHL/uCMwABEACKKUYrkOq3lgWmMKhbwbfCxS8UTmgIWBuztFM8u7fUATFDvYRFnJDZH+l1Ciq9UZin/kU/0GUKXspfQhB83llrfKGsQvWsvpViY/+dl9n5MCEBIQAl//tCxM4ACgDBd+YYS0lIHK789Y1pJN2h3ixyuJ/VIeB/Q5RpAy48WPHB0gOVnrQhLH43cw4Ell0C6aKxm85Fe93esyzpeHG+o8su5grl1HrD1f9Po9u5Y62p7eN2KBzdK/vIEAAABpyW0S4h2xZRxMGUlwrI+mYHpIf2oUTLj4+Y4NvnZz97ze0zrW6p98EYsZbX3h3Bi9KytiwVb/uxGHgkRAF8/5FeFUuvt/7t7v1BiAAAAAht//tCxM4ACg0XZ4Yga4E6kG1xhI0luNsS4Y3CIaKxxtQ2Xt1sNEhTBIod5zmaVD649jNn3uSt9YlMMDe2NRodhU1IloegnRFYiRGMz73fO/zZ4DFzQQbDxl+tvXajgAAAAAC1xKkZU8FO00IBEUj9CA10RigRJQpQ3Sjyg+y90GaGhnWUQ7nxOJwdGjSTQIOBwBKPRxMeRQqLBZ7TKEvBsWz6yZb1CCI2KdzTNP9sZAAAAm47ddg5//tCxM+ACpDRaeyMa2lOBq29l5id4WIxWMKhhhLWkhv4/sThCVXfctpfKcvTxzD3I6htAmGTlQp5fP6d0nxM8sqaxqZ+xIRd1GokBgsoAEJ0LD0KMoECzLYpb03X67UxAAIgJxOS7CNkmVKrM4caV1nEMYY8Dr8yUXh01UqdXU7oUvgvhEaz45uSsGzv//mGki5w5y5U3/+Lu/YEwQW0WMHIoHwAyMA7g6F9nX12aGYAZCTqkt3F//tAxMyACcRba6w8xwk7mi49hY1sUq0ddKPSJY9LTCoOrjc+diJurl84m/tA8ECNMQAf8aDf7OmAVm22TRg0Y4gRsvutHUHSyoRicx8xZ3X1b673etd99P/T7frpEAEAEhNy0YKVET5EVndNQClgPjEVsJ/2XcTwcbjUZM39fyIi/8kcp/djhNFM8uHOoOY06LiUOCVh6xIWWYEVS1NFzNy2DAqsH7os3W3TV6MkAAEgEtotyhT/+0LEzoAJ/DtjjOGGwUyaLv2EDWzIQxUromlZUUFqWMbk6cDKHqTmO8B6t0YqJ0Y7aqTsERekp2zLMiknSzf/TLOvzzT/Hz7yZ81BqhEef/EehkUVjDdheftr6qCgAAACSU5BDQoVESlLzyseNIpdZOhLeAa2h3hvOe55yqaJlaSTM2dLzLN6dGOCxpQaqp5E0FG9EjZ/IgFMseBU2xzGIQGqEas6wVuWpHf1UYiKoARCSbcu/DP/+0LEzgAJ7Mt17KxrYUKKbv2TDOWwZORFwUAus0jOc589CmOL3UrpuJhGdXbyv3/voZ5GZoK/OFNfil1Py7lZPnVyqzMFXIz4aGqqKweADQCDdhAaVECRE0eAj/Qf6oElIAAQBNJ2SjRVcr+PJoL9Erm6Dwl8LC1BwVmC78QUpCnVCKT8ibUxJykyOfDlpm+W+tly7w9Y22Xn2wmOyxGOxvPPsztUc4bFy/VFCrPjewl6iAAAAAT/+0LEzwAKAI1prDxpQUQgLf2ijXSnJBHkJ7CmSv02lMLth0WiVAIwk8FKSN33Uzppy5H6lDBpxIwGT0F14o/T6oSmwagJRtAMioaKoAJV0J0lqmPEVa7yP1qFV0aEO+vNIAAEjV3hjAQRMaotKn4HPgwa2KnAPCwUJ771nPzlnMmc1/+pavVCMZu53+tm9NujsZ/Vf1vUq+ullU3W7YYwaoLVUdurNIAAAgt1hNxBhvVTpNGcZGX/+0LEz4AKHI9lrDBpQVCdLr2ijWwyCIwQlygKy/Oc3Ne6k8xFd3Zv12RDPCXr1qK1sP3B2tKgEIVXDGEoRAQtofkm76B5A8tTbXoi6O2iAASC1+KoDjyqG23ZDjJGHReIunJm5CojEQ/OJq2367tT/Xx5XkdzqzvX9hQ58yLIqdEz3b1R+eTvumR1VDWRiHHd1EgMzMBBr51B4xaYFwvrkABuGwhmIAACRLSbcBtSly6in9IcLzj/+0LEzgAKUN1v7CRrYUIKLDWMmOARLKrB2oIWErf7faLXC8KSUf9i1nI44odAbtqRO/Z2Vy1kTd8lWQ9XnITKrPmEHIUWQdZ/P/+u9Ve98Ct9WxyRGpO7l2QQAAAlHLbaJdGCEiLcAoIITL7rz4TRGmsZRgijnt7P3vr3UPLKCRkHDhkbvXyDwYeEcEWuhUdw7GaU4ot4UyiU6V1+6W+Z/dgDCQIBgEKHt5vpR+6dCAAAjSScu4n/+0LEzYAIuRV3h6hLsSUOrnD1iSZdRTIJ3rrBLDPxVaTR9cdiQpHZ3XYhhUR8/RJu5GzmOF9+F40PuRZQj3kMmButO8v3+0tjNKZMHB85cPIiQQJPA8jht6OxGFCiuqqZVAEBAAABUv4geWL3NyDFqtOLKUlC6sKxEJPoBB9cMTap7zu7e1X+EogX7F1DlXeF2Ry7leMjVz7CL/T06VO9JX9czzypjmpQw9EV+n1iIAABACTlEif/+0DE1wAK3OtxjDxLcVidLv2ECW9FBAdxMlazBBcO/ZFwbeY11egRCyhNpr1BSfEbNo0wd5j/+1d0CuGjzerZONYCYWGvQh6H/X9BrtQ0c+iIQxIhAAQBbEzi5ixDLfFptl4wAIa+AW6FiYRCW6o+PjGInIMacPn/HiG/PnEtXan9+PY4iBPvTvse7413ruFNUivWVFQ4i3h+3vLWeeZLhZVwvTTHmL/tkokAEEp3ARgaNgiBTP/7QsTRAAqo43HsJGthUxkuvZMNbCLOH2xQGY+G6p1AKqlCdIKvssdlgnfD7PtnAcdOXXUTLRnq//pqJiu/7+//9vF5/Ubv8v/ocGBPQ2Vroxt3hJqKowj2i9Om5oc0IAEhAElOXBFptEhjcaJFL3VNskxriBfo0SPtwn3gFN8neb8jnOTvIFKt//9VIVDuJTpmleX9znGSaZSy3/+5jP8oOG6rP7/6ue9RTvv1ar/aiCSACSU7g//7QsTNAAmw52/smGtBFhMutYOJKm68pQG2c6jUULe4Y03dVN8veX3AOB9D1ol/f85RvV18W8E25tP/ZFQrEKlVZn/+10JM+8/b9UdqTnZ19mVqXcQI6PtYuiyX64tK+qIKIABKTlDE1Kk+DrQFYElWLOpuVjSnoMKIOtQWIwikhqdP1KqT+pSo53Zv/mRQjjBRQ5kr70WBgYBiTQG9zmXeS9G3xF0/YwAiAAQCk4IHgskcMSmKMP/7QsTUgArU9XfNnKu5WZOutbQVKwYrL67ObmIHEyZNicapATTeSxhtyQxnX63VR0b/9idKf9//9+yxSfhiUeNTP/jUyIo51TwUCw5g2Q6m/16bqZRDEBIREFEsJIgKAwyGnHGhtqwaAZ8MFJBKhxoegNCEIRZCzmT9r2tlhxwpwkfPIoa0MBsTXCTYxSVDXAJVz1Ig88eZ3MHd4ZHKbzaFIaj0kAAAAUU3cAbZAiVHIiyIOEKhdP/7QsTPAAo4nXntIElZSKRu9YQJs4syrMSCapAAkUlpkT1iZG926nkIqnuzmQrNf/sRgdEo8g6X1f+u/Fihnuv++/Zu6eVGmfxn+rIgUVJUT45LZrCEMgAAAQBJKLgIhkDErXRUXlq+pS2LX2+Y3bZDIUauORluFRQ61+gyf/y8Jh4N+U7+OaBfluBPoneL8UKKZPP/1OL+Lf8b0794d081J8erzqXGhvjQAAABSblwUXC6E8mNxP/7QsTOAAj8oXetDElRPJtt9ZSNaqJP++0hnohPFU9Uw0JhCVezoi7MHP8//zpqfdX2TZrCO87/N3Uim5sVL/n+cUWAjgfagUNHSAeJ6VGCxXOY/hJ5m91HgzIBAAAAm5LuEjxmMDNlk0YeZkWUksyteIfoKfI4ZOyU//OFfzVTkOPimVf9s4X01GM2W8VebVya1CcyPCs1Lsbdzv/uXpzbD9d/myRga6/nX73f5EgAAAElNygY4//7QsTTgAnkc3nMDEkxT5UudPGJa9K2QHKQZj1KMlrbSEl47rhSadKmi8ICrdv/Q69+9leQZT203/ZTGnRrHMQp0IcYDAgAZw40+NHLSgmQcnf/yjHBZ23rJ4QzAQEAAJJuXAODjCoKLDANGlMWKjxdDOZFW3hWm/uOJ4YywSIFFf8kDKpn/r05EDEIjtCbCJg2LH2DTY1oSrdGLpC4q6ZF2BEUMfruKiO56eR/sQBIABWrE5hH0v/7QMTTAApkQ3vsMGb5S5ku9YQNag0FiU7F1L4j77aywMTKhyYcS6HFOjlL53V1+lG62M1f0f2VnWrIpUZFKDPGDyg28mWaGVhMy08GEjB6/Ziqzh56G+mtAAAAAIpNwOuBStzA+k7m7rH9kNTC/D/cSjmMFg+IONFw6EKW2iFsh/1JwP1xQFAEpf/9D5ujjVZ5fel/J/lmZRWhDPqOId0CKDLI7/vf/o/0SBCIAKTjmDNweRDu//tCxNCACoTLe+wMa1k7mC408wlqJQY0oawhnMZ0DTyOaC6FDUY2HhFxZZE4obuqdOzEUQensOFg6FhijCwGpxoNIzQoKg6MKqKIGmw1p/BgySekWi72yxCIEFEAAEttwSYDGdgWy4bYUDYzNWNWUlu6bEqq1SFMGV4IBGeZq/4pcj+77LzBC5l17mPUg53clz3bPfahvRVLTKIYwUEwIpQwianZp6I9DrndYt9RAJIABSTcoauC//tCxNAAClR3d+2YaRE1lq8xkYluJ1OmQwifWbZqxXHGnWYux8M4lbhQTOX5VpSzTdJVdDqi1Xv9plU2397XsmyJWsxymszzsqwoOd/+zzm6sfQom3RvJ8rZ+iWmRBEzMCJbduwgYq8TDTMbo/iEuCOzmVsi2gMzxanXQ80TKSKVKZfl03IwlQGhzymc18zmX/7efnXW5SmiyFo5JGY9zQjlLMzKzhfkmM27es/antRgyIREQACi//tCxNEACijtbawga1FDDq51gZUik3KJeQ1pSstiz0O1Dl+gGa7a7+r9RlWRH3LZis5s88nI3N1goX6nGHzdHYn0tEl4O9UFlCSK55xqFH+yi0pS3yNAfH+ZyRf92pph+vqFMhMiABCBTbgaeIWJcOs5cUWbCcX9JBbnzt1MzVeNd/aHiaWhG9g534rT9mngyyPwcwSNOd7nSLzq5ZR4G1auYTH+j+tx6S/rN3tr78t7//WaAIQA//tCxNEACnzva6wkS1FEni21kYlzBCcloXYvS9wCcqs8l6eAxza+OBfvr5h7S9X7h715zKSdcl2a6uocquba1zVKz3a8tiaM+IcisrMChyi+hRsDkEGmJGNFNCUps9fQAAAAVhFOZfECm35JE7pPI+PDvqmtR4O4SongDvIx8V3QVTHrH2pnUu/M51ZohVr+thcWYfcPLjS4BGImBVwXVhlyP7Sj7v/1xNlFEAEppuCXl00PHPFk//tCxM+ACokXbewMa6FPDqz9kw0lJtQ3OXqwFGP88+tj7IwCuEDhb5Ekocjh38qdP1QMRfC/fIjVYoWjQhWUcUXnHzCUI6iN36MalbXLT5fNoptAApNyULrOKQkWZsHDvs326W9S5DXKQUoReUYUUdnZ77lICn2zU88yv5uR3/J5HfciCBjWKiyo6Naafwqfci/93M/c0SXO47Rq9boeUppgRmV0PGp+pE22QClJLg7x5ytqAVFl//tCxMyAChyLZ+wYaWk4l6x08wloG0+In9NSWe5ZLktoO8iLczpc+MvPn1Jn/k3l/6IqlAYNJDZlf3y3OKU5euX1s62f/zVJc6dQ98jPJP4RdzYGR2OCxTesp60w00QAUm5A1IQEQTrFSZg+Py6rlVqmkS1SyEtTkB6D4ofCjVqeyUGpX++s/nAddH7x2bp/mZGWsspoY4lMjEdYlNPG3sEJBSQYNpAL3sRv5D1/rciTQAKbktB///tAxM4ACSRvWye9iQEnk621gw0qApR4r5cSlUSeOdD96esLRLnPwgtSPZT5uxxq/ovs7gnW7KzFYp0tNR6B36zq8xrTUfo9ciKYOgdl18a+Ut4Hm81ryZE4663//IVVQ0UxABNNOBdRyjIBk/XGUQCBMhSa7iSViQIACACh3+b/tr/r5NVUmS/CvDW+3ze4fhTEkJW+8T+xub0H8VBzNntD9/m9n2V7Ot7uS4Zp4M1QiMQABKX/+0LE1QAK8QNxrJRrkVYmLrWBjbPbgt4dB0rCnIhkZFMeVsbUckiVgzk4SJhtZaTzT5VVsUhnf7OV0s61f3pMSqtE2siO6TuRlN3bb1WzFK4NRrSiFMPoWBE07lEvf0f1xNtABNyW4FyOAUYuRbzhNB7CqLQjXuSmXsEZN2T6uZHfrO5HZ51o+n3UoImNB5yyPX6Ne/lxqu+4TTFnk9/X/ugWxn/5fxw537a7Ocp3CW0AACnLsGr/+0LEz4AKYLVtrCBrUUgdLnTxiWuMxbhLG1KhXbvkndHoD+W3t8WmAHMoDDawsulaDIImrxUFWPMgE5aAlgU4hD3iMJWCwDkHdrVZkyPPAMxc4BjK20sUszufr/bG2yAX3+CHK4fjOziEBKDriPI2pPBoUZGRG09aVl18vpGpTOtGIyyr9/XLf/h2d79nvmtTcksXJlzimCBUWHvcLGR5JDWCIqyixvcLfGkUUCrNUERh9ze0j9v/+0LEzgAKCC197KRkuUscrTz0iWhwiDJ7drPKncpFZRz4pRSPP76/5fTUv9Ksv3y7paeXPvXUj+XJd/1S7xb6q2VZUDg1GObuPh4T6/LEpO4Rz//9I5ZAASIAAJJNuAww/WVVJkIQ3lM0+aHuJvEO7JaTuKul2PI6QIymMsuGmdXdo8rf3pkyGRWeS91m9PN53+6GTfZ0oSqX/w5PKzzuao/kwJTSbLVPvK8MggAiAAEWonA2cRr/+0LEzYAJxJN3p4xJWT+IrLWDJNhUFj7IX2gZjsItWOhkIndFg4eys5pCLWww4a68N67N+TRikKuZSQs4brYuZ8VjX0ZVCLKd+Zv7qNdrtfMHudRqfbf5/21uf9PGmLmwAQAQU5LQhS6N8A9UI3RFQzVC6fyln7ZL2FDkimuFHvzkTIe5OOn2/c9L5sRoptneqxn3rMubsVPOrkuGxKNOLypJMwug8VZUztmtG51or+1sAEAAFNz/+0LEz4AJqO17h4RrcTsdrjGBjW9wNKfF4P8wVSJ6MUW5WvdbnjyPpAyQHwfQ3T7OcPNNOKKtJkGmUPIhwcfDUOmRI0OpeutEmkkdKOWJ1NCRIsLu2ev6c1P36oZDAREAAANtyAQIWA5SbMonyFqpPquI+Hz1DAAXbKKRZqlEHaUOHlkXVdXMy0qY7pNdSqjnV5St2Raz6shtzaHaxaf3T5eYW7kEBKRsdjLxZFAexsAAAABa4Qb/+0LE0oAKnSVh54xtoViYK/2BjW0XxKnM7EcSkuhbYr5qHZJsUPoqxC1SYUVYxnQzAixNrZ5SOahnqFc9QgJwm5702D3jFETkuAUFsAOWpSikSoF44XDNAy/6Eem2MAEACL/glx8BllxygCfKYFoMT1RaN5omP04poiK9I8tCKNT6VZifKA7w5i6a7nENsfpLi44sciqGd3qj3HgjZ1UgMDu/Xf7b/1/Pv/8uyKbBBCSckBJmopH/+0DEzgAKKMFbp5hrQTcIqzT3oNiEuRO0gT2PDRHBKEUSCPUtkc0I8vaF9XjfQxrNcEcK8r6Q/QOSe9NXe/s+T9Fuymqr83Kdaz7XFXNEnz342p9f0+dr+vAD8bccmLw+VaqcNaeYI6FokNIqRUHSeoXbTeKOzCJl7HqzFIWzUudQ+lctVYFfI2UURy9t/uXZJ2Z6njB0cLh8nRXMIC1mWVS1irnykaIIAAAqqANMtpKzQQBPDv/7QsTPAAplC13njEuhP4/p8PSNIGN6FDjWeIsNhAEtUFuGqqcesdvPUWRZs3mdlYzHJLJkq0E2a/jykWbdpPATyy5YoeCbDAwyKQyRqldL0h70xGt96SFxUMpCpsKgiDYGwlKmVta1xpcuSeZNBgESr0luSjMyxxJJQdBXEoKhosdLA0eEwNCUFfyoaq9R7ES3Q7EQNPlgaPYKuTywdaCoKgr/qadni0SIMCljpQugNkwpEQyHhv/7QsTOgAnIRU+HrMbJPo4qNPGNIcQCsSCUVCEaD4wRkBOaCQYSBCxBMgqpZ2dndnZ2uFpETjSizLQRJGnFlHmJpInGlFmWICaaQNb////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7QsTQgAms3USnmGtBRpfosPGNaP/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7QsTSAAmgWzIEsMcBKBZSQGSZaf///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////1RBR0hhdHMgSWRlYSAgICAgICAgICAgICAgICAgICAgIFNhbGVtICAgICAgICAgICAgICAgICAgICAgICAgIFNhYnJpbmEgVGhlIFRlZW5hZ2UgV2l0Y2ggICAgIDE5OTcgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA5 \ No newline at end of file
diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini
new file mode 100644
index 0000000000..9ef275efd3
--- /dev/null
+++ b/toolkit/content/tests/browser/browser.ini
@@ -0,0 +1,149 @@
+[DEFAULT]
+support-files =
+ audio.ogg
+ empty.png
+ file_contentTitle.html
+ file_empty.html
+ file_iframe_media.html
+ file_findinframe.html
+ file_mediaPlayback2.html
+ file_multipleAudio.html
+ file_multiplePlayingAudio.html
+ file_nonAutoplayAudio.html
+ file_redirect.html
+ file_redirect_to.html
+ file_silentAudioTrack.html
+ file_webAudio.html
+ gizmo.mp4
+ head.js
+ image.jpg
+ image_page.html
+ silentAudioTrack.webm
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+
+[browser_about_logging.js]
+skip-if =
+ tsan # Bug 1804081
+[browser_about_networking.js]
+skip-if = socketprocess_networking
+[browser_autoscroll_disabled.js]
+skip-if = true # Bug 1312652
+[browser_autoscroll_disabled_on_editable_content.js]
+[browser_autoscroll_disabled_on_links.js]
+[browser_bug1170531.js]
+skip-if =
+ os == "linux" && !debug && !ccov # Bug 1647973
+[browser_bug1198465.js]
+[browser_bug1572798.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+support-files = file_document_open_audio.html
+[browser_bug1693577.js]
+[browser_bug295977_autoscroll_overflow.js]
+skip-if =
+ ((debug || asan) && os == "win" && bits == 64)
+ os == 'linux' && bits == 64 # Bug 1710788
+[browser_bug451286.js]
+skip-if = true # bug 1399845 tracks re-enabling this test.
+[browser_bug594509.js]
+[browser_bug982298.js]
+[browser_cancel_starting_autoscrolling_requested_by_background_tab.js]
+[browser_charsetMenu_disable_on_ascii.js]
+[browser_charsetMenu_swapBrowsers.js]
+[browser_click_event_during_autoscrolling.js]
+[browser_contentTitle.js]
+[browser_content_url_annotation.js]
+skip-if = !crashreporter
+[browser_crash_previous_frameloader.js]
+run-if = crashreporter
+[browser_default_audio_filename.js]
+support-files = audio_file.txt
+[browser_default_image_filename.js]
+[browser_default_image_filename_redirect.js]
+support-files =
+ doggy.png
+ firebird.png
+ firebird.png^headers^
+[browser_delay_autoplay_cross_origin_iframe.js]
+tags = audiochannel
+[browser_delay_autoplay_cross_origin_navigation.js]
+tags = audiochannel
+[browser_delay_autoplay_media.js]
+tags = audiochannel
+skip-if =
+ (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+[browser_delay_autoplay_media_pausedAfterPlay.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+[browser_delay_autoplay_multipleMedia.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+[browser_delay_autoplay_notInTreeAudio.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+[browser_delay_autoplay_playAfterTabVisible.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+[browser_delay_autoplay_playMediaInMuteTab.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+[browser_delay_autoplay_silentAudioTrack_media.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") || (os == "mac") || (os == "linux" && !debug) # aarch64 due to 1536573 #Bug 1524746
+[browser_delay_autoplay_webAudio.js]
+tags = audiochannel
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+[browser_f7_caret_browsing.js]
+[browser_findbar.js]
+skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1614739
+[browser_findbar_disabled_manual.js]
+[browser_findbar_hiddenframes.js]
+[browser_findbar_marks.js]
+[browser_isSynthetic.js]
+[browser_keyevents_during_autoscrolling.js]
+[browser_label_textlink.js]
+https_first_disabled = true
+[browser_license_links.js]
+[browser_media_wakelock.js]
+support-files =
+ browser_mediaStreamPlayback.html
+ browser_mediaStreamPlaybackWithoutAudio.html
+ file_video.html
+ file_videoWithAudioOnly.html
+ file_videoWithoutAudioTrack.html
+ gizmo.mp4
+ gizmo-noaudio.webm
+skip-if =
+ (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+ apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+[browser_media_wakelock_PIP.js]
+support-files =
+ file_video.html
+ gizmo.mp4
+[browser_media_wakelock_webaudio.js]
+[browser_moz_support_link_open_links_in_chrome.js]
+[browser_quickfind_editable.js]
+skip-if = (verify && debug && (os == 'linux'))
+[browser_remoteness_change_listeners.js]
+[browser_resume_bkg_video_on_tab_hover.js]
+skip-if =
+ os == "win" && processor == "aarch64" # Bug 1536573
+ debug # Bug 1388959
+[browser_richlistbox_keyboard.js]
+[browser_saveImageURL.js]
+[browser_save_folder_standalone_image.js]
+support-files =
+ doggy.png
+[browser_save_resend_postdata.js]
+support-files =
+ common/mockTransfer.js
+ data/post_form_inner.sjs
+ data/post_form_outer.sjs
+skip-if = true # Bug ?????? - test directly manipulates content (gBrowser.contentDocument.getElementById("postForm").submit();)
+[browser_starting_autoscroll_in_about_content.js]
+[browser_suspend_videos_outside_viewport.js]
+support-files =
+ file_outside_viewport_videos.html
+ gizmo.mp4
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
diff --git a/toolkit/content/tests/browser/browser_about_logging.js b/toolkit/content/tests/browser/browser_about_logging.js
new file mode 100644
index 0000000000..f458b36e0d
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_about_logging.js
@@ -0,0 +1,464 @@
+const PAGE = "about:logging";
+
+function clearLoggingPrefs() {
+ for (let pref of Services.prefs.getBranch("logging.").getChildList("")) {
+ info(`Clearing: ${pref}`);
+ Services.prefs.clearUserPref("logging." + pref);
+ }
+}
+
+// Before running, save any MOZ_LOG environment variable that might be preset,
+// and restore them at the end of this test.
+add_setup(async function saveRestoreLogModules() {
+ let savedLogModules = Services.env.get("MOZ_LOG");
+ Services.env.set("MOZ_LOG", "");
+ registerCleanupFunction(() => {
+ clearLoggingPrefs();
+ info(" -- Restoring log modules: " + savedLogModules);
+ for (let pref of savedLogModules.split(",")) {
+ let [logModule, level] = pref.split(":");
+ Services.prefs.setIntPref("logging." + logModule, parseInt(level));
+ }
+ // Removing this line causes a sandboxxing error in nsTraceRefCnt.cpp (!).
+ Services.env.set("MOZ_LOG", savedLogModules);
+ });
+});
+
+// Test that some UI elements are disabled in some cirumstances.
+add_task(async function testElementsDisabled() {
+ // This test needs a MOZ_LOG env var set.
+ Services.env.set("MOZ_LOG", "example:4");
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let $ = content.document.querySelector.bind(content.document);
+ Assert.ok(
+ $("#set-log-modules-button").disabled,
+ "Because a MOZ_LOG env var is set by the harness, it should be impossible to set new log modules."
+ );
+ });
+ });
+ Services.env.set("MOZ_LOG", "");
+
+ await BrowserTestUtils.withNewTab(
+ PAGE + "?modules=example:5&output=profiler",
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let $ = content.document.querySelector.bind(content.document);
+ Assert.ok(
+ !$("#some-elements-unavailable").hidden,
+ "If a log modules are configured via URL params, a warning should be visible."
+ );
+ Assert.ok(
+ $("#set-log-modules-button").disabled,
+ "If a log modules are configured via URL params, some in-page elements should be disabled (button)."
+ );
+ Assert.ok(
+ $("#log-modules").disabled,
+ "If a log modules are configured via URL params, some in-page elements should be disabled (input)."
+ );
+ Assert.ok(
+ $("#logging-preset-dropdown").disabled,
+ "If a log modules are configured via URL params, some in-page elements should be disabled (dropdown)."
+ );
+ Assert.ok(
+ $("#radio-logging-profiler").disabled &&
+ $("#radio-logging-file").disabled,
+ "If the ouptut type is configured via URL param, the radio buttons should be disabled."
+ );
+ });
+ }
+ );
+ await BrowserTestUtils.withNewTab(
+ PAGE + "?preset=media-playback",
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let $ = content.document.querySelector.bind(content.document);
+ Assert.ok(
+ !$("#some-elements-unavailable").hidden,
+ "If a preset is selected via URL, a warning should be displayed."
+ );
+ Assert.ok(
+ $("#set-log-modules-button").disabled,
+ "If a preset is selected via URL, some in-page elements should be disabled (button)."
+ );
+ Assert.ok(
+ $("#log-modules").disabled,
+ "If a preset is selected via URL, some in-page elements should be disabled (input)."
+ );
+ Assert.ok(
+ $("#logging-preset-dropdown").disabled,
+ "If a preset is selected via URL, some in-page elements should be disabled (dropdown)."
+ );
+ });
+ }
+ );
+ clearLoggingPrefs();
+});
+
+// Test URL parameters
+const modulesInURL = "example:4,otherexample:5";
+const presetInURL = "media-playback";
+const threadsInURL = "example,otherexample";
+const profilerPresetInURL = "media";
+add_task(async function testURLParameters() {
+ await BrowserTestUtils.withNewTab(
+ PAGE + "?modules=" + modulesInURL,
+ async browser => {
+ await SpecialPowers.spawn(browser, [modulesInURL], async modulesInURL => {
+ let $ = content.document.querySelector.bind(content.document);
+ Assert.ok(
+ !$("#some-elements-unavailable").hidden,
+ "If modules are selected via URL, a warning should be displayed."
+ );
+ var inPageSorted = $("#current-log-modules")
+ .innerText.split(",")
+ .sort()
+ .join(",");
+ var inURLSorted = modulesInURL.split(",").sort().join(",");
+ Assert.equal(
+ inPageSorted,
+ inURLSorted,
+ "When selecting modules via URL params, the same modules are reflected in the page."
+ );
+ });
+ }
+ );
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE + "?preset=" + presetInURL,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [presetInURL], async presetInURL => {
+ let $ = content.document.querySelector.bind(content.document);
+ Assert.ok(
+ !$("#some-elements-unavailable").hidden,
+ "If a preset is selected via URL, a warning should be displayed."
+ );
+ var inPageSorted = $("#current-log-modules")
+ .innerText.split(",")
+ .sort()
+ .join(",");
+ var presetSorted = content
+ .presets()
+ [presetInURL].modules.split(",")
+ .sort()
+ .join(",");
+ Assert.equal(
+ inPageSorted,
+ presetSorted,
+ "When selecting a preset via URL params, the correct log modules are reflected in the page."
+ );
+ });
+ }
+ );
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE + "?profiler-preset=" + profilerPresetInURL,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [profilerPresetInURL], async inURL => {
+ let $ = content.document.querySelector.bind(content.document);
+ // Threads override doesn't have a UI element, the warning shouldn't
+ // be displayed.
+ Assert.ok(
+ $("#some-elements-unavailable").hidden,
+ "When overriding the profiler preset, no warning is displayed on the page."
+ );
+ var inSettings = content.settings().profilerPreset;
+ Assert.equal(
+ inSettings,
+ inURL,
+ "When overriding the profiler preset via URL param, the correct preset is set in the logging manager settings."
+ );
+ });
+ }
+ );
+ await BrowserTestUtils.withNewTab(PAGE + "?profilerstacks", async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let $ = content.document.querySelector.bind(content.document);
+ Assert.ok(
+ !$("#some-elements-unavailable").hidden,
+ "If the profiler stacks config is set via URL, a warning should be displayed."
+ );
+ Assert.ok(
+ $("#with-profiler-stacks-checkbox").disabled,
+ "If the profiler stacks config is set via URL, its checkbox should be disabled."
+ );
+
+ Assert.ok(
+ Services.prefs.getBoolPref("logging.config.profilerstacks"),
+ "The preference for profiler stacks is set initially, as a result of parsing the URL parameter"
+ );
+
+ $("#radio-logging-file").click();
+ $("#radio-logging-profiler").click();
+
+ Assert.ok(
+ $("#with-profiler-stacks-checkbox").disabled,
+ "If the profiler stacks config is set via URL, its checkbox should be disabled even after clicking around."
+ );
+ });
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE + "?invalid-param",
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [profilerPresetInURL], async inURL => {
+ let $ = content.document.querySelector.bind(content.document);
+ Assert.ok(
+ !$("#error").hidden,
+ "When an invalid URL param is passed in, the page displays a warning."
+ );
+ });
+ }
+ );
+ clearLoggingPrefs();
+});
+
+// Test various things related to presets: that it's populated correctly, that
+// setting presets work in terms of UI, but also that it sets the logging.*
+// prefs correctly.
+add_task(async function testAboutLoggingPresets() {
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let $ = content.document.querySelector.bind(content.document);
+ let presetsDropdown = $("#logging-preset-dropdown");
+ Assert.equal(
+ Object.keys(content.presets()).length,
+ presetsDropdown.childNodes.length,
+ "Presets populated."
+ );
+
+ Assert.equal(presetsDropdown.value, "networking");
+ $("#set-log-modules-button").click();
+ Assert.ok(
+ $("#no-log-modules").hidden && !$("#current-log-modules").hidden,
+ "When log modules are set, they are visible."
+ );
+ var lengthModuleListNetworking = $("#log-modules").value.length;
+ var lengthCurrentModuleListNetworking = $("#current-log-modules")
+ .innerText.length;
+ Assert.notEqual(
+ lengthModuleListNetworking,
+ 0,
+ "When setting a profiler preset, the module string is non-empty (input)."
+ );
+ Assert.notEqual(
+ lengthCurrentModuleListNetworking,
+ 0,
+ "When setting a profiler preset, the module string is non-empty (selected modules)."
+ );
+
+ // Change preset
+ presetsDropdown.value = "media-playback";
+ presetsDropdown.dispatchEvent(new content.Event("change"));
+
+ // Check the following after "onchange".
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => content.setTimeout(resolve, 0));
+
+ Assert.equal(
+ presetsDropdown.value,
+ "media-playback",
+ "Selecting another preset is reflected in the page"
+ );
+ $("#set-log-modules-button").click();
+ Assert.ok(
+ $("#no-log-modules").hidden && !$("#current-log-modules").hidden,
+ "When other log modules are set, they are still visible"
+ );
+ Assert.notEqual(
+ $("#log-modules").value.length,
+ 0,
+ "When setting a profiler preset, the module string is non-empty (input)."
+ );
+ Assert.notEqual(
+ $("#current-log-modules").innerText.length,
+ 0,
+ "When setting a profiler preset, the module string is non-empty (selected modules)."
+ );
+ Assert.notEqual(
+ $("#log-modules").value.length,
+ lengthModuleListNetworking,
+ "When setting another profiler preset, the module string changes (input)."
+ );
+ let currentLogModulesString = $("#current-log-modules").innerText;
+ Assert.notEqual(
+ currentLogModulesString.length,
+ lengthCurrentModuleListNetworking,
+
+ "When setting another profiler preset, the module string changes (selected modules)."
+ );
+
+ // After setting some log modules via the preset dropdown, verify
+ // that they have been reflected to logging.* preferences.
+ var activeLogModules = [];
+ let children = Services.prefs.getBranch("logging.").getChildList("");
+ for (let pref of children) {
+ if (pref.startsWith("config.")) {
+ continue;
+ }
+
+ try {
+ let value = Services.prefs.getIntPref(`logging.${pref}`);
+ activeLogModules.push(`${pref}:${value}`);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ let mod;
+ while ((mod = activeLogModules.pop())) {
+ Assert.ok(
+ currentLogModulesString.includes(mod),
+ `${mod} was effectively set`
+ );
+ }
+ });
+ });
+ clearLoggingPrefs();
+});
+
+// Test various things around the profiler stacks feature
+add_task(async function testProfilerStacks() {
+ // Check the initial state before changing anything.
+ Assert.ok(
+ !Services.prefs.getBoolPref("logging.config.profilerstacks", false),
+ "The preference for profiler stacks isn't set initially"
+ );
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let $ = content.document.querySelector.bind(content.document);
+ const checkbox = $("#with-profiler-stacks-checkbox");
+ Assert.ok(
+ !checkbox.checked,
+ "The profiler stacks checkbox isn't checked at load time."
+ );
+ checkbox.checked = true;
+ checkbox.dispatchEvent(new content.Event("change"));
+ Assert.ok(
+ Services.prefs.getBoolPref("logging.config.profilerstacks"),
+ "The preference for profiler stacks is now set to true"
+ );
+ checkbox.checked = false;
+ checkbox.dispatchEvent(new content.Event("change"));
+ Assert.ok(
+ !Services.prefs.getBoolPref("logging.config.profilerstacks"),
+ "The preference for profiler stacks is now back to false"
+ );
+
+ $("#radio-logging-file").click();
+ Assert.ok(
+ checkbox.disabled,
+ "The profiler stacks checkbox is disabled when the output type is 'file'"
+ );
+ $("#radio-logging-profiler").click();
+ Assert.ok(
+ !checkbox.disabled,
+ "The profiler stacks checkbox is enabled when the output type is 'profiler'"
+ );
+ });
+ });
+ clearLoggingPrefs();
+});
+
+// Here we test that starting and stopping log collection to the Firefox
+// Profiler opens a new tab. We don't actually check the content of the profile.
+add_task(async function testProfilerOpens() {
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ let profilerOpenedPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://example.com/",
+ false
+ );
+ SpecialPowers.spawn(browser, [], async savedLogModules => {
+ let $ = content.document.querySelector.bind(content.document);
+ // Override the URL the profiler uses to avoid hitting external
+ // resources (and crash).
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["devtools.performance.recording.ui-base-url", "https://example.com"],
+ ["devtools.performance.recording.ui-base-url-path", "/"],
+ ],
+ });
+ $("#radio-logging-file").click();
+ $("#radio-logging-profiler").click();
+ $("#logging-preset-dropdown").value = "networking";
+ $("#logging-preset-dropdown").dispatchEvent(new content.Event("change"));
+ $("#set-log-modules-button").click();
+ $("#toggle-logging-button").click();
+ // Wait for the profiler to start. This can be very slow.
+ await content.profilerPromise();
+
+ // Wait for some time for good measure while the profiler collects some
+ // data. We don't really care about the data itself.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => content.setTimeout(resolve, 1000));
+ $("#toggle-logging-button").click();
+ });
+ let tab = await profilerOpenedPromise;
+ Assert.ok(true, "Profiler tab opened after profiling");
+ await BrowserTestUtils.removeTab(tab);
+ });
+ clearLoggingPrefs();
+});
+
+// Same test, outputing to a file, with network logging, while opening and
+// closing a tab. We only check that the file exists and has a non-zero size.
+add_task(async function testLogFileFound() {
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ // Clear any previous log file.
+ let $ = content.document.querySelector.bind(content.document);
+ $("#radio-logging-file").click();
+ $("#log-file").value = "";
+ $("#log-file").dispatchEvent(new content.Event("change"));
+ $("#set-log-file-button").click();
+
+ Assert.ok(
+ !$("#no-log-file").hidden,
+ "When a log file hasn't been set, it's indicated as such."
+ );
+ });
+ });
+ await BrowserTestUtils.withNewTab(PAGE, async browser => {
+ let logPath = await SpecialPowers.spawn(browser, [], async () => {
+ let $ = content.document.querySelector.bind(content.document);
+ $("#radio-logging-file").click();
+ // Set the log file (use the default path)
+ $("#set-log-file-button").click();
+ var logPath = $("#current-log-file").innerText;
+ // Set log modules for networking
+ $("#logging-preset-dropdown").value = "networking";
+ $("#logging-preset-dropdown").dispatchEvent(new content.Event("change"));
+ $("#set-log-modules-button").click();
+ return logPath;
+ });
+
+ // No need to start or stop logging when logging to a file. Just open
+ // a tab, any URL will do. Wait for this tab to be loaded so we're sure
+ // something (anything) has happened in necko.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com",
+ true /* waitForLoad */
+ );
+ await BrowserTestUtils.removeTab(tab);
+ let logDirectory = PathUtils.parent(logPath);
+ let logBasename = PathUtils.filename(logPath);
+ let entries = await IOUtils.getChildren(logDirectory);
+ let foundNonEmptyLogFile = false;
+ for (let entry of entries) {
+ if (entry.includes(logBasename)) {
+ info("-- Log file found: " + entry);
+ let fileinfo = await IOUtils.stat(entry);
+ foundNonEmptyLogFile |= fileinfo.size > 0;
+ }
+ }
+ Assert.ok(foundNonEmptyLogFile, "Found at least one non-empty log file.");
+ });
+ clearLoggingPrefs();
+});
diff --git a/toolkit/content/tests/browser/browser_about_networking.js b/toolkit/content/tests/browser/browser_about_networking.js
new file mode 100644
index 0000000000..bab285904c
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_about_networking.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_first() {
+ registerCleanupFunction(() => {
+ // Must clear mode first, otherwise we'll have non-local connections to
+ // the cloudflare URL.
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "about:networking#dns",
+ async function (browser) {
+ ok(!browser.isRemoteBrowser, "Browser should not be remote.");
+ await ContentTask.spawn(browser, null, async function () {
+ let url_tbody = content.document.getElementById("dns_trr_url");
+ info(url_tbody);
+ is(
+ url_tbody.children[0].children[0].textContent,
+ "https://mozilla.cloudflare-dns.com/dns-query"
+ );
+ is(url_tbody.children[0].children[1].textContent, "0");
+ });
+ }
+ );
+
+ Services.prefs.setCharPref("network.trr.uri", "https://localhost/testytest");
+ Services.prefs.setIntPref("network.trr.mode", 2);
+ await BrowserTestUtils.withNewTab(
+ "about:networking#dns",
+ async function (browser) {
+ ok(!browser.isRemoteBrowser, "Browser should not be remote.");
+ await ContentTask.spawn(browser, null, async function () {
+ let url_tbody = content.document.getElementById("dns_trr_url");
+ info(url_tbody);
+ is(
+ url_tbody.children[0].children[0].textContent,
+ "https://localhost/testytest"
+ );
+ is(url_tbody.children[0].children[1].textContent, "2");
+ });
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_autoscroll_disabled.js b/toolkit/content/tests/browser/browser_autoscroll_disabled.js
new file mode 100644
index 0000000000..e69c319d17
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_autoscroll_disabled.js
@@ -0,0 +1,82 @@
+add_task(async function () {
+ const kPrefName_AutoScroll = "general.autoScroll";
+ Services.prefs.setBoolPref(kPrefName_AutoScroll, false);
+
+ let dataUri =
+ 'data:text/html,<html><body id="i" style="overflow-y: scroll"><div style="height: 2000px"></div>\
+ <iframe id="iframe" style="display: none;"></iframe>\
+</body></html>';
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser, dataUri);
+ await loadedPromise;
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#i",
+ 50,
+ 50,
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ var iframe = content.document.getElementById("iframe");
+
+ if (iframe) {
+ var e = new iframe.contentWindow.PageTransitionEvent("pagehide", {
+ bubbles: true,
+ cancelable: true,
+ persisted: false,
+ });
+ iframe.contentDocument.dispatchEvent(e);
+ iframe.contentDocument.documentElement.dispatchEvent(e);
+ }
+ });
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#i",
+ 100,
+ 100,
+ { type: "mousemove", clickCount: "0" },
+ gBrowser.selectedBrowser
+ );
+
+ // If scrolling didn't work, we wouldn't do any redraws and thus time out, so
+ // request and force redraws to get the chance to check for scrolling at all.
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+
+ let msg = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ // Skip the first animation frame callback as it's the same callback that
+ // the browser uses to kick off the scrolling.
+ return new Promise(resolve => {
+ function checkScroll() {
+ let msg = "";
+ let elem = content.document.getElementById("i");
+ if (elem.scrollTop != 0) {
+ msg += "element should not have scrolled vertically";
+ }
+ if (elem.scrollLeft != 0) {
+ msg += "element should not have scrolled horizontally";
+ }
+
+ resolve(msg);
+ }
+
+ content.requestAnimationFrame(checkScroll);
+ });
+ }
+ );
+
+ ok(!msg, "element scroll " + msg);
+
+ // restore the changed prefs
+ if (Services.prefs.prefHasUserValue(kPrefName_AutoScroll)) {
+ Services.prefs.clearUserPref(kPrefName_AutoScroll);
+ }
+
+ // wait for focus to fix a failure in the next test if the latter runs too soon.
+ await SimpleTest.promiseFocus();
+});
diff --git a/toolkit/content/tests/browser/browser_autoscroll_disabled_on_editable_content.js b/toolkit/content/tests/browser/browser_autoscroll_disabled_on_editable_content.js
new file mode 100644
index 0000000000..6e66644bc9
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_autoscroll_disabled_on_editable_content.js
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.paste", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", false],
+ ],
+ });
+
+ let autoScroller;
+ function onPopupShown(aEvent) {
+ if (aEvent.originalTarget.id != "autoscroller") {
+ return false;
+ }
+ autoScroller = aEvent.originalTarget;
+ return true;
+ }
+ window.addEventListener("popupshown", onPopupShown, { capture: true });
+ registerCleanupFunction(() => {
+ window.removeEventListener("popupshown", onPopupShown, { capture: true });
+ });
+ function popupIsNotClosed() {
+ return autoScroller && autoScroller.state != "closed";
+ }
+
+ async function promiseNativeMouseMiddleButtonDown(aBrowser) {
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: aBrowser,
+ atCenter: true,
+ });
+ return EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: aBrowser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ }
+ async function promiseNativeMouseMiddleButtonUp(aBrowser) {
+ return EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: aBrowser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ }
+ function promiseWaitForAutoScrollerClosed() {
+ if (!autoScroller || autoScroller.state == "closed") {
+ return Promise.resolve();
+ }
+ let result = BrowserTestUtils.waitForEvent(
+ autoScroller,
+ "popuphidden",
+ { capture: true },
+ () => {
+ return true;
+ }
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ return result;
+ }
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<div contenteditable style="height: 10000px;"></div>';
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await TestUtils.waitForCondition(
+ popupIsNotClosed,
+ "Wait for timeout of popup",
+ 100,
+ 10
+ );
+ ok(
+ false,
+ "Autoscroll shouldn't be started on editable <div> if middle paste is enabled"
+ );
+ } catch (e) {
+ ok(
+ typeof e == "string" && e.includes(" - timed out after 10 tries."),
+ `Autoscroll shouldn't be started on editable <div> if middle paste is enabled (${
+ typeof e == "string" ? e : e.message
+ })`
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<div style="height: 10000px;"></div>';
+ content.document.designMode = "on";
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await TestUtils.waitForCondition(
+ popupIsNotClosed,
+ "Wait for timeout of popup",
+ 100,
+ 10
+ );
+ ok(
+ false,
+ "Autoscroll shouldn't be started in document whose designMode is 'on' if middle paste is enabled"
+ );
+ } catch (e) {
+ ok(
+ typeof e == "string" && e.includes(" - timed out after 10 tries."),
+ `Autoscroll shouldn't be started in document whose designMode is 'on' if middle paste is enabled (${
+ typeof e == "string" ? e : e.message
+ })`
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<div contenteditable style="height: 10000px;"><div contenteditable="false" style="height: 10000px;"></div></div>';
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ { capture: true },
+ onPopupShown
+ );
+ ok(
+ true,
+ "Auto scroll should be started on non-editable <div> in an editing host if middle paste is enabled"
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["middlemouse.paste", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<div contenteditable style="height: 10000px;"></div>';
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ { capture: true },
+ onPopupShown
+ );
+ ok(
+ true,
+ "Auto scroll should be started on editable <div> if middle paste is disabled"
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<div style="height: 10000px;"></div>';
+ content.document.designMode = "on";
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ { capture: true },
+ onPopupShown
+ );
+ ok(
+ true,
+ "Auto scroll should be started in document whose designMode is 'on' if middle paste is disabled"
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["middlemouse.paste", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<input style="height: 10000px; width: 10000px;">';
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ { capture: true },
+ onPopupShown
+ );
+ ok(
+ true,
+ "Auto scroll should be started on <input> if middle paste is disabled"
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["middlemouse.paste", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<input style="height: 10000px; width: 10000px;">';
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await TestUtils.waitForCondition(
+ popupIsNotClosed,
+ "Wait for timeout of popup",
+ 100,
+ 10
+ );
+ ok(
+ false,
+ "Autoscroll shouldn't be started on <input> if middle paste is enabled"
+ );
+ } catch (e) {
+ ok(
+ typeof e == "string" && e.includes(" - timed out after 10 tries."),
+ `Autoscroll shouldn't be started on <input> if middle paste is enabled (${
+ typeof e == "string" ? e : e.message
+ })`
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js b/toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js
new file mode 100644
index 0000000000..970870b919
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_autoscroll_links() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", false],
+ ],
+ });
+
+ let autoScroller;
+ function onPopupShown(aEvent) {
+ if (aEvent.originalTarget.id != "autoscroller") {
+ return false;
+ }
+ autoScroller = aEvent.originalTarget;
+ return true;
+ }
+ window.addEventListener("popupshown", onPopupShown, { capture: true });
+ registerCleanupFunction(() => {
+ window.removeEventListener("popupshown", onPopupShown, { capture: true });
+ });
+ function popupIsNotClosed() {
+ return autoScroller && autoScroller.state != "closed";
+ }
+
+ async function promiseNativeMouseMiddleButtonDown(aBrowser) {
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: aBrowser,
+ atCenter: true,
+ });
+ return EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: aBrowser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ }
+ async function promiseNativeMouseMiddleButtonUp(aBrowser) {
+ return EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: aBrowser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ }
+ function promiseWaitForAutoScrollerClosed() {
+ if (!autoScroller || autoScroller.state == "closed") {
+ return Promise.resolve();
+ }
+ let result = BrowserTestUtils.waitForEvent(
+ autoScroller,
+ "popuphidden",
+ { capture: true },
+ () => {
+ return true;
+ }
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ return result;
+ }
+
+ async function testMarkup(markup) {
+ return BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [markup], html => {
+ // eslint-disable-next-line no-unsanitized/property
+ content.document.body.innerHTML = html;
+ content.document.documentElement.scrollTop = 1;
+ content.document.documentElement.scrollTop; // Flush layout.
+ });
+ await promiseNativeMouseMiddleButtonDown(browser);
+ try {
+ await TestUtils.waitForCondition(
+ popupIsNotClosed,
+ "Wait for timeout of popup",
+ 100,
+ 10
+ );
+ ok(false, "Autoscroll shouldn't be started on " + markup);
+ } catch (e) {
+ ok(
+ typeof e == "string" && e.includes(" - timed out after 10 tries."),
+ `Autoscroll shouldn't be started on ${markup} (${
+ typeof e == "string" ? e : e.message
+ })`
+ );
+ } finally {
+ await promiseNativeMouseMiddleButtonUp(browser);
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ await waitForAutoScrollEnd;
+ }
+ }
+ );
+ }
+
+ await testMarkup(
+ '<a href="https://example.com/" style="display: block; position: absolute; height:100%; width:100%; background: aqua">Click me</a>'
+ );
+
+ await testMarkup(`
+ <svg viewbox="0 0 100 100" style="display: block; height: 100%; width: 100%;">
+ <a href="https://example.com/">
+ <rect height=100 width=100 fill=blue />
+ </a>
+ </svg>`);
+
+ await testMarkup(`
+ <a href="https://example.com/">
+ <svg viewbox="0 0 100 100" style="display: block; height: 100%; width: 100%;">
+ <use href="#x"/>
+ </svg>
+ </a>
+
+ <svg viewbox="0 0 100 100" style="display: none">
+ <rect id="x" height=100 width=100 fill=green />
+ </svg>
+ `);
+});
diff --git a/toolkit/content/tests/browser/browser_bug1170531.js b/toolkit/content/tests/browser/browser_bug1170531.js
new file mode 100644
index 0000000000..483d2dad7f
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug1170531.js
@@ -0,0 +1,139 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+// Test for bug 1170531
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1170531
+
+add_task(async function () {
+ // Get a bunch of DOM nodes
+ let editMenu = document.getElementById("edit-menu");
+ let menuPopup = editMenu.menupopup;
+
+ let closeMenu = function (aCallback) {
+ if (Services.appinfo.OS == "Darwin") {
+ executeSoon(aCallback);
+ return;
+ }
+
+ menuPopup.addEventListener(
+ "popuphidden",
+ function () {
+ executeSoon(aCallback);
+ },
+ { once: true }
+ );
+
+ executeSoon(function () {
+ editMenu.open = false;
+ });
+ };
+
+ let openMenu = function (aCallback) {
+ if (Services.appinfo.OS == "Darwin") {
+ goUpdateGlobalEditMenuItems();
+ // On OSX, we have a native menu, so it has to be updated. In single process browsers,
+ // this happens synchronously, but in e10s, we have to wait for the main thread
+ // to deal with it for us. 1 second should be plenty of time.
+ setTimeout(aCallback, 1000);
+ return;
+ }
+
+ menuPopup.addEventListener(
+ "popupshown",
+ function () {
+ executeSoon(aCallback);
+ },
+ { once: true }
+ );
+
+ executeSoon(function () {
+ editMenu.open = true;
+ });
+ };
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ let menu_cut_disabled, menu_copy_disabled;
+
+ BrowserTestUtils.loadURIString(
+ browser,
+ "data:text/html,<div>hello!</div>"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+ browser.focus();
+ await new Promise(resolve => waitForFocus(resolve, window));
+ await new Promise(resolve =>
+ window.requestAnimationFrame(() => executeSoon(resolve))
+ );
+ await new Promise(openMenu);
+ menu_cut_disabled =
+ menuPopup.querySelector("#menu_cut").getAttribute("disabled") == "true";
+ is(menu_cut_disabled, false, "menu_cut should be enabled");
+ menu_copy_disabled =
+ menuPopup.querySelector("#menu_copy").getAttribute("disabled") ==
+ "true";
+ is(menu_copy_disabled, false, "menu_copy should be enabled");
+ await new Promise(closeMenu);
+
+ // When there is no text selected in the contentEditable, we expect the Cut
+ // and Copy commands to be disabled.
+ BrowserTestUtils.loadURIString(
+ browser,
+ "data:text/html,<div contentEditable='true'>hello!</div>"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+ browser.focus();
+ await new Promise(resolve => waitForFocus(resolve, window));
+ await new Promise(resolve =>
+ window.requestAnimationFrame(() => executeSoon(resolve))
+ );
+ await new Promise(openMenu);
+ menu_cut_disabled =
+ menuPopup.querySelector("#menu_cut").getAttribute("disabled") == "true";
+ is(menu_cut_disabled, true, "menu_cut should be disabled");
+ menu_copy_disabled =
+ menuPopup.querySelector("#menu_copy").getAttribute("disabled") ==
+ "true";
+ is(menu_copy_disabled, true, "menu_copy should be disabled");
+ await new Promise(closeMenu);
+
+ // When the text of the contentEditable is selected, the Cut and Copy commands
+ // should be enabled.
+ BrowserTestUtils.loadURIString(
+ browser,
+ "data:text/html,<div contentEditable='true'>hello!</div><script>r=new Range;r.selectNodeContents(document.body.firstChild);document.getSelection().addRange(r);</script>"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+ browser.focus();
+ await new Promise(resolve => waitForFocus(resolve, window));
+ await new Promise(resolve =>
+ window.requestAnimationFrame(() => executeSoon(resolve))
+ );
+ await new Promise(openMenu);
+ menu_cut_disabled =
+ menuPopup.querySelector("#menu_cut").getAttribute("disabled") == "true";
+ is(menu_cut_disabled, false, "menu_cut should be enabled");
+ menu_copy_disabled =
+ menuPopup.querySelector("#menu_copy").getAttribute("disabled") ==
+ "true";
+ is(menu_copy_disabled, false, "menu_copy should be enabled");
+ await new Promise(closeMenu);
+
+ BrowserTestUtils.loadURIString(browser, "about:preferences");
+ await BrowserTestUtils.browserLoaded(browser);
+ browser.focus();
+ await new Promise(resolve => waitForFocus(resolve, window));
+ await new Promise(resolve =>
+ window.requestAnimationFrame(() => executeSoon(resolve))
+ );
+ await new Promise(openMenu);
+ menu_cut_disabled =
+ menuPopup.querySelector("#menu_cut").getAttribute("disabled") == "true";
+ is(menu_cut_disabled, true, "menu_cut should be disabled");
+ menu_copy_disabled =
+ menuPopup.querySelector("#menu_copy").getAttribute("disabled") ==
+ "true";
+ is(menu_copy_disabled, true, "menu_copy should be disabled");
+ await new Promise(closeMenu);
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_bug1198465.js b/toolkit/content/tests/browser/browser_bug1198465.js
new file mode 100644
index 0000000000..52a3705ac4
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug1198465.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var kPrefName = "accessibility.typeaheadfind.prefillwithselection";
+var kEmptyURI = "data:text/html,";
+
+// This pref is false by default in OSX; ensure the test still works there.
+Services.prefs.setBoolPref(kPrefName, true);
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(kPrefName);
+});
+
+add_task(async function () {
+ let aTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kEmptyURI);
+ ok(!gFindBarInitialized, "findbar isn't initialized yet");
+
+ // Note: the use case here is when the user types directly in the findbar
+ // _before_ it's prefilled with a text selection in the page.
+
+ // So `yield BrowserTestUtils.sendChar()` can't be used here:
+ // - synthesizing a key in the browser won't actually send it to the
+ // findbar; the findbar isn't part of the browser content.
+ // - we need to _not_ wait for _startFindDeferred to be resolved; yielding
+ // a synthesized keypress on the browser implicitely happens after the
+ // browser has dispatched its return message with the prefill value for
+ // the findbar, which essentially nulls these tests.
+
+ // The parent-side of the sidebar initialization is also async, so we do
+ // need to wait for that. We verify a bit further down that _startFindDeferred
+ // hasn't been resolved yet.
+ await gFindBarPromise;
+
+ let findBar = gFindBar;
+ is(findBar._findField.value, "", "findbar is empty");
+
+ // Test 1
+ // Any input in the findbar should erase a previous search.
+
+ findBar._findField.value = "xy";
+ findBar.startFind();
+ is(findBar._findField.value, "xy", "findbar should have xy initial query");
+ is(findBar._findField, document.activeElement, "findbar is now focused");
+
+ EventUtils.sendChar("z", window);
+ is(findBar._findField.value, "z", "z erases xy");
+
+ findBar._findField.value = "";
+ ok(!findBar._findField.value, "erase findbar after first test");
+
+ // Test 2
+ // Prefilling the findbar should be ignored if a search has been run.
+
+ findBar.startFind();
+ ok(findBar._startFindDeferred, "prefilled value hasn't been fetched yet");
+ is(findBar._findField, document.activeElement, "findbar is still focused");
+
+ EventUtils.sendChar("a", window);
+ EventUtils.sendChar("b", window);
+ is(findBar._findField.value, "ab", "initial ab typed in the findbar");
+
+ // This resolves _startFindDeferred if it's still pending; let's just skip
+ // over waiting for the browser's return message that should do this as it
+ // doesn't really matter.
+ findBar.onCurrentSelection("foo", true);
+ ok(!findBar._startFindDeferred, "prefilled value fetched");
+ is(findBar._findField.value, "ab", "ab kept instead of prefill value");
+
+ EventUtils.sendChar("c", window);
+ is(findBar._findField.value, "abc", "c is appended after ab");
+
+ // Clear the findField value to make the test run successfully
+ // for multiple runs in the same browser session.
+ findBar._findField.value = "";
+ BrowserTestUtils.removeTab(aTab);
+});
diff --git a/toolkit/content/tests/browser/browser_bug1572798.js b/toolkit/content/tests/browser/browser_bug1572798.js
new file mode 100644
index 0000000000..0e15ef4f3d
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug1572798.js
@@ -0,0 +1,29 @@
+add_task(async function test_bug_1572798() {
+ let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank");
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ "https://example.com/browser/toolkit/content/tests/browser/file_document_open_audio.html"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let windowLoaded = BrowserTestUtils.waitForNewWindow();
+ info("- clicking button to spawn a new window -");
+ await ContentTask.spawn(tab.linkedBrowser, null, function () {
+ content.document.querySelector("button").click();
+ });
+ info("- waiting for the new window -");
+ let newWin = await windowLoaded;
+ info("- checking that the new window plays the audio -");
+ let documentOpenedBrowser = newWin.gBrowser.selectedBrowser;
+ await ContentTask.spawn(documentOpenedBrowser, null, async function () {
+ try {
+ await content.document.querySelector("audio").play();
+ ok(true, "Could play the audio");
+ } catch (e) {
+ ok(false, "Rejected audio promise" + e);
+ }
+ });
+
+ info("- Cleaning up -");
+ await BrowserTestUtils.closeWindow(newWin);
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_bug1693577.js b/toolkit/content/tests/browser/browser_bug1693577.js
new file mode 100644
index 0000000000..712749dc89
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug1693577.js
@@ -0,0 +1,49 @@
+/*
+ * This test checks that the popupshowing event for input fields, which do not
+ * have a dedicated contextmenu event, but use the global one (added by
+ * editMenuOverlay.js, see bug 1693577) include a triggerNode.
+ *
+ * The search-input field of the browser-sidebar is one of the rare cases in
+ * mozilla-central, which can be used to test this. There are a few more in
+ * comm-central, which need the triggerNode information.
+ */
+
+add_task(async function test_search_input_popupshowing() {
+ let sidebar = document.getElementById("sidebar");
+
+ let loadPromise = BrowserTestUtils.waitForEvent(sidebar, "load", true);
+ SidebarUI.toggle("viewBookmarksSidebar");
+ await loadPromise;
+
+ let inputField =
+ sidebar.contentDocument.getElementById("search-box").inputField;
+ const popupshowing = BrowserTestUtils.waitForEvent(
+ sidebar.contentWindow,
+ "popupshowing"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ inputField,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ sidebar.contentWindow
+ );
+ let popupshowingEvent = await popupshowing;
+
+ Assert.equal(
+ popupshowingEvent.target.triggerNode?.id,
+ "search-box",
+ "Popupshowing event for the search input includes triggernode."
+ );
+
+ const popup = popupshowingEvent.target;
+ await BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+ const popuphidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.hidePopup();
+ await popuphidden;
+
+ SidebarUI.toggle("viewBookmarksSidebar");
+});
diff --git a/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js b/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js
new file mode 100644
index 0000000000..431fdf048f
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js
@@ -0,0 +1,389 @@
+requestLongerTimeout(2);
+add_task(async function () {
+ function pushPrefs(prefs) {
+ return SpecialPowers.pushPrefEnv({ set: prefs });
+ }
+
+ await pushPrefs([
+ ["general.autoScroll", true],
+ ["test.events.async.enabled", true],
+ ]);
+
+ const expectScrollNone = 0;
+ const expectScrollVert = 1;
+ const expectScrollHori = 2;
+ const expectScrollBoth = 3;
+
+ var allTests = [
+ {
+ dataUri:
+ 'data:text/html,<html><head><meta charset="utf-8"></head><body><style type="text/css">div { display: inline-block; }</style>\
+ <div id="a" style="width: 100px; height: 100px; overflow: hidden;"><div style="width: 200px; height: 200px;"></div></div>\
+ <div id="b" style="width: 100px; height: 100px; overflow: auto;"><div style="width: 200px; height: 200px;"></div></div>\
+ <div id="c" style="width: 100px; height: 100px; overflow-x: auto; overflow-y: hidden;"><div style="width: 200px; height: 200px;"></div></div>\
+ <div id="d" style="width: 100px; height: 100px; overflow-y: auto; overflow-x: hidden;"><div style="width: 200px; height: 200px;"></div></div>\
+ <select id="e" style="width: 100px; height: 100px;" multiple="multiple"><option>aaaaaaaaaaaaaaaaaaaaaaaa</option><option>a</option><option>a</option>\
+ <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option>\
+ <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option></select>\
+ <select id="f" style="width: 100px; height: 100px;"><option>a</option><option>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</option><option>a</option>\
+ <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option>\
+ <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option></select>\
+ <div id="g" style="width: 99px; height: 99px; border: 10px solid black; margin: 10px; overflow: auto;"><div style="width: 100px; height: 100px;"></div></div>\
+ <div id="h" style="width: 100px; height: 100px; overflow: clip;"><div style="width: 200px; height: 200px;"></div></div>\
+ <iframe id="iframe" style="display: none;"></iframe>\
+ </body></html>',
+ },
+ { elem: "a", expected: expectScrollNone },
+ { elem: "b", expected: expectScrollBoth },
+ { elem: "c", expected: expectScrollHori },
+ { elem: "d", expected: expectScrollVert },
+ { elem: "e", expected: expectScrollVert },
+ { elem: "f", expected: expectScrollNone },
+ { elem: "g", expected: expectScrollBoth },
+ { elem: "h", expected: expectScrollNone },
+ {
+ dataUri:
+ 'data:text/html,<html><head><meta charset="utf-8"></head><body id="i" style="overflow-y: scroll"><div style="height: 2000px"></div>\
+ <iframe id="iframe" style="display: none;"></iframe>\
+ </body></html>',
+ },
+ { elem: "i", expected: expectScrollVert }, // bug 695121
+ {
+ dataUri:
+ 'data:text/html,<html><head><meta charset="utf-8"></head><style>html, body { width: 100%; height: 100%; overflow-x: hidden; overflow-y: scroll; }</style>\
+ <body id="j"><div style="height: 2000px"></div>\
+ <iframe id="iframe" style="display: none;"></iframe>\
+ </body></html>',
+ },
+ { elem: "j", expected: expectScrollVert }, // bug 914251
+ {
+ dataUri:
+ 'data:text/html,<html><head><meta charset="utf-8">\
+<style>\
+body > div {scroll-behavior: smooth;width: 300px;height: 300px;overflow: scroll;}\
+body > div > div {width: 1000px;height: 1000px;}\
+</style>\
+</head><body><div id="t"><div></div></div></body></html>',
+ },
+ { elem: "t", expected: expectScrollBoth }, // bug 1308775
+ {
+ dataUri:
+ 'data:text/html,<html><head><meta charset="utf-8"></head><body>\
+<div id="k" style="height: 150px; width: 200px; overflow: scroll; border: 1px solid black;">\
+<iframe style="height: 200px; width: 300px;"></iframe>\
+</div>\
+<div id="l" style="height: 150px; width: 300px; overflow: scroll; border: 1px dashed black;">\
+<iframe style="height: 200px; width: 200px;" src="data:text/html,<div style=\'border: 5px solid blue; height: 200%; width: 200%;\'></div>"></iframe>\
+</div>\
+<iframe id="m"></iframe>\
+<div style="height: 200%; border: 5px dashed black;">filler to make document overflow: scroll;</div>\
+</body></html>',
+ },
+ { elem: "k", expected: expectScrollBoth },
+ { elem: "k", expected: expectScrollNone, testwindow: true },
+ { elem: "l", expected: expectScrollNone },
+ { elem: "m", expected: expectScrollVert, testwindow: true },
+ {
+ dataUri:
+ 'data:text/html,<html><head><meta charset="utf-8"></head><body>\
+<img width="100" height="100" alt="image map" usemap="%23planetmap">\
+<map name="planetmap">\
+ <area id="n" shape="rect" coords="0,0,100,100" href="javascript:void(null)">\
+</map>\
+<a href="javascript:void(null)" id="o" style="width: 100px; height: 100px; border: 1px solid black; display: inline-block; vertical-align: top;">link</a>\
+<input id="p" style="width: 100px; height: 100px; vertical-align: top;">\
+<textarea id="q" style="width: 100px; height: 100px; vertical-align: top;"></textarea>\
+<div style="height: 200%; border: 1px solid black;"></div>\
+</body></html>',
+ },
+ { elem: "n", expected: expectScrollNone, testwindow: true },
+ { elem: "o", expected: expectScrollNone, testwindow: true },
+ {
+ elem: "p",
+ expected: expectScrollVert,
+ testwindow: true,
+ middlemousepastepref: false,
+ },
+ {
+ elem: "q",
+ expected: expectScrollVert,
+ testwindow: true,
+ middlemousepastepref: false,
+ },
+ {
+ dataUri:
+ 'data:text/html,<html><head><meta charset="utf-8"></head><body>\
+<input id="r" style="width: 100px; height: 100px; vertical-align: top;">\
+<textarea id="s" style="width: 100px; height: 100px; vertical-align: top;"></textarea>\
+<div style="height: 200%; border: 1px solid black;"></div>\
+</body></html>',
+ },
+ {
+ elem: "r",
+ expected: expectScrollNone,
+ testwindow: true,
+ middlemousepastepref: true,
+ },
+ {
+ elem: "s",
+ expected: expectScrollNone,
+ testwindow: true,
+ middlemousepastepref: true,
+ },
+ {
+ dataUri:
+ "data:text/html," +
+ encodeURIComponent(`
+<!doctype html>
+<iframe id=i height=100 width=100 scrolling="no" srcdoc="<div style='height: 200px'>Auto-scrolling should never make me disappear"></iframe>
+<div style="height: 100vh"></div>
+ `),
+ },
+ {
+ elem: "i",
+ // We expect the outer window to scroll vertically, not the iframe's window.
+ expected: expectScrollVert,
+ testwindow: true,
+ },
+ {
+ dataUri:
+ "data:text/html," +
+ encodeURIComponent(`
+<!doctype html>
+<iframe id=i height=100 width=100 srcdoc="<div style='height: 200px'>Auto-scrolling should make me disappear"></iframe>
+<div style="height: 100vh"></div>
+ `),
+ },
+ {
+ elem: "i",
+ // We expect the iframe's window to scroll vertically, so the outer window should not scroll.
+ expected: expectScrollNone,
+ testwindow: true,
+ },
+ {
+ // Test: scroll is initiated in out of process iframe having no scrollable area
+ dataUri:
+ "data:text/html," +
+ encodeURIComponent(`
+<!doctype html>
+<head><meta content="text/html;charset=utf-8"></head><body>
+<div id="scroller" style="width: 300px; height: 300px; overflow-y: scroll; overflow-x: hidden; border: solid 1px blue;">
+ <iframe id="noscroll-outofprocess-iframe"
+ src="https://example.com/document-builder.sjs?html=<html><body>Hey!</body></html>"
+ style="border: solid 1px green; margin: 2px;"></iframe>
+ <div style="width: 100%; height: 200px;"></div>
+</div></body>
+ `),
+ },
+ {
+ elem: "noscroll-outofprocess-iframe",
+ // We expect the div to scroll vertically, not the iframe's window.
+ expected: expectScrollVert,
+ scrollable: "scroller",
+ },
+ ];
+
+ for (let test of allTests) {
+ if (test.dataUri) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURIString(gBrowser, test.dataUri);
+ await loadedPromise;
+ await ContentTask.spawn(gBrowser.selectedBrowser, {}, async () => {
+ // Wait for a paint so that hit-testing works correctly.
+ await new Promise(resolve =>
+ content.requestAnimationFrame(() =>
+ content.requestAnimationFrame(resolve)
+ )
+ );
+ });
+ continue;
+ }
+
+ let prefsChanged = "middlemousepastepref" in test;
+ if (prefsChanged) {
+ await pushPrefs([["middlemouse.paste", test.middlemousepastepref]]);
+ }
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#" + test.elem,
+ 50,
+ 80,
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+
+ // This ensures bug 605127 is fixed: pagehide in an unrelated document
+ // should not cancel the autoscroll.
+ await ContentTask.spawn(
+ gBrowser.selectedBrowser,
+ { waitForAutoScrollStart: test.expected != expectScrollNone },
+ async ({ waitForAutoScrollStart }) => {
+ var iframe = content.document.getElementById("iframe");
+
+ if (iframe) {
+ var e = new iframe.contentWindow.PageTransitionEvent("pagehide", {
+ bubbles: true,
+ cancelable: true,
+ persisted: false,
+ });
+ iframe.contentDocument.dispatchEvent(e);
+ iframe.contentDocument.documentElement.dispatchEvent(e);
+ }
+ if (waitForAutoScrollStart) {
+ await new Promise(resolve =>
+ Services.obs.addObserver(resolve, "autoscroll-start")
+ );
+ }
+ }
+ );
+
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Browser still focused after autoscroll started"
+ );
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#" + test.elem,
+ 100,
+ 100,
+ { type: "mousemove", clickCount: "0" },
+ gBrowser.selectedBrowser
+ );
+
+ if (prefsChanged) {
+ await SpecialPowers.popPrefEnv();
+ }
+
+ // Start checking for the scroll.
+ let firstTimestamp = undefined;
+ let timeCompensation;
+ do {
+ let timestamp = await new Promise(resolve =>
+ window.requestAnimationFrame(resolve)
+ );
+ if (firstTimestamp === undefined) {
+ firstTimestamp = timestamp;
+ }
+
+ // This value is calculated similarly to the value of the same name in
+ // ClickEventHandler.autoscrollLoop, except here it's cumulative across
+ // all frames after the first one instead of being based only on the
+ // current frame.
+ timeCompensation = (timestamp - firstTimestamp) / 20;
+ info(
+ "timestamp=" +
+ timestamp +
+ " firstTimestamp=" +
+ firstTimestamp +
+ " timeCompensation=" +
+ timeCompensation
+ );
+
+ // Try to wait until enough time has passed to allow the scroll to happen.
+ // autoscrollLoop incrementally scrolls during each animation frame, but
+ // due to how its calculations work, when a frame is very close to the
+ // previous frame, no scrolling may actually occur during that frame.
+ // After 100ms's worth of frames, timeCompensation will be 1, making it
+ // more likely that the accumulated scroll in autoscrollLoop will be >= 1,
+ // although it also depends on acceleration, which here in this test
+ // should be > 1 due to how it synthesizes mouse events below.
+ } while (timeCompensation < 5);
+
+ // Close the autoscroll popup by synthesizing Esc.
+ EventUtils.synthesizeKey("KEY_Escape");
+ let scrollVert = test.expected & expectScrollVert;
+ let scrollHori = test.expected & expectScrollHori;
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [
+ {
+ scrollVert,
+ scrollHori,
+ elemid: test.scrollable || test.elem,
+ checkWindow: test.testwindow,
+ },
+ ],
+ async function (args) {
+ let msg = "";
+ if (args.checkWindow) {
+ if (
+ !(
+ (args.scrollVert && content.scrollY > 0) ||
+ (!args.scrollVert && content.scrollY == 0)
+ )
+ ) {
+ msg += "Failed: ";
+ }
+ msg +=
+ "Window for " +
+ args.elemid +
+ " should" +
+ (args.scrollVert ? "" : " not") +
+ " have scrolled vertically\n";
+
+ if (
+ !(
+ (args.scrollHori && content.scrollX > 0) ||
+ (!args.scrollHori && content.scrollX == 0)
+ )
+ ) {
+ msg += "Failed: ";
+ }
+ msg +=
+ " Window for " +
+ args.elemid +
+ " should" +
+ (args.scrollHori ? "" : " not") +
+ " have scrolled horizontally\n";
+ } else {
+ let elem = content.document.getElementById(args.elemid);
+ if (
+ !(
+ (args.scrollVert && elem.scrollTop > 0) ||
+ (!args.scrollVert && elem.scrollTop == 0)
+ )
+ ) {
+ msg += "Failed: ";
+ }
+ msg +=
+ " " +
+ args.elemid +
+ " should" +
+ (args.scrollVert ? "" : " not") +
+ " have scrolled vertically\n";
+ if (
+ !(
+ (args.scrollHori && elem.scrollLeft > 0) ||
+ (!args.scrollHori && elem.scrollLeft == 0)
+ )
+ ) {
+ msg += "Failed: ";
+ }
+ msg +=
+ args.elemid +
+ " should" +
+ (args.scrollHori ? "" : " not") +
+ " have scrolled horizontally";
+ }
+
+ Assert.ok(!msg.includes("Failed"), msg);
+ }
+ );
+
+ // Before continuing the test, we need to ensure that the IPC
+ // message that stops autoscrolling has had time to arrive.
+ await new Promise(resolve => executeSoon(resolve));
+ }
+
+ // remove 2 tabs that were opened by middle-click on links
+ while (gBrowser.visibleTabs.length > 1) {
+ gBrowser.removeTab(gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1]);
+ }
+
+ // wait for focus to fix a failure in the next test if the latter runs too soon.
+ await SimpleTest.promiseFocus();
+});
diff --git a/toolkit/content/tests/browser/browser_bug451286.js b/toolkit/content/tests/browser/browser_bug451286.js
new file mode 100644
index 0000000000..e7f03c96f7
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug451286.js
@@ -0,0 +1,166 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js",
+ this
+);
+
+add_task(async function () {
+ const SEARCH_TEXT = "text";
+ const DATAURI = "data:text/html," + SEARCH_TEXT;
+
+ // Bug 451286. An iframe that should be highlighted
+ let visible = "<iframe id='visible' src='" + DATAURI + "'></iframe>";
+
+ // Bug 493658. An invisible iframe that shouldn't interfere with
+ // highlighting matches lying after it in the document
+ let invisible =
+ "<iframe id='invisible' style='display: none;' " +
+ "src='" +
+ DATAURI +
+ "'></iframe>";
+
+ let uri = DATAURI + invisible + SEARCH_TEXT + visible + SEARCH_TEXT;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri);
+ let contentRect = tab.linkedBrowser.getBoundingClientRect();
+ let noHighlightSnapshot = snapshotRect(window, contentRect);
+ ok(noHighlightSnapshot, "Got noHighlightSnapshot");
+
+ await openFindBarAndWait();
+ gFindBar._findField.value = SEARCH_TEXT;
+ await findAgainAndWait();
+ var matchCase = gFindBar.getElement("find-case-sensitive");
+ if (matchCase.checked) {
+ matchCase.doCommand();
+ }
+
+ // Turn on highlighting
+ await toggleHighlightAndWait(true);
+ await closeFindBarAndWait();
+
+ // Take snapshot of highlighting
+ let findSnapshot = snapshotRect(window, contentRect);
+ ok(findSnapshot, "Got findSnapshot");
+
+ // Now, remove the highlighting, and take a snapshot to compare
+ // to our original state
+ await openFindBarAndWait();
+ await toggleHighlightAndWait(false);
+ await closeFindBarAndWait();
+
+ let unhighlightSnapshot = snapshotRect(window, contentRect);
+ ok(unhighlightSnapshot, "Got unhighlightSnapshot");
+
+ // Select the matches that should have been highlighted manually
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let doc = content.document;
+ let win = doc.defaultView;
+
+ // Create a manual highlight in the visible iframe to test bug 451286
+ let iframe = doc.getElementById("visible");
+ let ifBody = iframe.contentDocument.body;
+ let range = iframe.contentDocument.createRange();
+ range.selectNodeContents(ifBody.childNodes[0]);
+ let ifWindow = iframe.contentWindow;
+ let ifDocShell = ifWindow.docShell;
+
+ let ifController = ifDocShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+
+ let frameFindSelection = ifController.getSelection(
+ ifController.SELECTION_FIND
+ );
+ frameFindSelection.addRange(range);
+
+ // Create manual highlights in the main document (the matches that lie
+ // before/after the iframes
+ let docShell = win.docShell;
+
+ let controller = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+
+ let docFindSelection = controller.getSelection(ifController.SELECTION_FIND);
+
+ range = doc.createRange();
+ range.selectNodeContents(doc.body.childNodes[0]);
+ docFindSelection.addRange(range);
+ range = doc.createRange();
+ range.selectNodeContents(doc.body.childNodes[2]);
+ docFindSelection.addRange(range);
+ range = doc.createRange();
+ range.selectNodeContents(doc.body.childNodes[4]);
+ docFindSelection.addRange(range);
+ });
+
+ // Take snapshot of manual highlighting
+ let manualSnapshot = snapshotRect(window, contentRect);
+ ok(manualSnapshot, "Got manualSnapshot");
+
+ // Test 1: Were the matches in iframe correctly highlighted?
+ let res = compareSnapshots(findSnapshot, manualSnapshot, true);
+ ok(res[0], "Matches found in iframe correctly highlighted");
+
+ // Test 2: Were the matches in iframe correctly unhighlighted?
+ res = compareSnapshots(noHighlightSnapshot, unhighlightSnapshot, true);
+ ok(res[0], "Highlighting in iframe correctly removed");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+function toggleHighlightAndWait(shouldHighlight) {
+ return new Promise(resolve => {
+ let listener = {
+ onFindResult() {},
+ onHighlightFinished() {
+ gFindBar.browser.finder.removeResultListener(listener);
+ resolve();
+ },
+ onMatchesCountResult() {},
+ };
+ gFindBar.browser.finder.addResultListener(listener);
+ gFindBar.toggleHighlight(shouldHighlight);
+ });
+}
+
+function findAgainAndWait() {
+ return new Promise(resolve => {
+ let listener = {
+ onFindResult() {
+ gFindBar.browser.finder.removeResultListener(listener);
+ resolve();
+ },
+ onHighlightFinished() {},
+ onMatchesCountResult() {},
+ };
+ gFindBar.browser.finder.addResultListener(listener);
+ gFindBar.onFindAgainCommand();
+ });
+}
+
+async function openFindBarAndWait() {
+ await gFindBarPromise;
+ let awaitTransitionEnd = BrowserTestUtils.waitForEvent(
+ gFindBar,
+ "transitionend"
+ );
+ gFindBar.open();
+ await awaitTransitionEnd;
+}
+
+// This test is comparing snapshots. It is necessary to wait for the gFindBar
+// to close before taking the snapshot so the gFindBar does not take up space
+// on the new snapshot.
+async function closeFindBarAndWait() {
+ let awaitTransitionEnd = BrowserTestUtils.waitForEvent(
+ gFindBar,
+ "transitionend",
+ false,
+ event => {
+ return event.propertyName == "visibility";
+ }
+ );
+ gFindBar.close();
+ await awaitTransitionEnd;
+}
diff --git a/toolkit/content/tests/browser/browser_bug594509.js b/toolkit/content/tests/browser/browser_bug594509.js
new file mode 100644
index 0000000000..b177c00d7c
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug594509.js
@@ -0,0 +1,15 @@
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:rights"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ Assert.ok(
+ content.document.getElementById("your-rights"),
+ "about:rights content loaded"
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_bug982298.js b/toolkit/content/tests/browser/browser_bug982298.js
new file mode 100644
index 0000000000..94846ff1cd
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_bug982298.js
@@ -0,0 +1,77 @@
+const scrollHtml =
+ '<textarea id="textarea1" row=2>Firefox\n\nFirefox\n\n\n\n\n\n\n\n\n\n' +
+ '</textarea><a href="about:blank">blank</a>';
+
+add_task(async function () {
+ let url = "data:text/html;base64," + btoa(scrollHtml);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url },
+ async function (browser) {
+ let awaitFindResult = new Promise(resolve => {
+ let listener = {
+ onFindResult(aData) {
+ info("got find result");
+ browser.finder.removeResultListener(listener);
+
+ ok(
+ aData.result == Ci.nsITypeAheadFind.FIND_FOUND,
+ "should find string"
+ );
+ resolve();
+ },
+ onCurrentSelection() {},
+ onMatchesCountResult() {},
+ };
+ info(
+ "about to add results listener, open find bar, and send 'F' string"
+ );
+ browser.finder.addResultListener(listener);
+ });
+ await gFindBarPromise;
+ gFindBar.onFindCommand();
+ EventUtils.sendString("F");
+ info("added result listener and sent string 'F'");
+ await awaitFindResult;
+
+ // scroll textarea to bottom
+ await SpecialPowers.spawn(browser, [], () => {
+ let textarea = content.document.getElementById("textarea1");
+ textarea.scrollTop = textarea.scrollHeight;
+ });
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ ok(
+ browser.currentURI.spec == "about:blank",
+ "got load event for about:blank"
+ );
+
+ let awaitFindResult2 = new Promise(resolve => {
+ let listener = {
+ onFindResult(aData) {
+ info("got find result #2");
+ browser.finder.removeResultListener(listener);
+ resolve();
+ },
+ onCurrentSelection() {},
+ onMatchesCountResult() {},
+ };
+
+ browser.finder.addResultListener(listener);
+ info("added result listener");
+ });
+ // find again needs delay for crash test
+ setTimeout(function () {
+ // ignore exception if occured
+ try {
+ info("about to send find again command");
+ gFindBar.onFindAgainCommand(false);
+ info("sent find again command");
+ } catch (e) {
+ info("got exception from onFindAgainCommand: " + e);
+ }
+ }, 0);
+ await awaitFindResult2;
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_cancel_starting_autoscrolling_requested_by_background_tab.js b/toolkit/content/tests/browser/browser_cancel_starting_autoscrolling_requested_by_background_tab.js
new file mode 100644
index 0000000000..989c91f2fb
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_cancel_starting_autoscrolling_requested_by_background_tab.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testStopStartingAutoScroll() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", true],
+ [
+ "accessibility.mouse_focuses_formcontrol",
+ !navigator.platform.includes("Mac"),
+ ],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ async function doTest({
+ aInnerHTML,
+ aDescription,
+ aExpectedActiveElement,
+ }) {
+ await SpecialPowers.spawn(browser, [aInnerHTML], async contentHTML => {
+ // eslint-disable-next-line no-unsanitized/property
+ content.document.body.innerHTML = contentHTML;
+ content.document.documentElement.scrollTop; // Flush layout.
+ const iframe = content.document.querySelector("iframe");
+ // If the test page has an iframe, we need to ensure it has loaded.
+ if (!iframe || iframe.contentDocument?.readyState == "complete") {
+ return;
+ }
+ // It's too late to check "load" event. Let's check
+ // Document#readyState instead.
+ await ContentTaskUtils.waitForCondition(
+ () => iframe.contentDocument?.readyState == "complete",
+ "Waiting for loading the subdocument"
+ );
+ });
+
+ let autoScroller;
+ let onPopupShown = event => {
+ if (event.originalTarget.id !== "autoscroller") {
+ return false;
+ }
+ autoScroller = event.originalTarget;
+ info(`${aDescription}: "popupshown" event is fired`);
+ autoScroller.getBoundingClientRect(); // Flush layout of the autoscroller
+ return true;
+ };
+ window.addEventListener("popupshown", onPopupShown, { capture: true });
+ registerCleanupFunction(() => {
+ window.removeEventListener("popupshown", onPopupShown, {
+ capture: true,
+ });
+ });
+
+ let waitForNewTabForeground = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "TabSwitchDone"
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ button: 1, // middle click
+ target: browser,
+ atCenter: true,
+ });
+ info(`${aDescription}: Waiting for active tab switched...`);
+ await waitForNewTabForeground;
+ // To confirm that autoscrolling won't start accidentally, we should
+ // wait a while.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ is(
+ autoScroller,
+ undefined,
+ `${aDescription}: autoscroller should not be open because requested tab is now in background`
+ );
+ // Clean up
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ button: 1, // middle click
+ target: browser,
+ atCenter: true,
+ }); // release implicit capture
+ EventUtils.synthesizeKey("KEY_Escape"); // To close unexpected autoscroller
+ isnot(
+ browser,
+ gBrowser.selectedBrowser,
+ `${aDescription}: The clicked tab shouldn't be foreground tab`
+ );
+ is(
+ gBrowser.selectedTab,
+ gBrowser.tabs[gBrowser.tabs.length - 1],
+ `${aDescription}: The opened tab should be foreground tab`
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [aExpectedActiveElement, aDescription],
+ (expectedActiveElement, description) => {
+ if (expectedActiveElement != null) {
+ if (expectedActiveElement == "iframe") {
+ // Check only whether the subdocument gets focus.
+ return;
+ }
+ Assert.equal(
+ content.document.activeElement,
+ content.document.querySelector(expectedActiveElement),
+ `${description}: Active element should be the result of querySelector("${expectedActiveElement}")`
+ );
+ } else {
+ Assert.deepEqual(
+ content.document.activeElement,
+ new content.window.Object(null),
+ `${description}: No element should be active`
+ );
+ }
+ }
+ );
+ gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ await SimpleTest.promiseFocus(browser);
+ if (aExpectedActiveElement == "iframe") {
+ await SpecialPowers.spawn(browser, [aDescription], description => {
+ // XXX Due to no `Assert#todo`, this checks opposite result.
+ Assert.ok(
+ !content.document
+ .querySelector("iframe")
+ .contentDocument.hasFocus(),
+ `TODO: ${description}: The subdocument should have focus when the tab gets foreground`
+ );
+ });
+ }
+ }
+ await doTest({
+ aInnerHTML: `<div style="height: 10000px;" onmousedown="window.open('https://example.com/', '_blank')">Click to open new tab</div>`,
+ aDescription: "Clicking non-focusable <div> with middle mouse button",
+ aExpectedActiveElement: null,
+ });
+ await doTest({
+ aInnerHTML: `<button style="height: 10000px; width: 100%" onmousedown="window.open('https://example.com/', '_blank')">Click to open new tab</button>`,
+ aDescription: `Clicking focusable <button> with middle mouse button`,
+ aExpectedActiveElement: navigator.platform.includes("Mac")
+ ? null
+ : "button",
+ });
+ await doTest({
+ aInnerHTML: `<iframe style="height: 90vh; width: 90vw" srcdoc="<div onmousedown='window.open(\`https://example.com/\`, \`_blank\`)' style='width: 100%; height: 10000px'>Click to open new tab"></iframe>`,
+ aDescription: `Clicking non-focusable <div> in <iframe> with middle mouse button`,
+ aExpectedActiveElement: "iframe",
+ });
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_charsetMenu_disable_on_ascii.js b/toolkit/content/tests/browser/browser_charsetMenu_disable_on_ascii.js
new file mode 100644
index 0000000000..2982a63141
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_charsetMenu_disable_on_ascii.js
@@ -0,0 +1,18 @@
+/* Test that the charset menu is properly enabled when swapping browsers. */
+add_task(async function test() {
+ function charsetMenuEnabled() {
+ return !document
+ .getElementById("repair-text-encoding")
+ .hasAttribute("disabled");
+ }
+
+ const PAGE = "data:text/html,<!DOCTYPE html><body>ASCII-only";
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: PAGE,
+ waitForStateStop: true,
+ });
+ ok(!charsetMenuEnabled(), "should have a charset menu here");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_charsetMenu_swapBrowsers.js b/toolkit/content/tests/browser/browser_charsetMenu_swapBrowsers.js
new file mode 100644
index 0000000000..9ea8fccf29
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_charsetMenu_swapBrowsers.js
@@ -0,0 +1,39 @@
+/* Test that the charset menu is properly enabled when swapping browsers. */
+add_task(async function test() {
+ function charsetMenuEnabled() {
+ return !document
+ .getElementById("repair-text-encoding")
+ .hasAttribute("disabled");
+ }
+
+ const PAGE =
+ "data:text/html;charset=windows-1252,<!DOCTYPE html><body>hello %e4";
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: PAGE,
+ });
+ await BrowserTestUtils.waitForMutationCondition(
+ document.getElementById("repair-text-encoding"),
+ { attributes: true },
+ charsetMenuEnabled
+ );
+ ok(charsetMenuEnabled(), "should have a charset menu here");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
+ ok(!charsetMenuEnabled(), "about:blank shouldn't have a charset menu");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ let swapped = BrowserTestUtils.waitForEvent(
+ tab2.linkedBrowser,
+ "SwapDocShells"
+ );
+
+ // NB: Closes tab1.
+ gBrowser.swapBrowsersAndCloseOther(tab2, tab1);
+ await swapped;
+
+ ok(charsetMenuEnabled(), "should have a charset after the swap");
+
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js b/toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js
new file mode 100644
index 0000000000..b47f72ed5b
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_click_event_during_autoscrolling.js
@@ -0,0 +1,577 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ ok(browser.isRemoteBrowser, "This test passes only in e10s mode");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML =
+ '<div style="height: 10000px;"></div>';
+ content.document.documentElement.scrollTop = 500;
+ content.document.documentElement.scrollTop; // Flush layout.
+ // Prevent to open context menu when testing the secondary button click.
+ content.window.addEventListener(
+ "contextmenu",
+ event => event.preventDefault(),
+ { capture: true }
+ );
+ });
+
+ function promiseFlushLayoutInContent() {
+ return SpecialPowers.spawn(browser, [], () => {
+ content.document.documentElement.scrollTop; // Flush layout in the remote content.
+ });
+ }
+
+ function promiseContentTick() {
+ return SpecialPowers.spawn(browser, [], async () => {
+ await new Promise(r => {
+ content.requestAnimationFrame(() => {
+ content.requestAnimationFrame(r);
+ });
+ });
+ });
+ }
+
+ let autoScroller;
+ function promiseWaitForAutoScrollerOpen() {
+ if (autoScroller?.state == "open") {
+ info("The autoscroller has already been open");
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ { capture: true },
+ event => {
+ if (event.originalTarget.id != "autoscroller") {
+ return false;
+ }
+ autoScroller = event.originalTarget;
+ info('"popupshown" event is fired');
+ autoScroller.getBoundingClientRect(); // Flush layout of the autoscroller
+ return true;
+ }
+ );
+ }
+
+ function promiseWaitForAutoScrollerClosed() {
+ if (!autoScroller || autoScroller.state == "closed") {
+ info("The autoscroller has already been closed");
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(
+ autoScroller,
+ "popuphidden",
+ { capture: true },
+ () => {
+ info('"popuphidden" event is fired');
+ return true;
+ }
+ );
+ }
+
+ // Unfortunately, we cannot use synthesized mouse events for starting and
+ // stopping autoscrolling because they may run different path from user
+ // operation especially when there is a popup.
+
+ /**
+ * Instead of using `waitForContentEvent`, we use `addContentEventListener`
+ * for checking which events are fired because `waitForContentEvent` cannot
+ * detect redundant event since it's removed automatically at first event
+ * or timeout if the expected count is 0.
+ */
+ class ContentEventCounter {
+ constructor(aBrowser, aEventTypes) {
+ this.eventData = new Map();
+ for (let eventType of aEventTypes) {
+ const removeEventListener =
+ BrowserTestUtils.addContentEventListener(
+ aBrowser,
+ eventType,
+ () => {
+ let eventData = this.eventData.get(eventType);
+ eventData.count++;
+ },
+ { capture: true }
+ );
+ this.eventData.set(eventType, {
+ count: 0, // how many times the event fired.
+ removeEventListener, // function to remove the event listener.
+ });
+ }
+ }
+
+ getCountAndRemoveEventListener(aEventType) {
+ let eventData = this.eventData.get(aEventType);
+ if (eventData.removeEventListener) {
+ eventData.removeEventListener();
+ eventData.removeEventListener = null;
+ }
+ return eventData.count;
+ }
+
+ promiseMouseEvents(aEventTypes, aMessage) {
+ let needsToWait = [];
+ for (const eventType of aEventTypes) {
+ let eventData = this.eventData.get(eventType);
+ if (eventData.count > 0) {
+ info(`${aMessage}: Waiting "${eventType}" event in content...`);
+ needsToWait.push(
+ // Let's use `waitForCondition` here. "timeout" is not worthwhile
+ // to debug this test. We want clearer failure log.
+ TestUtils.waitForCondition(
+ () => eventData.count > 0,
+ `${aMessage}: "${eventType}" should be fired, but timed-out`
+ )
+ );
+ break;
+ }
+ }
+ return Promise.all(needsToWait);
+ }
+ }
+
+ await SpecialPowers.pushPrefEnv({ set: [["middlemouse.paste", true]] });
+ await (async function testMouseEventsAtStartingAutoScrolling() {
+ info(
+ "Waiting autoscroller popup for testing mouse events at starting autoscrolling"
+ );
+ await promiseFlushLayoutInContent();
+ let eventsInContent = new ContentEventCounter(browser, [
+ "click",
+ "auxclick",
+ "mousedown",
+ "mouseup",
+ "paste",
+ ]);
+ // Ensure that the event listeners added in the content with accessing
+ // the remote content.
+ await promiseFlushLayoutInContent();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: browser,
+ atCenter: true,
+ });
+ const waitForOpenAutoScroll = promiseWaitForAutoScrollerOpen();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await waitForOpenAutoScroll;
+ // In the wild, native "mouseup" event occurs after the popup is open.
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await promiseFlushLayoutInContent();
+ await promiseContentTick();
+ await eventsInContent.promiseMouseEvents(
+ ["mouseup"],
+ "At starting autoscrolling"
+ );
+ for (let eventType of ["click", "auxclick", "paste"]) {
+ is(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 0,
+ `"${eventType}" event shouldn't be fired in the content when a middle click starts autoscrolling`
+ );
+ }
+ for (let eventType of ["mousedown", "mouseup"]) {
+ is(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 1,
+ `"${eventType}" event should be fired in the content when a middle click starts autoscrolling`
+ );
+ }
+ info("Waiting autoscroller close for preparing the following tests");
+ let waitForAutoScrollEnd = promiseWaitForAutoScrollerClosed();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await waitForAutoScrollEnd;
+ })();
+
+ if (
+ // Bug 1693240: We don't support setting modifiers while posting a mouse event on Windows.
+ !navigator.platform.includes("Win") &&
+ // Bug 1693237: We don't support setting modifiers on Android.
+ !navigator.appVersion.includes("Android") &&
+ // In Headless mode, modifiers are not supported by this kind of APIs.
+ !Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoscroll.prevent_to_start.shiftKey", true],
+ ["general.autoscroll.prevent_to_start.altKey", true],
+ ["general.autoscroll.prevent_to_start.ctrlKey", true],
+ ["general.autoscroll.prevent_to_start.metaKey", true],
+ ],
+ });
+ for (const modifier of ["Shift", "Control", "Alt", "Meta"]) {
+ if (modifier == "Meta" && !navigator.platform.includes("Mac")) {
+ continue; // Delete this after fixing bug 1232918.
+ }
+ await (async function modifiersPreventToStartAutoScrolling() {
+ info(
+ `Waiting to check not to open autoscroller popup with middle button click with ${modifier}`
+ );
+ await promiseFlushLayoutInContent();
+ let eventsInContent = new ContentEventCounter(browser, [
+ "click",
+ "auxclick",
+ "mousedown",
+ "mouseup",
+ "paste",
+ ]);
+ // Ensure that the event listeners added in the content with accessing
+ // the remote content.
+ await promiseFlushLayoutInContent();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: browser,
+ atCenter: true,
+ });
+ info(
+ `Waiting to MozAutoScrollNoStart event for the middle button click with ${modifier}`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ modifiers: {
+ altKey: modifier == "Alt",
+ ctrlKey: modifier == "Control",
+ metaKey: modifier == "Meta",
+ shiftKey: modifier == "Shift",
+ },
+ });
+ try {
+ await TestUtils.waitForCondition(
+ () => autoScroller?.state == "open",
+ `Waiting to check not to open autoscroller popup with ${modifier}`,
+ 100,
+ 10
+ );
+ ok(
+ false,
+ `The autoscroller popup shouldn't be opened by middle click with ${modifier}`
+ );
+ } catch (ex) {
+ ok(
+ true,
+ `The autoscroller popup was not open as expected after middle click with ${modifier}`
+ );
+ }
+ // In the wild, native "mouseup" event occurs after the popup is open.
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await promiseFlushLayoutInContent();
+ await promiseContentTick();
+ await eventsInContent.promiseMouseEvents(
+ ["paste"],
+ `At middle clicking with ${modifier}`
+ );
+ for (let eventType of [
+ "mousedown",
+ "mouseup",
+ "click",
+ "auxclick",
+ "paste",
+ ]) {
+ is(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 1,
+ `"${eventType}" event should be fired in the content when a middle click with ${modifier}`
+ );
+ }
+ info(
+ "Waiting autoscroller close for preparing the following tests"
+ );
+ })();
+ }
+ }
+
+ async function doTestMouseEventsAtStoppingAutoScrolling({
+ aButton = 0,
+ aClickOutsideAutoScroller = false,
+ aDescription = "Unspecified",
+ }) {
+ info(
+ `Starting autoscrolling for testing to stop autoscrolling with ${aDescription}`
+ );
+ await promiseFlushLayoutInContent();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: browser,
+ atCenter: true,
+ });
+ const waitForOpenAutoScroll = promiseWaitForAutoScrollerOpen();
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ // In the wild, native "mouseup" event occurs after the popup is open.
+ await waitForOpenAutoScroll;
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: 1, // middle button
+ });
+ await promiseFlushLayoutInContent();
+ // Just to be sure, wait for a tick for wait APZ stable.
+ await TestUtils.waitForTick();
+
+ let eventsInContent = new ContentEventCounter(browser, [
+ "click",
+ "auxclick",
+ "mousedown",
+ "mouseup",
+ "paste",
+ "contextmenu",
+ ]);
+ // Ensure that the event listeners added in the content with accessing
+ // the remote content.
+ await promiseFlushLayoutInContent();
+
+ aDescription = `Stop autoscrolling with ${aDescription}`;
+ info(
+ `${aDescription}: Synthesizing primary mouse button event on the autoscroller`
+ );
+ const autoScrollerRect = autoScroller.getOuterScreenRect();
+ info(
+ `${aDescription}: autoScroller: { left: ${autoScrollerRect.left}, top: ${autoScrollerRect.top}, width: ${autoScrollerRect.width}, height: ${autoScrollerRect.height} }`
+ );
+ const waitForCloseAutoScroller = promiseWaitForAutoScrollerClosed();
+ if (aClickOutsideAutoScroller) {
+ info(
+ `${aDescription}: Synthesizing mousemove move cursor outside the autoscroller...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: autoScroller,
+ offsetX: -10,
+ offsetY: -10,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ info(
+ `${aDescription}: Synthesizing mousedown to stop autoscrolling...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: autoScroller,
+ offsetX: -10,
+ offsetY: -10,
+ button: aButton,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ } else {
+ info(
+ `${aDescription}: Synthesizing mousemove move cursor onto the autoscroller...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: autoScroller,
+ atCenter: true,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ info(
+ `${aDescription}: Synthesizing mousedown to stop autoscrolling...`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ target: autoScroller,
+ atCenter: true,
+ button: aButton,
+ elementOnWidget: browser, // use widget for the parent window of the autoscroller
+ });
+ }
+ // In the wild, native "mouseup" event occurs after the popup is closed.
+ await waitForCloseAutoScroller;
+ info(
+ `${aDescription}: Synthesizing mouseup event for preceding mousedown which is for stopping autoscrolling`
+ );
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ target: browser,
+ atCenter: true,
+ button: aButton,
+ });
+ await promiseFlushLayoutInContent();
+ await promiseContentTick();
+ await eventsInContent.promiseMouseEvents(
+ aButton != 2 ? ["mouseup"] : ["mouseup", "contextmenu"],
+ aDescription
+ );
+ is(
+ autoScroller.state,
+ "closed",
+ `${aDescription}: The autoscroller should've been closed`
+ );
+ // - On macOS, when clicking outside autoscroller, nsChildView
+ // intentionally blocks both "mousedown" and "mouseup" events in the
+ // case of the primary button click, and only "mousedown" for the
+ // middle button when the "mousedown". I'm not sure how it should work
+ // on macOS for conforming to the platform manner. Note that autoscroll
+ // isn't available on the other browsers on macOS. So, there is no
+ // reference, but for consistency between platforms, it may be better
+ // to ignore the platform manner.
+ // - On Windows, when clicking outside autoscroller, nsWindow
+ // intentionally blocks only "mousedown" events for the primary button
+ // and the middle button. But this behavior is different from Chrome
+ // so that we need to fix this in the future.
+ // - On Linux, when clicking outside autoscroller, nsWindow
+ // intentionally blocks only "mousedown" events for any buttons. But
+ // on Linux, autoscroll isn't available by the default settings. So,
+ // not so urgent, but should be fixed in the future for consistency
+ // between platforms and compatibility with Chrome on Windows.
+ const rollingUpPopupConsumeMouseDown =
+ aClickOutsideAutoScroller &&
+ (aButton != 2 || navigator.platform.includes("Linux"));
+ const rollingUpPopupConsumeMouseUp =
+ aClickOutsideAutoScroller &&
+ aButton == 0 &&
+ navigator.platform.includes("Mac");
+ const checkFuncForClick =
+ aClickOutsideAutoScroller &&
+ aButton == 2 &&
+ !navigator.platform.includes("Linux")
+ ? todo_is
+ : is;
+ for (let eventType of ["click", "auxclick"]) {
+ checkFuncForClick(
+ eventsInContent.getCountAndRemoveEventListener(eventType),
+ 0,
+ `${aDescription}: "${eventType}" event shouldn't be fired in the remote content`
+ );
+ }
+ is(
+ eventsInContent.getCountAndRemoveEventListener("paste"),
+ 0,
+ `${aDescription}: "paste" event shouldn't be fired in the remote content`
+ );
+ const checkFuncForMouseDown = rollingUpPopupConsumeMouseDown
+ ? todo_is
+ : is;
+ checkFuncForMouseDown(
+ eventsInContent.getCountAndRemoveEventListener("mousedown"),
+ 1,
+ `${aDescription}: "mousedown" event should be fired in the remote content`
+ );
+ const checkFuncForMouseUp = rollingUpPopupConsumeMouseUp ? todo_is : is;
+ checkFuncForMouseUp(
+ eventsInContent.getCountAndRemoveEventListener("mouseup"),
+ 1,
+ `${aDescription}: "mouseup" event should be fired in the remote content`
+ );
+ const checkFuncForContextMenu =
+ aButton == 2 &&
+ aClickOutsideAutoScroller &&
+ navigator.platform.includes("Linux")
+ ? todo_is
+ : is;
+ checkFuncForContextMenu(
+ eventsInContent.getCountAndRemoveEventListener("contextmenu"),
+ aButton == 2 ? 1 : 0,
+ `${aDescription}: "contextmenu" event should${
+ aButton != 2 ? " not" : ""
+ } be fired in the remote content`
+ );
+
+ const promiseClickEvent = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "click",
+ {
+ capture: true,
+ }
+ );
+ await promiseFlushLayoutInContent();
+ info(`${aDescription}: Waiting for click event in the remote content`);
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "click",
+ target: browser,
+ atCenter: true,
+ });
+ await promiseClickEvent;
+ ok(
+ true,
+ `${aDescription}: click event is fired in the remote content after stopping autoscrolling`
+ );
+ }
+
+ // Clicking the primary button to stop autoscrolling.
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 0,
+ aClickOutsideAutoScroller: false,
+ aDescription: "a primary button click on autoscroller",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 0,
+ aClickOutsideAutoScroller: true,
+ aDescription: "a primary button click outside autoscroller",
+ });
+
+ // Clicking the secondary button to stop autoscrolling.
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 2,
+ aClickOutsideAutoScroller: false,
+ aDescription: "a secondary button click on autoscroller",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 2,
+ aClickOutsideAutoScroller: true,
+ aDescription: "a secondary button click outside autoscroller",
+ });
+
+ // Clicking the middle button to stop autoscrolling.
+ await SpecialPowers.pushPrefEnv({ set: [["middlemouse.paste", true]] });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: false,
+ aDescription:
+ "a middle button click on autoscroller (middle click paste enabled)",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: true,
+ aDescription:
+ "a middle button click outside autoscroller (middle click paste enabled)",
+ });
+ await SpecialPowers.pushPrefEnv({ set: [["middlemouse.paste", false]] });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: false,
+ aDescription:
+ "a middle button click on autoscroller (middle click paste disabled)",
+ });
+ await doTestMouseEventsAtStoppingAutoScrolling({
+ aButton: 1,
+ aClickOutsideAutoScroller: true,
+ aDescription:
+ "a middle button click outside autoscroller (middle click paste disabled)",
+ });
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_contentTitle.js b/toolkit/content/tests/browser/browser_contentTitle.js
new file mode 100644
index 0000000000..e8330eca0f
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_contentTitle.js
@@ -0,0 +1,17 @@
+var url =
+ "https://example.com/browser/toolkit/content/tests/browser/file_contentTitle.html";
+
+add_task(async function () {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url));
+ await BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "TestLocationChange",
+ true,
+ null,
+ true
+ );
+
+ is(gBrowser.contentTitle, "Test Page", "Should have the right title.");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_content_url_annotation.js b/toolkit/content/tests/browser/browser_content_url_annotation.js
new file mode 100644
index 0000000000..29332ce036
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_content_url_annotation.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global Services, requestLongerTimeout, TestUtils, BrowserTestUtils,
+ ok, info, dump, is, Ci, Cu, Components, ctypes,
+ gBrowser, add_task, addEventListener, removeEventListener, ContentTask */
+
+"use strict";
+
+// Running this test in ASAN is slow.
+requestLongerTimeout(2);
+
+/**
+ * Removes a file from a directory. This is a no-op if the file does not
+ * exist.
+ *
+ * @param directory
+ * The nsIFile representing the directory to remove from.
+ * @param filename
+ * A string for the file to remove from the directory.
+ */
+function removeFile(directory, filename) {
+ let file = directory.clone();
+ file.append(filename);
+ if (file.exists()) {
+ file.remove(false);
+ }
+}
+
+/**
+ * Returns the directory where crash dumps are stored.
+ *
+ * @return nsIFile
+ */
+function getMinidumpDirectory() {
+ let dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dir.append("minidumps");
+ return dir;
+}
+
+/**
+ * Checks that the URL is correctly annotated on a content process crash.
+ */
+add_task(async function test_content_url_annotation() {
+ let url =
+ "https://example.com/browser/toolkit/content/tests/browser/file_redirect.html";
+ let redirect_url =
+ "https://example.com/browser/toolkit/content/tests/browser/file_redirect_to.html";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ },
+ async function (browser) {
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+
+ // file_redirect.html should send us to file_redirect_to.html
+ let promise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "RedirectDone",
+ true,
+ null,
+ true
+ );
+ BrowserTestUtils.loadURIString(browser, url);
+ await promise;
+
+ // Crash the tab
+ let annotations = await BrowserTestUtils.crashFrame(browser);
+
+ ok("URL" in annotations, "annotated a URL");
+ is(
+ annotations.URL,
+ redirect_url,
+ "Should have annotated the URL after redirect"
+ );
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_crash_previous_frameloader.js b/toolkit/content/tests/browser/browser_crash_previous_frameloader.js
new file mode 100644
index 0000000000..0fa2f17912
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_crash_previous_frameloader.js
@@ -0,0 +1,131 @@
+"use strict";
+
+/**
+ * Returns the id of the crash minidump.
+ *
+ * @param subject (nsISupports)
+ * The subject passed through the ipc:content-shutdown
+ * observer notification when a content process crash has
+ * occurred.
+ * @returns {String} The crash dump id.
+ */
+function getCrashDumpId(subject) {
+ Assert.ok(
+ subject instanceof Ci.nsIPropertyBag2,
+ "Subject needs to be a nsIPropertyBag2 to clean up properly"
+ );
+
+ return subject.getPropertyAsAString("dumpID");
+}
+
+/**
+ * Cleans up the .dmp and .extra file from a crash.
+ *
+ * @param id {String} The crash dump id.
+ */
+function cleanUpMinidump(id) {
+ let dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dir.append("minidumps");
+
+ let file = dir.clone();
+ file.append(id + ".dmp");
+ file.remove(true);
+
+ file = dir.clone();
+ file.append(id + ".extra");
+ file.remove(true);
+}
+
+/**
+ * This test ensures that if a remote frameloader crashes after
+ * the frameloader owner swaps it out for a new frameloader,
+ * that a oop-browser-crashed event is not sent to the new
+ * frameloader's browser element.
+ */
+add_task(async function test_crash_in_previous_frameloader() {
+ // 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);
+
+ if (!gMultiProcessBrowser) {
+ Assert.ok(false, "This test should only be run in multi-process mode.");
+ return;
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://example.com",
+ },
+ async function (browser) {
+ // First, sanity check...
+ Assert.ok(
+ browser.isRemoteBrowser,
+ "This browser needs to be remote if this test is going to " +
+ "work properly."
+ );
+
+ // We will wait for the oop-browser-crashed event to have
+ // a chance to appear. That event is fired when RemoteTabs
+ // are destroyed, and that occurs _before_ ContentParents
+ // are destroyed, so we'll wait on the ipc:content-shutdown
+ // observer notification, which is fired when a ContentParent
+ // goes away. After we see this notification, oop-browser-crashed
+ // events should have fired.
+ let contentProcessGone = TestUtils.topicObserved("ipc:content-shutdown");
+ let sawTabCrashed = false;
+ let onTabCrashed = () => {
+ sawTabCrashed = true;
+ };
+
+ browser.addEventListener("oop-browser-crashed", onTabCrashed);
+
+ // The name of the game is to cause a crash in a remote browser,
+ // and then immediately swap out the browser for a non-remote one.
+ await SpecialPowers.spawn(browser, [], function () {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+
+ let dies = function () {
+ ChromeUtils.privateNoteIntentionalCrash();
+ let zero = new ctypes.intptr_t(8);
+ let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+ badptr.contents;
+ };
+
+ // When the parent flips the remoteness of the browser, the
+ // page should receive the pagehide event, which we'll then
+ // use to crash the frameloader.
+ docShell.chromeEventHandler.addEventListener("pagehide", function () {
+ dump("\nEt tu, Brute?\n");
+ dies();
+ });
+ });
+
+ gBrowser.updateBrowserRemoteness(browser, {
+ remoteType: E10SUtils.NOT_REMOTE,
+ });
+ info("Waiting for content process to go away.");
+ let [subject /* , data */] = await contentProcessGone;
+
+ // If we don't clean up the minidump, the harness will
+ // complain.
+ let dumpID = getCrashDumpId(subject);
+
+ Assert.ok(dumpID, "There should be a dumpID");
+ if (dumpID) {
+ await Services.crashmanager.ensureCrashIsPresent(dumpID);
+ cleanUpMinidump(dumpID);
+ }
+
+ info("Content process is gone!");
+ Assert.ok(
+ !sawTabCrashed,
+ "Should not have seen the oop-browser-crashed event."
+ );
+ browser.removeEventListener("oop-browser-crashed", onTabCrashed);
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_default_audio_filename.js b/toolkit/content/tests/browser/browser_default_audio_filename.js
new file mode 100644
index 0000000000..c32dda6878
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_default_audio_filename.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+});
+
+/**
+ * TestCase for bug 789550
+ * <https://bugzilla.mozilla.org/show_bug.cgi?id=789550>
+ */
+add_task(async function () {
+ const DATA_AUDIO_URL = await fetch(
+ getRootDirectory(gTestPath) + "audio_file.txt"
+ ).then(async response => response.text());
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: DATA_AUDIO_URL,
+ },
+ async function (browser) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "video",
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+
+ await popupShownPromise;
+
+ let showFilePickerPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ is(
+ fp.defaultString.startsWith("Untitled"),
+ true,
+ "File name should be Untitled"
+ );
+ resolve();
+ };
+ });
+
+ // Select "Save Audio As" option from context menu
+ var saveImageAsCommand = document.getElementById("context-saveaudio");
+ saveImageAsCommand.doCommand();
+
+ await showFilePickerPromise;
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+ }
+ );
+});
+
+/**
+ * TestCase for bug 789550
+ * <https://bugzilla.mozilla.org/show_bug.cgi?id=789550>
+ */
+add_task(async function () {
+ const DATA_AUDIO_URL = await fetch(
+ getRootDirectory(gTestPath) + "audio_file.txt"
+ ).then(async response => response.text());
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: DATA_AUDIO_URL,
+ },
+ async function (browser) {
+ let showFilePickerPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ is(
+ fp.defaultString.startsWith("Untitled"),
+ true,
+ "File name should be Untitled"
+ );
+ resolve();
+ };
+ });
+
+ saveBrowser(browser);
+
+ await showFilePickerPromise;
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_default_image_filename_redirect.js b/toolkit/content/tests/browser/browser_default_image_filename_redirect.js
new file mode 100644
index 0000000000..a3fdd2d19e
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_default_image_filename_redirect.js
@@ -0,0 +1,53 @@
+/**
+ * TestCase for bug 1406253
+ * <https://bugzilla.mozilla.org/show_bug.cgi?id=1406253>
+ *
+ * Load firebird.png, redirect it to doggy.png, and verify the filename is
+ * doggy.png in file picker dialog.
+ */
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+add_task(async function () {
+ // This URL will redirect to doggy.png.
+ const URL_FIREBIRD =
+ "http://mochi.test:8888/browser/toolkit/content/tests/browser/firebird.png";
+
+ await BrowserTestUtils.withNewTab(URL_FIREBIRD, async function (browser) {
+ // Click image to show context menu.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "img",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShownPromise;
+
+ // Prepare mock file picker.
+ let showFilePickerPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = fp => resolve(fp.defaultString);
+ });
+ registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+ });
+
+ // Select "Save Image As" option from context menu
+ var saveImageAsCommand = document.getElementById("context-saveimage");
+ saveImageAsCommand.doCommand();
+
+ let filename = await showFilePickerPromise;
+ is(filename, "doggy.png", "Verify image filename.");
+
+ // Close context menu.
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+ });
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_iframe.js b/toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_iframe.js
new file mode 100644
index 0000000000..63be8b01a0
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_iframe.js
@@ -0,0 +1,91 @@
+/**
+ * After the tab has been visited, all media should be able to start playing.
+ * This test is used to ensure that playing media from a cross-origin iframe in
+ * a tab that has been already visited won't fail.
+ */
+"use strict";
+
+add_task(async function setupTestEnvironment() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", 0],
+ ["media.block-autoplay-until-in-foreground", true],
+ ],
+ });
+});
+
+add_task(async function testCrossOriginIframeShouldBeAbleToStart() {
+ info("Create a new foreground tab");
+ const originalTab = gBrowser.selectedTab;
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+
+ info("As tab has been visited, media should be allowed to start");
+ const MEDIA_FILE = "gizmo.mp4";
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [getTestWebBasedURL(MEDIA_FILE)],
+ async url => {
+ let vid = content.document.createElement("video");
+ vid.src = url;
+ ok(
+ await vid.play().then(
+ _ => true,
+ _ => false
+ ),
+ "video started playing"
+ );
+ }
+ );
+
+ info("Make the tab to background");
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+
+ info(
+ "As tab has been visited, a cross-origin iframe should be able to start media"
+ );
+ const IFRAME_FILE = "file_iframe_media.html";
+ await createIframe(
+ tab.linkedBrowser,
+ getTestWebBasedURL(IFRAME_FILE, { crossOrigin: true })
+ );
+ await ensureCORSIframeCanStartPlayingMedia(tab.linkedBrowser);
+
+ info("Remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Following are helper functions
+ */
+function createIframe(browser, iframeUrl) {
+ return SpecialPowers.spawn(browser, [iframeUrl], async url => {
+ info(`Create iframe and wait until it finishes loading`);
+ const iframe = content.document.createElement("iframe");
+ const iframeLoaded = new Promise(r => (iframe.onload = r));
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ await iframeLoaded;
+ });
+}
+
+function ensureCORSIframeCanStartPlayingMedia(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ info(`check if media in iframe can start playing`);
+ const iframe = content.document.querySelector("iframe");
+ if (!iframe) {
+ ok(false, `can not get the iframe!`);
+ return;
+ }
+ const playPromise = new Promise(r => {
+ content.onmessage = event => {
+ is(event.data, "played", `started playing media from CORS iframe`);
+ r();
+ };
+ });
+ iframe.contentWindow.postMessage("play", "*");
+ await playPromise;
+ });
+}
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_navigation.js b/toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_navigation.js
new file mode 100644
index 0000000000..03e008b9da
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_cross_origin_navigation.js
@@ -0,0 +1,65 @@
+/**
+ * This test is used to ensure that media would still be able to play even if
+ * the page has been navigated to a cross-origin url.
+ */
+"use strict";
+
+add_task(async function setupTestEnvironment() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", 0],
+ ["media.block-autoplay-until-in-foreground", true],
+ ],
+ });
+});
+
+add_task(async function testCrossOriginNavigation() {
+ info("Create a new foreground tab");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+
+ info("As tab has been visited, media should be allowed to start");
+ const MEDIA_FILE = "gizmo.mp4";
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [getTestWebBasedURL(MEDIA_FILE)],
+ async url => {
+ let vid = content.document.createElement("video");
+ vid.src = url;
+ ok(
+ await vid.play().then(
+ _ => true,
+ _ => false
+ ),
+ "video started playing"
+ );
+ }
+ );
+
+ info("Navigate to a cross-origin video file");
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ getTestWebBasedURL(MEDIA_FILE, { crossOrigin: true })
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info(
+ "As tab has been visited, a cross-origin media should also be able to start"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ let vid = content.document.querySelector("video");
+ ok(vid, "Video exists");
+ ok(
+ await vid.play().then(
+ _ => true,
+ _ => false
+ ),
+ "video started playing"
+ );
+ });
+
+ info("Remove tab");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_media.js b/toolkit/content/tests/browser/browser_delay_autoplay_media.js
new file mode 100644
index 0000000000..62a56157ba
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_media.js
@@ -0,0 +1,145 @@
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_multipleAudio.html";
+const PAGE_NO_AUTOPLAY =
+ "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html";
+
+function check_audio_paused(browser, shouldBePaused) {
+ return SpecialPowers.spawn(browser, [shouldBePaused], shouldBePaused => {
+ var autoPlay = content.document.getElementById("autoplay");
+ if (!autoPlay) {
+ ok(false, "Can't get the audio element!");
+ }
+ is(
+ autoPlay.paused,
+ shouldBePaused,
+ "autoplay audio should " + (!shouldBePaused ? "not " : "") + "be paused."
+ );
+ });
+}
+
+add_task(async function setup_test_preference() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.useAudioChannelService.testing", true],
+ ["media.block-autoplay-until-in-foreground", true],
+ ],
+ });
+});
+
+function set_media_autoplay() {
+ let audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ return;
+ }
+ audio.autoplay = true;
+}
+
+add_task(async function delay_media_with_autoplay_keyword() {
+ info("- open new background tab -");
+ const tab = BrowserTestUtils.addTab(window.gBrowser, PAGE_NO_AUTOPLAY);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- set media's autoplay property -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], set_media_autoplay);
+
+ info("- should delay autoplay media -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- switch tab to foreground -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- media should be resumed because tab has been visited -");
+ await waitForTabPlayingEvent(tab, true);
+ await waitForTabBlockEvent(tab, false);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function delay_media_with_play_invocation() {
+ info("- open new background tab1 -");
+ let tab1 = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab1.linkedBrowser);
+
+ info("- should delay autoplay media for non-visited tab1 -");
+ await waitForTabBlockEvent(tab1, true);
+
+ info("- open new background tab2 -");
+ let tab2 = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
+
+ info("- should delay autoplay for non-visited tab2 -");
+ await waitForTabBlockEvent(tab2, true);
+
+ info("- switch to tab1 -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab1);
+
+ info("- media in tab1 should be unblocked because the tab was visited -");
+ await waitForTabPlayingEvent(tab1, true);
+ await waitForTabBlockEvent(tab1, false);
+
+ info("- open another new foreground tab3 -");
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ info("- should still play media from tab1 -");
+ await waitForTabPlayingEvent(tab1, true);
+
+ info("- should still block media from tab2 -");
+ await waitForTabPlayingEvent(tab2, false);
+ await waitForTabBlockEvent(tab2, true);
+
+ info("- remove tabs -");
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
+
+add_task(async function resume_delayed_media_when_enable_blocking_autoplay() {
+ // Disable autoplay and verify that when a tab is opened in the
+ // background and has had its playback start delayed, resuming via the audio
+ // tab indicator overrides the autoplay blocking logic.
+ //
+ // Clicking "play" on the audio tab indicator should always start playback
+ // in that tab, even if it's in an autoplay-blocked origin.
+ //
+ // Also test that that this block-autoplay logic override doesn't survive
+ // a new document being loaded into the tab; the new document should have
+ // to satisfy the autoplay requirements on its own.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ],
+ });
+
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- should block autoplay for non-visited tab -");
+ await waitForTabBlockEvent(tab, true);
+ await check_audio_paused(tab.linkedBrowser, true);
+ tab.linkedBrowser.resumeMedia();
+
+ info("- should not block media from tab -");
+ await waitForTabPlayingEvent(tab, true);
+ await check_audio_paused(tab.linkedBrowser, false);
+
+ info(
+ "- check that loading a new URI in page clears gesture activation status -"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- should block autoplay again as gesture activation status cleared -");
+ await check_audio_paused(tab.linkedBrowser, true);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+
+ // Clear the block-autoplay pref.
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_media_pausedAfterPlay.js b/toolkit/content/tests/browser/browser_delay_autoplay_media_pausedAfterPlay.js
new file mode 100644
index 0000000000..09dc5f9691
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_media_pausedAfterPlay.js
@@ -0,0 +1,121 @@
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html";
+
+add_task(async function setup_test_preference() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.useAudioChannelService.testing", true],
+ ["media.block-autoplay-until-in-foreground", true],
+ ],
+ });
+});
+
+/**
+ * When media starts in an unvisited tab, we would delay its playback and resume
+ * media playback when the tab goes to foreground first time. There are two test
+ * cases are used to check different situations.
+ *
+ * The first one is used to check if the delayed media has been paused before
+ * the tab goes to foreground. Then, when the tab goes to foreground, media
+ * should still be paused.
+ *
+ * The second one is used to check if the delayed media has been paused, but
+ * it eventually was started again before the tab goes to foreground. Then, when
+ * the tab goes to foreground, media should be resumed.
+ */
+add_task(async function testShouldNotResumePausedMedia() {
+ info("- open new background tab and wait until it finishes loading -");
+ const tab = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- play media and then immediately pause it -");
+ await doPlayThenPauseOnMedia(tab);
+
+ info("- show delay media playback icon on tab -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- selecting tab as foreground tab would resume the tab -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- resuming tab should dismiss delay autoplay icon -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- paused media should still be paused -");
+ await checkAudioPauseState(tab, true /* should be paused */);
+
+ info("- paused media won't generate tab playing icon -");
+ await waitForTabPlayingEvent(tab, false);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testShouldResumePlayedMedia() {
+ info("- open new background tab and wait until it finishes loading -");
+ const tab = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- play media, pause it, then play it again -");
+ await doPlayPauseThenPlayOnMedia(tab);
+
+ info("- show delay media playback icon on tab -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- select tab as foreground tab -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- resuming tab should dismiss delay autoplay icon -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- played media should still be played -");
+ await checkAudioPauseState(tab, false /* should be played */);
+
+ info("- played media would generate tab playing icon -");
+ await waitForTabPlayingEvent(tab, true);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Helper functions.
+ */
+async function checkAudioPauseState(tab, expectedPaused) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expectedPaused],
+ expectedPaused => {
+ const audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ is(audio.paused, expectedPaused, "The pause state of audio is corret.");
+ }
+ );
+}
+
+async function doPlayThenPauseOnMedia(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ audio.play();
+ audio.pause();
+ });
+}
+
+async function doPlayPauseThenPlayOnMedia(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ audio.play();
+ audio.pause();
+ audio.play();
+ });
+}
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_multipleMedia.js b/toolkit/content/tests/browser/browser_delay_autoplay_multipleMedia.js
new file mode 100644
index 0000000000..6a81e508ea
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_multipleMedia.js
@@ -0,0 +1,77 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_multipleAudio.html";
+
+function check_autoplay_audio_onplay() {
+ let autoPlay = content.document.getElementById("autoplay");
+ if (!autoPlay) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ return new Promise((resolve, reject) => {
+ autoPlay.onplay = () => {
+ ok(false, "Should not receive play event!");
+ this.onplay = null;
+ reject();
+ };
+
+ autoPlay.pause();
+ autoPlay.play();
+
+ content.setTimeout(() => {
+ ok(true, "Doesn't receive play event when media was blocked.");
+ autoPlay.onplay = null;
+ resolve();
+ }, 1000);
+ });
+}
+
+function play_nonautoplay_audio_should_be_blocked() {
+ let nonAutoPlay = content.document.getElementById("nonautoplay");
+ if (!nonAutoPlay) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ nonAutoPlay.play();
+ ok(nonAutoPlay.paused, "The blocked audio can't be playback.");
+}
+
+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 block_multiple_media() {
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+ BrowserTestUtils.loadURIString(browser, PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("- tab should be blocked -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- autoplay media should be blocked -");
+ await SpecialPowers.spawn(browser, [], check_autoplay_audio_onplay);
+
+ info("- non-autoplay can't start playback when the tab is blocked -");
+ await SpecialPowers.spawn(
+ browser,
+ [],
+ play_nonautoplay_audio_should_be_blocked
+ );
+
+ info("- select tab as foreground tab -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- tab should be resumed -");
+ await waitForTabPlayingEvent(tab, true);
+ await waitForTabBlockEvent(tab, false);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_notInTreeAudio.js b/toolkit/content/tests/browser/browser_delay_autoplay_notInTreeAudio.js
new file mode 100644
index 0000000000..3e2df53648
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_notInTreeAudio.js
@@ -0,0 +1,66 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html";
+
+function check_audio_pause_state(expectPause) {
+ var audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ is(audio.paused, expectPause, "The pause state of audio is corret.");
+}
+
+function play_not_in_tree_audio() {
+ var audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ content.document.body.removeChild(audio);
+
+ // Add timeout to ensure the audio is removed from DOM tree.
+ content.setTimeout(function () {
+ info("Prepare to start playing audio.");
+ audio.play();
+ }, 1000);
+}
+
+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 block_not_in_tree_media() {
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- tab should not be blocked -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- check audio's playing state -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state);
+
+ info("- playing audio -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], play_not_in_tree_audio);
+
+ info("- tab should be blocked -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- switch tab -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- tab should be resumed -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- tab should be audible -");
+ await waitForTabPlayingEvent(tab, true);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_playAfterTabVisible.js b/toolkit/content/tests/browser/browser_delay_autoplay_playAfterTabVisible.js
new file mode 100644
index 0000000000..198deeb85f
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_playAfterTabVisible.js
@@ -0,0 +1,68 @@
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html";
+
+function check_audio_pause_state(expectPause) {
+ var audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ is(audio.paused, expectPause, "The pause state of audio is corret.");
+}
+
+function play_audio() {
+ var audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ audio.play();
+ return new Promise(resolve => {
+ audio.onplay = function () {
+ audio.onplay = null;
+ ok(true, "Audio starts playing.");
+ resolve();
+ };
+ });
+}
+
+add_task(async function setup_test_preference() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.useAudioChannelService.testing", true],
+ ["media.block-autoplay-until-in-foreground", true],
+ ],
+ });
+});
+
+/**
+ * This test is used for testing the visible tab which was not resumed yet.
+ * If the tab doesn't have any media component, it won't be resumed even it
+ * has already gone to foreground until we start audio.
+ */
+add_task(async function media_should_be_able_to_play_in_visible_tab() {
+ info("- open new background tab, and check tab's media pause state -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state);
+
+ info(
+ "- select tab as foreground tab, and tab's media should still be paused -"
+ );
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state);
+
+ info("- start audio in tab -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], play_audio);
+
+ info("- audio should be playing -");
+ await waitForTabBlockEvent(tab, false);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [false],
+ check_audio_pause_state
+ );
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_playMediaInMuteTab.js b/toolkit/content/tests/browser/browser_delay_autoplay_playMediaInMuteTab.js
new file mode 100644
index 0000000000..c333021697
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_playMediaInMuteTab.js
@@ -0,0 +1,97 @@
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.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 check_audio_volume_and_mute(expectedMute) {
+ var audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ let expectedVolume = expectedMute ? 0.0 : 1.0;
+ is(expectedVolume, audio.computedVolume, "Audio's volume is correct!");
+ is(expectedMute, audio.computedMuted, "Audio's mute state is correct!");
+}
+
+function check_audio_pause_state(expectedPauseState) {
+ var audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ is(audio.paused, expectedPauseState, "Audio is paused.");
+}
+
+function play_audio() {
+ var audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "Can't get the audio element!");
+ }
+
+ audio.play();
+ ok(audio.paused, "Can't play audio, because the tab was still blocked.");
+}
+
+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 unblock_icon_should_disapear_after_resume_tab() {
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- audio doesn't be started in beginning -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state);
+
+ info("- audio shouldn't be muted -");
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [false],
+ check_audio_volume_and_mute
+ );
+
+ info("- tab shouldn't display unblocking icon -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- mute tab -");
+ tab.linkedBrowser.mute();
+ ok(tab.linkedBrowser.audioMuted, "Audio should be muted now");
+
+ info("- try to start audio in background tab -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], play_audio);
+
+ info("- tab should display unblocking icon -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- select tab as foreground tab -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- audio shoule be muted, but not be blocked -");
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [true],
+ check_audio_volume_and_mute
+ );
+
+ info("- tab should not display unblocking icon -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_silentAudioTrack_media.js b/toolkit/content/tests/browser/browser_delay_autoplay_silentAudioTrack_media.js
new file mode 100644
index 0000000000..5124359bc4
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_silentAudioTrack_media.js
@@ -0,0 +1,63 @@
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_silentAudioTrack.html";
+
+async function click_unblock_icon(tab) {
+ let icon = tab.overlayIcon;
+
+ await hover_icon(icon, document.getElementById("tabbrowser-tab-tooltip"));
+ EventUtils.synthesizeMouseAtCenter(icon, { button: 0 });
+ leave_icon(icon);
+}
+
+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 unblock_icon_should_disapear_after_resume_tab() {
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank");
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- tab should display unblocking icon -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- select tab as foreground tab -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- should not display unblocking icon -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- should not display sound indicator icon -");
+ await waitForTabPlayingEvent(tab, false);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function should_not_show_sound_indicator_after_resume_tab() {
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank");
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- tab should display unblocking icon -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- click play tab icon -");
+ await click_unblock_icon(tab);
+
+ info("- should not display unblocking icon -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- should not display sound indicator icon -");
+ await waitForTabPlayingEvent(tab, false);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js b/toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js
new file mode 100644
index 0000000000..37011c852d
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js
@@ -0,0 +1,42 @@
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_webAudio.html";
+
+// The tab closing code leaves an uncaught rejection. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+ const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+ );
+ PromiseTestUtils.expectUncaughtRejection(/is no longer, usable/);
+}
+
+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 block_web_audio() {
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank");
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- tab should be blocked -");
+ await waitForTabBlockEvent(tab, true);
+
+ info("- switch tab -");
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+
+ info("- tab should be resumed -");
+ await waitForTabBlockEvent(tab, false);
+
+ info("- tab should be audible -");
+ await waitForTabPlayingEvent(tab, true);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_f7_caret_browsing.js b/toolkit/content/tests/browser/browser_f7_caret_browsing.js
new file mode 100644
index 0000000000..be6ae7d1f7
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_f7_caret_browsing.js
@@ -0,0 +1,367 @@
+var gListener = null;
+const kURL =
+ "data:text/html;charset=utf-8,Caret browsing is fun.<input id='in'>";
+
+const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
+const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
+const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
+
+var oldPrefs = {};
+for (let pref of [
+ kPrefShortcutEnabled,
+ kPrefWarnOnEnable,
+ kPrefCaretBrowsingOn,
+]) {
+ oldPrefs[pref] = Services.prefs.getBoolPref(pref);
+}
+
+Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+registerCleanupFunction(function () {
+ for (let pref of [
+ kPrefShortcutEnabled,
+ kPrefWarnOnEnable,
+ kPrefCaretBrowsingOn,
+ ]) {
+ Services.prefs.setBoolPref(pref, oldPrefs[pref]);
+ }
+});
+
+// NB: not using BrowserTestUtils.promiseAlertDialog here because there's no way to
+// undo waiting for a dialog. If we don't want the window to be opened, and
+// wait for it to verify that it indeed does not open, we need to be able to
+// then "stop" waiting so that when we next *do* want it to open, our "old"
+// listener doesn't fire and do things we don't want (like close the window...).
+let gCaretPromptOpeningObserver;
+function promiseCaretPromptOpened() {
+ return new Promise(resolve => {
+ function observer(subject, topic, data) {
+ info("Dialog opened.");
+ resolve(subject);
+ gCaretPromptOpeningObserver();
+ }
+ Services.obs.addObserver(observer, "common-dialog-loaded");
+ gCaretPromptOpeningObserver = () => {
+ Services.obs.removeObserver(observer, "common-dialog-loaded");
+ gCaretPromptOpeningObserver = () => {};
+ };
+ });
+}
+
+function hitF7() {
+ SimpleTest.executeSoon(() => EventUtils.synthesizeKey("KEY_F7"));
+}
+
+async function toggleCaretNoDialog(expected) {
+ let openedDialog = false;
+ promiseCaretPromptOpened().then(function (win) {
+ openedDialog = true;
+ win.close(); // This will eventually return focus here and allow the test to continue...
+ });
+ // Cause the dialog to appear synchronously when focused element is in chrome,
+ // otherwise, i.e., when focused element is in remote content, it appears
+ // asynchronously.
+ const focusedElementInChrome = Services.focus.focusedElement;
+ const isAsync = focusedElementInChrome?.isRemoteBrowser;
+ const waitForF7KeyHandled = new Promise(resolve => {
+ let eventCount = 0;
+ const expectedEventCount = isAsync ? 2 : 1;
+ let listener = async event => {
+ if (event.key == "F7") {
+ info("F7 keypress is fired");
+ if (++eventCount == expectedEventCount) {
+ window.removeEventListener("keypress", listener, {
+ capture: true,
+ mozSystemGroup: true,
+ });
+ // Wait for the event handled in chrome.
+ await TestUtils.waitForTick();
+ resolve();
+ return;
+ }
+ info(
+ "Waiting for next F7 keypress which is a reply event from the remote content"
+ );
+ }
+ };
+ info(
+ `Synthesizing "F7" key press and wait ${expectedEventCount} keypress events...`
+ );
+ window.addEventListener("keypress", listener, {
+ capture: true,
+ mozSystemGroup: true,
+ });
+ });
+ hitF7();
+ await waitForF7KeyHandled;
+
+ let expectedStr = expected ? "on." : "off.";
+ ok(
+ !openedDialog,
+ "Shouldn't open a dialog to turn caret browsing " + expectedStr
+ );
+ // Need to clean up if the dialog wasn't opened, so the observer doesn't get
+ // re-triggered later on causing "issues".
+ if (!openedDialog) {
+ gCaretPromptOpeningObserver();
+ }
+ let prefVal = Services.prefs.getBoolPref(kPrefCaretBrowsingOn);
+ is(prefVal, expected, "Caret browsing should now be " + expectedStr);
+}
+
+function waitForFocusOnInput(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ let textEl = content.document.getElementById("in");
+ return ContentTaskUtils.waitForCondition(() => {
+ return content.document.activeElement == textEl;
+ }, "Input should get focused.");
+ });
+}
+
+function focusInput(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ let textEl = content.document.getElementById("in");
+ textEl.focus();
+ });
+}
+
+add_task(async function checkTogglingCaretBrowsing() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
+ await focusInput(tab.linkedBrowser);
+
+ let promiseGotKey = promiseCaretPromptOpened();
+ hitF7();
+ let prompt = await promiseGotKey;
+ let doc = prompt.document;
+ let dialog = doc.getElementById("commonDialog");
+ is(dialog.defaultButton, "cancel", "No button should be the default");
+ ok(
+ !doc.getElementById("checkbox").checked,
+ "Checkbox shouldn't be checked by default."
+ );
+ let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
+ dialog.cancelDialog();
+ await promiseDialogUnloaded;
+ info("Dialog unloaded");
+ await waitForFocusOnInput(tab.linkedBrowser);
+ ok(
+ !Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
+ "Caret browsing should still be off after cancelling the dialog."
+ );
+
+ promiseGotKey = promiseCaretPromptOpened();
+ hitF7();
+ prompt = await promiseGotKey;
+
+ doc = prompt.document;
+ dialog = doc.getElementById("commonDialog");
+ is(dialog.defaultButton, "cancel", "No button should be the default");
+ ok(
+ !doc.getElementById("checkbox").checked,
+ "Checkbox shouldn't be checked by default."
+ );
+ promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
+ dialog.acceptDialog();
+ await promiseDialogUnloaded;
+ info("Dialog unloaded");
+ await waitForFocusOnInput(tab.linkedBrowser);
+ ok(
+ Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
+ "Caret browsing should be on after accepting the dialog."
+ );
+
+ await toggleCaretNoDialog(false);
+
+ promiseGotKey = promiseCaretPromptOpened();
+ hitF7();
+ prompt = await promiseGotKey;
+ doc = prompt.document;
+ dialog = doc.getElementById("commonDialog");
+
+ is(dialog.defaultButton, "cancel", "No button should be the default");
+ ok(
+ !doc.getElementById("checkbox").checked,
+ "Checkbox shouldn't be checked by default."
+ );
+
+ promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+ dialog.cancelDialog();
+ await promiseDialogUnloaded;
+ info("Dialog unloaded");
+ await waitForFocusOnInput(tab.linkedBrowser);
+
+ ok(
+ !Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
+ "Caret browsing should still be off after cancelling the dialog."
+ );
+
+ Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+ Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+ Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function toggleCheckboxNoCaretBrowsing() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
+ await focusInput(tab.linkedBrowser);
+
+ let promiseGotKey = promiseCaretPromptOpened();
+ hitF7();
+ let prompt = await promiseGotKey;
+ let doc = prompt.document;
+ let dialog = doc.getElementById("commonDialog");
+ is(dialog.defaultButton, "cancel", "No button should be the default");
+ let checkbox = doc.getElementById("checkbox");
+ ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");
+
+ // Check the box:
+ checkbox.click();
+
+ let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
+ // Say no:
+ dialog.getButton("cancel").click();
+
+ await promiseDialogUnloaded;
+ info("Dialog unloaded");
+ await waitForFocusOnInput(tab.linkedBrowser);
+ ok(
+ !Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
+ "Caret browsing should still be off."
+ );
+ ok(
+ !Services.prefs.getBoolPref(kPrefShortcutEnabled),
+ "Shortcut should now be disabled."
+ );
+
+ await toggleCaretNoDialog(false);
+ ok(
+ !Services.prefs.getBoolPref(kPrefShortcutEnabled),
+ "Shortcut should still be disabled."
+ );
+
+ Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+ Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+ Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function toggleCheckboxWantCaretBrowsing() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
+ await focusInput(tab.linkedBrowser);
+
+ let promiseGotKey = promiseCaretPromptOpened();
+ hitF7();
+ let prompt = await promiseGotKey;
+ let doc = prompt.document;
+ let dialog = doc.getElementById("commonDialog");
+ is(dialog.defaultButton, "cancel", "No button should be the default");
+ let checkbox = doc.getElementById("checkbox");
+ ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");
+
+ // Check the box:
+ checkbox.click();
+
+ let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
+ // Say yes:
+ dialog.acceptDialog();
+ await promiseDialogUnloaded;
+ info("Dialog unloaded");
+ await waitForFocusOnInput(tab.linkedBrowser);
+ ok(
+ Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
+ "Caret browsing should now be on."
+ );
+ ok(
+ Services.prefs.getBoolPref(kPrefShortcutEnabled),
+ "Shortcut should still be enabled."
+ );
+ ok(
+ !Services.prefs.getBoolPref(kPrefWarnOnEnable),
+ "Should no longer warn when enabling."
+ );
+
+ await toggleCaretNoDialog(false);
+ await toggleCaretNoDialog(true);
+ await toggleCaretNoDialog(false);
+
+ Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+ Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+ Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test for bug 1743878: Many repeated modal caret-browsing dialogs, if you
+// accidentally hold down F7 for a few seconds
+add_task(async function testF7SpamDoesNotOpenDialogs() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(() => BrowserTestUtils.removeTab(tab));
+
+ let promiseGotKey = promiseCaretPromptOpened();
+ hitF7();
+ let prompt = await promiseGotKey;
+ let doc = prompt.document;
+ let dialog = doc.getElementById("commonDialog");
+
+ let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
+ // Listen for an additional prompt to open, which should not happen.
+ let promiseDialogOrTimeout = () =>
+ Promise.race([
+ promiseCaretPromptOpened(),
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ new Promise(resolve => setTimeout(resolve, 100)),
+ ]);
+
+ let openedPromise = promiseDialogOrTimeout();
+
+ // Hit F7 two more times: once to test that _awaitingToggleCaretBrowsingPrompt
+ // is applied, and again to test that its value isn't somehow reset by
+ // pressing F7 while the dialog is open.
+ for (let i = 0; i < 2; i++) {
+ await new Promise(resolve =>
+ SimpleTest.executeSoon(() => {
+ hitF7();
+ resolve();
+ })
+ );
+ }
+
+ // Say no:
+ dialog.cancelDialog();
+ await promiseDialogUnloaded;
+ info("Dialog unloaded");
+
+ let openedDialog = await openedPromise;
+ ok(!openedDialog, "No additional dialog should have opened.");
+
+ // If the test fails, clean up any dialogs we erroneously opened so they don't
+ // interfere with other tests.
+ let extraDialogs = 0;
+ while (openedDialog) {
+ extraDialogs += 1;
+ let doc = openedDialog.document;
+ let dialog = doc.getElementById("commonDialog");
+ openedPromise = promiseDialogOrTimeout();
+ dialog.cancelDialog();
+ openedDialog = await openedPromise;
+ }
+ if (extraDialogs) {
+ info(`Closed ${extraDialogs} extra dialogs.`);
+ }
+
+ // Either way, we now have an extra observer, so clean it up.
+ gCaretPromptOpeningObserver();
+
+ Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+ Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+ Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+});
diff --git a/toolkit/content/tests/browser/browser_findbar.js b/toolkit/content/tests/browser/browser_findbar.js
new file mode 100644
index 0000000000..df3d9aae12
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_findbar.js
@@ -0,0 +1,560 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+requestLongerTimeout(2);
+
+const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s.";
+// Using 'javascript' schema to bypass E10SUtils.canLoadURIInRemoteType, because
+// it does not allow 'data:' URI to be loaded in the parent process.
+const E10S_PARENT_TEST_PAGE_URI =
+ getRootDirectory(gTestPath) + "file_empty.html";
+const TEST_PAGE_URI_WITHIFRAME =
+ "https://example.com/browser/toolkit/content/tests/browser/file_findinframe.html";
+
+/**
+ * Makes sure that the findbar hotkeys (' and /) event listeners
+ * are added to the system event group and do not get blocked
+ * by calling stopPropagation on a keypress event on a page.
+ */
+add_task(async function test_hotkey_event_propagation() {
+ info("Ensure hotkeys are not affected by stopPropagation.");
+
+ // Opening new tab
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+ let browser = gBrowser.getBrowserForTab(tab);
+ let findbar = await gBrowser.getFindBar();
+
+ // Pressing these keys open the findbar.
+ const HOTKEYS = ["/", "'"];
+
+ // Checking if findbar appears when any hotkey is pressed.
+ for (let key of HOTKEYS) {
+ is(findbar.hidden, true, "Findbar is hidden now.");
+ gBrowser.selectedTab = tab;
+ await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
+ await BrowserTestUtils.sendChar(key, browser);
+ is(findbar.hidden, false, "Findbar should not be hidden.");
+ await closeFindbarAndWait(findbar);
+ }
+
+ // Stop propagation for all keyboard events.
+ await SpecialPowers.spawn(browser, [], () => {
+ const stopPropagation = e => {
+ e.stopImmediatePropagation();
+ };
+ let window = content.document.defaultView;
+ window.addEventListener("keydown", stopPropagation);
+ window.addEventListener("keypress", stopPropagation);
+ window.addEventListener("keyup", stopPropagation);
+ });
+
+ // Checking if findbar still appears when any hotkey is pressed.
+ for (let key of HOTKEYS) {
+ is(findbar.hidden, true, "Findbar is hidden now.");
+ gBrowser.selectedTab = tab;
+ await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
+ await BrowserTestUtils.sendChar(key, browser);
+ is(findbar.hidden, false, "Findbar should not be hidden.");
+ await closeFindbarAndWait(findbar);
+ }
+
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_not_found() {
+ info("Check correct 'Phrase not found' on new tab");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+
+ // Search for the first word.
+ await promiseFindFinished(gBrowser, "--- THIS SHOULD NEVER MATCH ---", false);
+ let findbar = gBrowser.getCachedFindBar();
+ is(
+ findbar._findStatusDesc.dataset.l10nId,
+ "findbar-not-found",
+ "Findbar status text should be 'Phrase not found'"
+ );
+
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_found() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+
+ // Search for a string that WILL be found, with 'Highlight All' on
+ await promiseFindFinished(gBrowser, "S", true);
+ ok(
+ gBrowser.getCachedFindBar()._findStatusDesc.dataset.l10nId === undefined,
+ "Findbar status should be empty"
+ );
+
+ gBrowser.removeTab(tab);
+});
+
+// Setting first findbar to case-sensitive mode should not affect
+// new tab find bar.
+add_task(async function test_tabwise_case_sensitive() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+ let findbar1 = await gBrowser.getFindBar();
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+ let findbar2 = await gBrowser.getFindBar();
+
+ // Toggle case sensitivity for first findbar
+ findbar1.getElement("find-case-sensitive").click();
+
+ gBrowser.selectedTab = tab1;
+
+ // Not found for first tab.
+ await promiseFindFinished(gBrowser, "S", true);
+ is(
+ findbar1._findStatusDesc.dataset.l10nId,
+ "findbar-not-found",
+ "Findbar status text should be 'Phrase not found'"
+ );
+
+ gBrowser.selectedTab = tab2;
+
+ // But it didn't affect the second findbar.
+ await promiseFindFinished(gBrowser, "S", true);
+ ok(
+ findbar2._findStatusDesc.dataset.l10nId === undefined,
+ "Findbar status should be empty"
+ );
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+});
+
+/**
+ * Navigating from a web page (for example mozilla.org) to an internal page
+ * (like about:addons) might trigger a change of browser's remoteness.
+ * 'Remoteness change' means that rendering page content moves from child
+ * process into the parent process or the other way around.
+ * This test ensures that findbar properly handles such a change.
+ */
+add_task(async function test_reinitialization_at_remoteness_change() {
+ // This test only makes sence in e10s evironment.
+ if (!gMultiProcessBrowser) {
+ info("Skipping this test because of non-e10s environment.");
+ return;
+ }
+
+ info("Ensure findbar re-initialization at remoteness change.");
+
+ // Load a remote page and trigger findbar construction.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+ let browser = gBrowser.getBrowserForTab(tab);
+ let findbar = await gBrowser.getFindBar();
+
+ // Findbar should operate normally.
+ await promiseFindFinished(gBrowser, "z", false);
+ is(
+ findbar._findStatusDesc.dataset.l10nId,
+ "findbar-not-found",
+ "Findbar status text should be 'Phrase not found'"
+ );
+
+ await promiseFindFinished(gBrowser, "s", false);
+ ok(
+ findbar._findStatusDesc.dataset.l10nId === undefined,
+ "Findbar status should be empty"
+ );
+
+ // Moving browser into the parent process and reloading sample data.
+ ok(browser.isRemoteBrowser, "Browser should be remote now.");
+ await promiseRemotenessChange(tab, false);
+ let docLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ E10S_PARENT_TEST_PAGE_URI
+ );
+ BrowserTestUtils.loadURIString(browser, E10S_PARENT_TEST_PAGE_URI);
+ await docLoaded;
+ ok(!browser.isRemoteBrowser, "Browser should not be remote any more.");
+ browser.contentDocument.body.append("The letter s.");
+ browser.contentDocument.body.clientHeight; // Force flush.
+
+ // Findbar should keep operating normally after remoteness change.
+ await promiseFindFinished(gBrowser, "z", false);
+ is(
+ findbar._findStatusDesc.dataset.l10nId,
+ "findbar-not-found",
+ "Findbar status text should be 'Phrase not found'"
+ );
+
+ await promiseFindFinished(gBrowser, "s", false);
+ ok(
+ findbar._findStatusDesc.dataset.l10nId === undefined,
+ "Findbar status should be empty"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Ensure that the initial typed characters aren't lost immediately after
+ * opening the find bar.
+ */
+add_task(async function e10sLostKeys() {
+ // This test only makes sence in e10s evironment.
+ if (!gMultiProcessBrowser) {
+ info("Skipping this test because of non-e10s environment.");
+ return;
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+
+ ok(!gFindBarInitialized, "findbar isn't initialized yet");
+
+ await gFindBarPromise;
+ let findBar = gFindBar;
+ let initialValue = findBar._findField.value;
+
+ await EventUtils.synthesizeAndWaitKey(
+ "F",
+ { accelKey: true },
+ window,
+ null,
+ () => {
+ // We can't afford to wait for the promise to resolve, by then the
+ // find bar is visible and focused, so sending characters to the
+ // content browser wouldn't work.
+ isnot(
+ document.activeElement,
+ findBar._findField,
+ "findbar is not yet focused"
+ );
+ EventUtils.synthesizeKey("a");
+ EventUtils.synthesizeKey("b");
+ EventUtils.synthesizeKey("c");
+ is(
+ findBar._findField.value,
+ initialValue,
+ "still has initial find query"
+ );
+ }
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => findBar._findField.value.length == 3
+ );
+ is(document.activeElement, findBar._findField, "findbar is now focused");
+ is(findBar._findField.value, "abc", "abc fully entered as find query");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * This test makes sure that keyboard operations still occur
+ * after the findbar is opened and closed.
+ */
+add_task(async function test_open_and_close_keys() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<body style='height: 5000px;'>Hello There</body>"
+ );
+
+ await gFindBarPromise;
+ let findBar = gFindBar;
+
+ is(findBar.hidden, true, "Findbar is hidden now.");
+ let openedPromise = BrowserTestUtils.waitForEvent(findBar, "findbaropen");
+ await EventUtils.synthesizeKey("f", { accelKey: true });
+ await openedPromise;
+
+ is(findBar.hidden, false, "Findbar should not be hidden.");
+
+ let closedPromise = BrowserTestUtils.waitForEvent(findBar, "findbarclose");
+ await EventUtils.synthesizeKey("KEY_Escape");
+ await closedPromise;
+
+ let scrollPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "scroll"
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await scrollPromise;
+
+ let scrollPosition = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ return content.document.body.scrollTop;
+ }
+ );
+
+ ok(scrollPosition > 0, "Scrolled ok to " + scrollPosition);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * This test makes sure that keyboard navigation (for example arrow up/down,
+ * accel+arrow up/down) still works while the findbar is open.
+ */
+add_task(async function test_input_keypress() {
+ await SpecialPowers.pushPrefEnv({ set: [["general.smoothScroll", false]] });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ /* html */
+ `data:text/html,
+ <!DOCTYPE html>
+ <body style='height: 5000px;'>
+ Hello There
+ </body>`
+ );
+
+ await gFindBarPromise;
+ let findBar = gFindBar;
+
+ is(findBar.hidden, true, "Findbar is hidden now.");
+ let openedPromise = BrowserTestUtils.waitForEvent(findBar, "findbaropen");
+ await EventUtils.synthesizeKey("f", { accelKey: true });
+ await openedPromise;
+
+ is(findBar.hidden, false, "Findbar should not be hidden.");
+
+ let scrollPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "scroll"
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await scrollPromise;
+
+ await ContentTask.spawn(tab.linkedBrowser, null, async function () {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.defaultView.innerHeight +
+ content.document.defaultView.pageYOffset >
+ 0,
+ "Scroll with ArrowDown"
+ );
+ });
+
+ let completeScrollPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "scroll"
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown", { accelKey: true });
+ await completeScrollPromise;
+
+ await ContentTask.spawn(tab.linkedBrowser, null, async function () {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.defaultView.innerHeight +
+ content.document.defaultView.pageYOffset >=
+ content.document.body.offsetHeight,
+ "Scroll with Accel+ArrowDown"
+ );
+ });
+
+ let closedPromise = BrowserTestUtils.waitForEvent(findBar, "findbarclose");
+ await EventUtils.synthesizeKey("KEY_Escape");
+ await closedPromise;
+
+ info("Scrolling ok");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test loads an editable area within an iframe and then
+// performs a search. Focusing the editable area should still
+// allow keyboard events to be received.
+add_task(async function test_hotkey_insubframe() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI_WITHIFRAME
+ );
+
+ await gFindBarPromise;
+ let findBar = gFindBar;
+
+ // Focus the editable area within the frame.
+ let browser = gBrowser.selectedBrowser;
+ let frameBC = browser.browsingContext.children[0];
+ await SpecialPowers.spawn(frameBC, [], async () => {
+ content.document.body.focus();
+ content.document.defaultView.focus();
+ });
+
+ // Start a find and wait for the findbar to open.
+ let findBarOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "findbaropen"
+ );
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ await findBarOpenPromise;
+
+ // Opening the findbar would have focused the find textbox.
+ // Focus the editable area again.
+ let cursorPos = await SpecialPowers.spawn(frameBC, [], async () => {
+ content.document.body.focus();
+ content.document.defaultView.focus();
+ return content.getSelection().anchorOffset;
+ });
+ is(cursorPos, 0, "initial cursor position");
+
+ // Try moving the caret.
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, frameBC);
+
+ cursorPos = await SpecialPowers.spawn(frameBC, [], async () => {
+ return content.getSelection().anchorOffset;
+ });
+ is(cursorPos, 1, "cursor moved");
+
+ await closeFindbarAndWait(findBar);
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Reloading a page should use the same match case / whole word
+ * state for the search.
+ */
+add_task(async function test_preservestate_on_reload() {
+ for (let stateChange of ["case-sensitive", "entire-word"]) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<!DOCTYPE html><p>There is a cat named Theo in the kitchen with another cat named Catherine. The two of them are thirsty."
+ );
+
+ // Start a find and wait for the findbar to open.
+ let findBarOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser,
+ "findbaropen"
+ );
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ await findBarOpenPromise;
+
+ let isEntireWord = stateChange == "entire-word";
+
+ let findbar = await gBrowser.getFindBar();
+
+ // Find some text.
+ let promiseMatches = promiseGetMatchCount(findbar);
+ await promiseFindFinished(gBrowser, "The", true);
+
+ let matches = await promiseMatches;
+ is(matches.current, 1, "Correct match position " + stateChange);
+ is(matches.total, 7, "Correct number of matches " + stateChange);
+
+ // Turn on the case sensitive or entire word option.
+ findbar.getElement("find-" + stateChange).click();
+
+ promiseMatches = promiseGetMatchCount(findbar);
+ gFindBar.onFindAgainCommand();
+ matches = await promiseMatches;
+ is(
+ matches.current,
+ 2,
+ "Correct match position after state change matches " + stateChange
+ );
+ is(
+ matches.total,
+ isEntireWord ? 2 : 3,
+ "Correct number after state change matches " + stateChange
+ );
+
+ // Reload the page.
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true
+ );
+ gBrowser.reload();
+ await loadedPromise;
+
+ // Perform a find again. The state should be preserved.
+ promiseMatches = promiseGetMatchCount(findbar);
+ gFindBar.onFindAgainCommand();
+ matches = await promiseMatches;
+ is(
+ matches.current,
+ 1,
+ "Correct match position after reload and find again " + stateChange
+ );
+ is(
+ matches.total,
+ isEntireWord ? 2 : 3,
+ "Correct number of matches after reload and find again " + stateChange
+ );
+
+ // Turn off the case sensitive or entire word option and find again.
+ findbar.getElement("find-" + stateChange).click();
+
+ promiseMatches = promiseGetMatchCount(findbar);
+ gFindBar.onFindAgainCommand();
+ matches = await promiseMatches;
+
+ is(
+ matches.current,
+ isEntireWord ? 4 : 2,
+ "Correct match position after reload and find again reset " + stateChange
+ );
+ is(
+ matches.total,
+ 7,
+ "Correct number of matches after reload and find again reset " +
+ stateChange
+ );
+
+ findbar.clear();
+ await closeFindbarAndWait(findbar);
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+function promiseGetMatchCount(findbar) {
+ return new Promise(resolve => {
+ let resultListener = {
+ onFindResult() {},
+ onCurrentSelection() {},
+ onHighlightFinished() {},
+ onMatchesCountResult(response) {
+ if (response.total > 0) {
+ findbar.browser.finder.removeResultListener(resultListener);
+ resolve(response);
+ }
+ },
+ };
+ findbar.browser.finder.addResultListener(resultListener);
+ });
+}
+
+function promiseRemotenessChange(tab, shouldBeRemote) {
+ return new Promise(resolve => {
+ let browser = gBrowser.getBrowserForTab(tab);
+ tab.addEventListener(
+ "TabRemotenessChange",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ let remoteType = shouldBeRemote
+ ? E10SUtils.DEFAULT_REMOTE_TYPE
+ : E10SUtils.NOT_REMOTE;
+ gBrowser.updateBrowserRemoteness(browser, { remoteType });
+ });
+}
diff --git a/toolkit/content/tests/browser/browser_findbar_disabled_manual.js b/toolkit/content/tests/browser/browser_findbar_disabled_manual.js
new file mode 100644
index 0000000000..ef37f7dc9a
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_findbar_disabled_manual.js
@@ -0,0 +1,33 @@
+const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s.";
+
+// Disable manual (FAYT) findbar hotkeys.
+add_task(async function setup_test_preference() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["accessibility.typeaheadfind.manual", false]],
+ });
+});
+
+// Makes sure that the findbar hotkeys (' and /) have no effect.
+add_task(async function test_hotkey_disabled() {
+ // Opening new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+ let browser = gBrowser.getBrowserForTab(tab);
+ let findbar = await gBrowser.getFindBar();
+
+ // Pressing these keys open the findbar normally.
+ const HOTKEYS = ["/", "'"];
+
+ // Make sure no findbar appears when pressed.
+ for (let key of HOTKEYS) {
+ is(findbar.hidden, true, "Findbar is hidden now.");
+ gBrowser.selectedTab = tab;
+ await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
+ await BrowserTestUtils.sendChar(key, browser);
+ is(findbar.hidden, true, "Findbar should still be hidden.");
+ }
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_findbar_hiddenframes.js b/toolkit/content/tests/browser/browser_findbar_hiddenframes.js
new file mode 100644
index 0000000000..f0f4131464
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_findbar_hiddenframes.js
@@ -0,0 +1,59 @@
+const TEST_PAGE = "https://example.com/document-builder.sjs?html=";
+
+let content =
+ "<html><body><iframe id='a' src='data:text/html,This is the first page'></iframe><iframe id='b' src='data:text/html,That is another page'></iframe></body></html>";
+
+async function doAndCheckFind(bc, text) {
+ await promiseFindFinished(gBrowser, text, false);
+
+ let foundText = await SpecialPowers.spawn(bc, [], () => {
+ return content.getSelection().toString();
+ });
+ is(foundText, text, text + " is found");
+}
+
+// This test verifies that find continues to work when a find begins and the frame
+// is hidden during the next find step.
+add_task(async function test_frame() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE + content
+ );
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ let findbar = await gBrowser.getFindBar();
+
+ await doAndCheckFind(browser.browsingContext.children[0], "This");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("a").style.display = "none";
+ content.document.getElementById("a").getBoundingClientRect(); // flush
+ });
+
+ await doAndCheckFind(browser.browsingContext.children[1], "another");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("a").style.display = "";
+ content.document.getElementById("a").getBoundingClientRect();
+ });
+
+ await doAndCheckFind(browser.browsingContext.children[0], "first");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("a").style.visibility = "hidden";
+ content.document.getElementById("a").getBoundingClientRect();
+ });
+
+ await doAndCheckFind(browser.browsingContext.children[1], "That");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("a").style.visibility = "";
+ content.document.getElementById("a").getBoundingClientRect();
+ });
+
+ await doAndCheckFind(browser.browsingContext.children[0], "This");
+
+ await closeFindbarAndWait(findbar);
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_findbar_marks.js b/toolkit/content/tests/browser/browser_findbar_marks.js
new file mode 100644
index 0000000000..fd1966041a
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_findbar_marks.js
@@ -0,0 +1,263 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// This test verifies that the find scrollbar marks are triggered in the right locations.
+// Reftests in layout/xul/reftest are used to verify their appearance.
+
+const TEST_PAGE_URI =
+ "data:text/html,<body style='font-size: 20px; margin: 0;'><p style='margin: 0; block-size: 30px;'>This is some fun text.</p><p style='margin-block-start: 2000px; block-size: 30px;'>This is some tex to find.</p><p style='margin-block-start: 500px; block-size: 30px;'>This is some text to find.</p></body>";
+
+let gUpdateCount = 0;
+
+requestLongerTimeout(5);
+
+function initForBrowser(browser) {
+ gUpdateCount = 0;
+
+ browser.sendMessageToActor(
+ "Finder:EnableMarkTesting",
+ { enable: true },
+ "Finder"
+ );
+
+ let checkFn = event => {
+ event.target.lastMarks = event.detail;
+ event.target.eventsCount = event.target.eventsCount
+ ? event.target.eventsCount + 1
+ : 1;
+ return false;
+ };
+
+ let endFn = BrowserTestUtils.addContentEventListener(
+ browser,
+ "find-scrollmarks-changed",
+ () => {},
+ { capture: true },
+ checkFn
+ );
+
+ return () => {
+ browser.sendMessageToActor(
+ "Finder:EnableMarkTesting",
+ { enable: false },
+ "Finder"
+ );
+
+ endFn();
+ };
+}
+
+add_task(async function test_findmarks() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+
+ // Open the findbar so that the content scroll size can be measured.
+ await promiseFindFinished(gBrowser, "s");
+
+ let browser = tab.linkedBrowser;
+ let scrollMaxY = await SpecialPowers.spawn(browser, [], () => {
+ return content.scrollMaxY;
+ });
+
+ let endFn = initForBrowser(browser);
+
+ for (let step = 0; step < 3; step++) {
+ // If the document root or body is absolutely positioned, this can affect the scroll height.
+ await SpecialPowers.spawn(browser, [step], stepChild => {
+ let document = content.document;
+ let adjustments = [
+ () => {},
+ () => {
+ document.documentElement.style.position = "absolute;";
+ },
+ () => {
+ document.documentElement.style.position = "";
+ document.body.style.position = "absolute";
+ },
+ ];
+
+ adjustments[stepChild]();
+ });
+
+ // For the first value, get the numbers and ensure that they are approximately
+ // in the right place. Later tests should give the same values.
+ await promiseFindFinished(gBrowser, "tex", true);
+
+ let values = await getMarks(browser, true);
+
+ // The exact values vary on each platform, so use fuzzy matches.
+ // 2610 is the approximate expected document height, and
+ // 10, 2040, 2570 are the approximate positions of the marks.
+ const expectedDocHeight = 2610;
+ isfuzzy(
+ values[0],
+ Math.round(10 * (scrollMaxY / expectedDocHeight)),
+ 10,
+ "first value"
+ );
+ isfuzzy(
+ values[1],
+ Math.round(2040 * (scrollMaxY / expectedDocHeight)),
+ 10,
+ "second value"
+ );
+ isfuzzy(
+ values[2],
+ Math.round(2570 * (scrollMaxY / expectedDocHeight)),
+ 10,
+ "third value"
+ );
+
+ await doAndVerifyFind(browser, "text", true, [values[0], values[2]]);
+ await doAndVerifyFind(browser, "", true, []);
+ await doAndVerifyFind(browser, "isz", false, [], true); // marks should not be updated here
+ await doAndVerifyFind(browser, "tex", true, values);
+ await doAndVerifyFind(browser, "isz", true, []);
+ await doAndVerifyFind(browser, "tex", true, values);
+
+ let findbar = await gBrowser.getFindBar();
+ let closedPromise = BrowserTestUtils.waitForEvent(findbar, "findbarclose");
+ await EventUtils.synthesizeKey("KEY_Escape");
+ await closedPromise;
+
+ await verifyFind(browser, "", true, []);
+ }
+
+ endFn();
+
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_findmarks_vertical() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE_URI
+ );
+ let browser = tab.linkedBrowser;
+ let endFn = initForBrowser(browser);
+
+ for (let mode of [
+ "sideways-lr",
+ "sideways-rl",
+ "vertical-lr",
+ "vertical-rl",
+ ]) {
+ const maxMarkPos = await SpecialPowers.spawn(
+ browser,
+ [mode],
+ writingMode => {
+ let document = content.document;
+ document.documentElement.style.writingMode = writingMode;
+
+ return content.scrollMaxX - content.scrollMinX;
+ }
+ );
+
+ await promiseFindFinished(gBrowser, "tex", true);
+ const marks = await getMarks(browser, true, true);
+ Assert.equal(marks.length, 3, `marks count with text "tex"`);
+ for (const markPos of marks) {
+ Assert.ok(
+ 0 <= markPos <= maxMarkPos,
+ `mark position ${markPos} should be in the range 0 ~ ${maxMarkPos}`
+ );
+ }
+ }
+
+ endFn();
+ gBrowser.removeTab(tab);
+});
+
+// This test verifies what happens when scroll marks are visible and the window is resized.
+add_task(async function test_found_resize() {
+ let window2 = await BrowserTestUtils.openNewBrowserWindow({});
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ TEST_PAGE_URI
+ );
+
+ let browser = tab.linkedBrowser;
+ let endFn = initForBrowser(browser);
+
+ await promiseFindFinished(window2.gBrowser, "tex", true);
+ let values = await getMarks(browser, true);
+
+ let resizePromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "resize",
+ true
+ );
+ window2.resizeTo(window2.outerWidth - 100, window2.outerHeight - 80);
+ await resizePromise;
+
+ // Some number of extra scrollbar adjustment and painting events can occur
+ // when resizing the window, so don't use an exact match for the count.
+ let resizedValues = await getMarks(browser, true);
+ info(`values: ${JSON.stringify(values)}`);
+ info(`resizedValues: ${JSON.stringify(resizedValues)}`);
+ isfuzzy(resizedValues[0], values[0], 2, "first value");
+ ok(resizedValues[1] - 50 > values[1], "second value");
+ ok(resizedValues[2] - 50 > values[2], "third value");
+
+ endFn();
+
+ await BrowserTestUtils.closeWindow(window2);
+});
+
+// Returns the scroll marks that should have been assigned
+// to the scrollbar after a find. As a side effect, also
+// verifies that the marks have been updated since the last
+// call to getMarks. If increase is true, then the marks should
+// have been updated, and if increase is false, the marks should
+// not have been updated.
+async function getMarks(browser, increase, shouldBeOnHScrollbar = false) {
+ let results = await SpecialPowers.spawn(browser, [], () => {
+ let { marks, onHorizontalScrollbar } = content.lastMarks;
+ content.lastMarks = {};
+ return {
+ onHorizontalScrollbar,
+ marks: marks || [],
+ count: content.eventsCount,
+ };
+ });
+
+ // The marks are updated whenever the scrollbar is updated and
+ // this could happen several times as either a find for multiple
+ // characters occurs. This check allows for mutliple updates to occur.
+ if (increase) {
+ Assert.ok(results.count > gUpdateCount, "expected events count");
+
+ Assert.strictEqual(
+ results.onHorizontalScrollbar,
+ shouldBeOnHScrollbar,
+ "marks should be on the horizontal scrollbar"
+ );
+ } else {
+ Assert.equal(results.count, gUpdateCount, "expected events count");
+ }
+
+ gUpdateCount = results.count;
+ return results.marks;
+}
+
+async function doAndVerifyFind(browser, text, increase, expectedMarks) {
+ await promiseFindFinished(browser.getTabBrowser(), text, true);
+ return verifyFind(browser, text, increase, expectedMarks);
+}
+
+async function verifyFind(browser, text, increase, expectedMarks) {
+ let foundMarks = await getMarks(browser, increase);
+
+ is(foundMarks.length, expectedMarks.length, "marks count with text " + text);
+ for (let t = 0; t < foundMarks.length; t++) {
+ isfuzzy(
+ foundMarks[t],
+ expectedMarks[t],
+ 5,
+ "mark " + t + " with text " + text
+ );
+ }
+
+ Assert.deepEqual(foundMarks, expectedMarks, "basic find with text " + text);
+}
diff --git a/toolkit/content/tests/browser/browser_isSynthetic.js b/toolkit/content/tests/browser/browser_isSynthetic.js
new file mode 100644
index 0000000000..7788d6317e
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_isSynthetic.js
@@ -0,0 +1,69 @@
+function LocationChangeListener(browser) {
+ this.browser = browser;
+ browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+}
+
+LocationChangeListener.prototype = {
+ wasSynthetic: false,
+ browser: null,
+
+ destroy() {
+ this.browser.removeProgressListener(this);
+ },
+
+ onLocationChange(webProgress, request, location, flags) {
+ this.wasSynthetic = this.browser.isSyntheticDocument;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+const FILES = gTestPath
+ .replace("browser_isSynthetic.js", "")
+ .replace("chrome://mochitests/content/", "http://example.com/");
+
+function waitForPageShow(browser) {
+ return BrowserTestUtils.waitForContentEvent(browser, "pageshow", true);
+}
+
+add_task(async function () {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+ let listener = new LocationChangeListener(browser);
+
+ is(browser.isSyntheticDocument, false, "Should not be synthetic");
+
+ let loadPromise = waitForPageShow(browser);
+ BrowserTestUtils.loadURIString(
+ browser,
+ "data:text/html;charset=utf-8,<html/>"
+ );
+ await loadPromise;
+ is(listener.wasSynthetic, false, "Should not be synthetic");
+ is(browser.isSyntheticDocument, false, "Should not be synthetic");
+
+ loadPromise = waitForPageShow(browser);
+ BrowserTestUtils.loadURIString(browser, FILES + "empty.png");
+ await loadPromise;
+ is(listener.wasSynthetic, true, "Should be synthetic");
+ is(browser.isSyntheticDocument, true, "Should be synthetic");
+
+ loadPromise = waitForPageShow(browser);
+ browser.goBack();
+ await loadPromise;
+ is(listener.wasSynthetic, false, "Should not be synthetic");
+ is(browser.isSyntheticDocument, false, "Should not be synthetic");
+
+ loadPromise = waitForPageShow(browser);
+ browser.goForward();
+ await loadPromise;
+ is(listener.wasSynthetic, true, "Should be synthetic");
+ is(browser.isSyntheticDocument, true, "Should be synthetic");
+
+ listener.destroy();
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js b/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js
new file mode 100644
index 0000000000..f92d7089ab
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js
@@ -0,0 +1,129 @@
+add_task(async function () {
+ const kPrefName_AutoScroll = "general.autoScroll";
+ Services.prefs.setBoolPref(kPrefName_AutoScroll, true);
+ registerCleanupFunction(() =>
+ Services.prefs.clearUserPref(kPrefName_AutoScroll)
+ );
+
+ const kNoKeyEvents = 0;
+ const kKeyDownEvent = 1;
+ const kKeyPressEvent = 2;
+ const kKeyUpEvent = 4;
+ const kAllKeyEvents = 7;
+
+ var expectedKeyEvents;
+ var dispatchedKeyEvents;
+ var key;
+
+ /**
+ * Encapsulates EventUtils.sendChar().
+ */
+ function sendChar(aChar) {
+ key = aChar;
+ dispatchedKeyEvents = kNoKeyEvents;
+ EventUtils.sendChar(key);
+ is(
+ dispatchedKeyEvents,
+ expectedKeyEvents,
+ "unexpected key events were dispatched or not dispatched: " + key
+ );
+ }
+
+ /**
+ * Encapsulates EventUtils.sendKey().
+ */
+ function sendKey(aKey) {
+ key = aKey;
+ dispatchedKeyEvents = kNoKeyEvents;
+ EventUtils.sendKey(key);
+ is(
+ dispatchedKeyEvents,
+ expectedKeyEvents,
+ "unexpected key events were dispatched or not dispatched: " + key
+ );
+ }
+
+ function onKey(aEvent) {
+ // if (aEvent.target != root && aEvent.target != root.ownerDocument.body) {
+ // ok(false, "unknown target: " + aEvent.target.tagName);
+ // return;
+ // }
+
+ var keyFlag;
+ switch (aEvent.type) {
+ case "keydown":
+ keyFlag = kKeyDownEvent;
+ break;
+ case "keypress":
+ keyFlag = kKeyPressEvent;
+ break;
+ case "keyup":
+ keyFlag = kKeyUpEvent;
+ break;
+ default:
+ ok(false, "Unknown events: " + aEvent.type);
+ return;
+ }
+ dispatchedKeyEvents |= keyFlag;
+ is(keyFlag, expectedKeyEvents & keyFlag, aEvent.type + " fired: " + key);
+ }
+
+ var dataUri = 'data:text/html,<body style="height:10000px;"></body>';
+
+ await BrowserTestUtils.withNewTab(dataUri, async function (browser) {
+ info("Loaded data URI in new tab");
+ await SimpleTest.promiseFocus(browser);
+ info("Focused selected browser");
+
+ window.addEventListener("keydown", onKey);
+ window.addEventListener("keypress", onKey);
+ window.addEventListener("keyup", onKey);
+ registerCleanupFunction(() => {
+ window.removeEventListener("keydown", onKey);
+ window.removeEventListener("keypress", onKey);
+ window.removeEventListener("keyup", onKey);
+ });
+
+ // Test whether the key events are handled correctly under normal condition
+ expectedKeyEvents = kAllKeyEvents;
+ sendChar("A");
+
+ // Start autoscrolling by middle button click on the page
+ info("Creating popup shown promise");
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ false,
+ event => event.originalTarget.className == "autoscroller"
+ );
+ await BrowserTestUtils.synthesizeMouseAtPoint(
+ 10,
+ 10,
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ info("Waiting for autoscroll popup to show");
+ await shownPromise;
+
+ // Most key events should be eaten by the browser.
+ expectedKeyEvents = kNoKeyEvents;
+ sendChar("A");
+ sendKey("DOWN");
+ sendKey("RETURN");
+ sendKey("RETURN");
+ sendKey("HOME");
+ sendKey("END");
+ sendKey("TAB");
+ sendKey("RETURN");
+
+ // Finish autoscrolling by ESC key. Note that only keydown and keypress
+ // events are eaten because keyup event is fired *after* the autoscrolling
+ // is finished.
+ expectedKeyEvents = kKeyUpEvent;
+ sendKey("ESCAPE");
+
+ // Test whether the key events are handled correctly under normal condition
+ expectedKeyEvents = kAllKeyEvents;
+ sendChar("A");
+ });
+});
diff --git a/toolkit/content/tests/browser/browser_label_textlink.js b/toolkit/content/tests/browser/browser_label_textlink.js
new file mode 100644
index 0000000000..4d53c2b5e9
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_label_textlink.js
@@ -0,0 +1,65 @@
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences" },
+ async function (browser) {
+ let newTabURL = "http://www.example.com/";
+ await SpecialPowers.spawn(
+ browser,
+ [newTabURL],
+ async function (newTabURL) {
+ let doc = content.document;
+ let label = doc.createXULElement("label", { is: "text-link" });
+ label.href = newTabURL;
+ label.id = "textlink-test";
+ label.textContent = "click me";
+ doc.body.append(label);
+ }
+ );
+
+ // Test that click will open tab in foreground.
+ let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#textlink-test",
+ {},
+ browser
+ );
+ let newTab = await awaitNewTab;
+ is(
+ newTab.linkedBrowser,
+ gBrowser.selectedBrowser,
+ "selected tab should be example page"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Test that ctrl+shift+click/meta+shift+click will open tab in background.
+ awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#textlink-test",
+ { ctrlKey: true, metaKey: true, shiftKey: true },
+ browser
+ );
+ await awaitNewTab;
+ is(
+ gBrowser.selectedBrowser,
+ browser,
+ "selected tab should be original tab"
+ );
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+
+ // Middle-clicking should open tab in foreground.
+ awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#textlink-test",
+ { button: 1 },
+ browser
+ );
+ newTab = await awaitNewTab;
+ is(
+ newTab.linkedBrowser,
+ gBrowser.selectedBrowser,
+ "selected tab should be example page"
+ );
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_license_links.js b/toolkit/content/tests/browser/browser_license_links.js
new file mode 100644
index 0000000000..3eff69ba75
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_license_links.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that we can reach about:rights and about:buildconfig using links
+ * from about:license.
+ */
+add_task(async function check_links() {
+ await BrowserTestUtils.withNewTab("about:license", async browser => {
+ for (let otherPage of ["about:rights", "about:buildconfig"]) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, otherPage);
+ await BrowserTestUtils.synthesizeMouse(
+ `a[href='${otherPage}']`,
+ 2,
+ 2,
+ { accelKey: true },
+ browser
+ );
+ info("Clicked " + otherPage + " link");
+ let tab = await tabPromise;
+ ok(true, otherPage + " tab opened correctly");
+ BrowserTestUtils.removeTab(tab);
+ }
+ });
+});
diff --git a/toolkit/content/tests/browser/browser_mediaStreamPlayback.html b/toolkit/content/tests/browser/browser_mediaStreamPlayback.html
new file mode 100644
index 0000000000..09685d488e
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_mediaStreamPlayback.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<body>
+<video id="v" controls></video>
+<script>
+const v = document.getElementById("v");
+
+function audioTrack() {
+ const ctx = new AudioContext(), oscillator = ctx.createOscillator();
+ const dst = oscillator.connect(ctx.createMediaStreamDestination());
+ oscillator.start();
+ return dst.stream.getAudioTracks()[0];
+}
+
+function videoTrack(width = 640, height = 480) {
+ const canvas = Object.assign(document.createElement("canvas"), {width, height});
+ canvas.getContext('2d').fillRect(0, 0, width, height);
+ return canvas.captureStream(10).getVideoTracks()[0];
+}
+
+onload = () => v.srcObject = new MediaStream([videoTrack(), audioTrack()]);
+</script>
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/browser_mediaStreamPlaybackWithoutAudio.html b/toolkit/content/tests/browser/browser_mediaStreamPlaybackWithoutAudio.html
new file mode 100644
index 0000000000..fbc9fcb033
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_mediaStreamPlaybackWithoutAudio.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<body>
+<video id="v" controls></video>
+<script>
+const v = document.getElementById("v");
+
+function videoTrack(width = 640, height = 480) {
+ const canvas = Object.assign(document.createElement("canvas"), {width, height});
+ canvas.getContext('2d').fillRect(0, 0, width, height);
+ return canvas.captureStream(10).getVideoTracks()[0];
+}
+
+onload = () => v.srcObject = new MediaStream([videoTrack()]);
+</script>
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/browser_media_wakelock.js b/toolkit/content/tests/browser/browser_media_wakelock.js
new file mode 100644
index 0000000000..da27805a9d
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_media_wakelock.js
@@ -0,0 +1,159 @@
+/**
+ * Test whether the wakelock state is correct under different situations. However,
+ * the lock state of power manager doesn't equal to the actual platform lock.
+ * Now we don't have any way to detect whether platform lock is set correctly or
+ * not, but we can at least make sure the specific topic's state in power manager
+ * is correct.
+ */
+"use strict";
+
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const LOCATION = "https://example.com/browser/toolkit/content/tests/browser/";
+const AUDIO_WAKELOCK_NAME = "audio-playing";
+const VIDEO_WAKELOCK_NAME = "video-playing";
+
+add_task(async function testCheckWakelockWhenChangeTabVisibility() {
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing video",
+ url: "file_video.html",
+ lockAudio: true,
+ lockVideo: true,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing muted video",
+ url: "file_video.html",
+ additionalParams: {
+ muted: true,
+ },
+ lockAudio: false,
+ lockVideo: true,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing volume=0 video",
+ url: "file_video.html",
+ additionalParams: {
+ volume: 0.0,
+ },
+ lockAudio: false,
+ lockVideo: true,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing video without audio in it",
+ url: "file_videoWithoutAudioTrack.html",
+ lockAudio: false,
+ lockVideo: false,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing audio in video element",
+ url: "file_videoWithAudioOnly.html",
+ lockAudio: true,
+ lockVideo: false,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing audio in audio element",
+ url: "file_mediaPlayback2.html",
+ lockAudio: true,
+ lockVideo: false,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing video from media stream with audio and video tracks",
+ url: "browser_mediaStreamPlayback.html",
+ lockAudio: true,
+ lockVideo: true,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing video from media stream without audio track",
+ url: "browser_mediaStreamPlaybackWithoutAudio.html",
+ lockAudio: true,
+ lockVideo: true,
+ });
+});
+
+/**
+ * Following are helper functions.
+ */
+async function checkWakelockWhenChangeTabVisibility({
+ description,
+ url,
+ additionalParams,
+ lockAudio,
+ lockVideo,
+}) {
+ const originalTab = gBrowser.selectedTab;
+ info(`start a new tab for '${description}'`);
+ const mediaTab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ LOCATION + url
+ );
+
+ info(`wait for media starting playing`);
+ await waitUntilVideoStarted(mediaTab, additionalParams);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: lockAudio,
+ isForegroundLock: true,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: lockVideo,
+ isForegroundLock: true,
+ });
+
+ info(`switch media tab to background`);
+ await BrowserTestUtils.switchTab(window.gBrowser, originalTab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: lockAudio,
+ isForegroundLock: false,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: lockVideo,
+ isForegroundLock: false,
+ });
+
+ info(`switch media tab to foreground again`);
+ await BrowserTestUtils.switchTab(window.gBrowser, mediaTab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: lockAudio,
+ isForegroundLock: true,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: lockVideo,
+ isForegroundLock: true,
+ });
+
+ info(`remove tab`);
+ if (mediaTab.PIPWindow) {
+ await BrowserTestUtils.closeWindow(mediaTab.PIPWindow);
+ }
+ BrowserTestUtils.removeTab(mediaTab);
+}
+
+async function waitUntilVideoStarted(tab, { muted, volume } = {}) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [muted, volume],
+ async (muted, volume) => {
+ const video = content.document.getElementById("v");
+ if (!video) {
+ ok(false, "can't get media element!");
+ return;
+ }
+ if (muted) {
+ video.muted = muted;
+ }
+ if (volume !== undefined) {
+ video.volume = volume;
+ }
+ ok(
+ await video.play().then(
+ () => true,
+ () => false
+ ),
+ `video started playing.`
+ );
+ }
+ );
+}
diff --git a/toolkit/content/tests/browser/browser_media_wakelock_PIP.js b/toolkit/content/tests/browser/browser_media_wakelock_PIP.js
new file mode 100644
index 0000000000..d550fc2ffa
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_media_wakelock_PIP.js
@@ -0,0 +1,155 @@
+/**
+ * Test the wakelock usage for video being used in the picture-in-picuture (PIP)
+ * mode. When video is playing in PIP window, we would always request a video
+ * wakelock, and request audio wakelock only when video is audible.
+ */
+add_task(async function testCheckWakelockForPIPVideo() {
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing a PIP video",
+ lockAudio: true,
+ lockVideo: true,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing a muted PIP video",
+ additionalParams: {
+ muted: true,
+ },
+ lockAudio: false,
+ lockVideo: true,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing a volume=0 PIP video",
+ additionalParams: {
+ volume: 0.0,
+ },
+ lockAudio: false,
+ lockVideo: true,
+ });
+});
+
+/**
+ * Following are helper functions and variables.
+ */
+const PAGE_URL =
+ "https://example.com/browser/toolkit/content/tests/browser/file_video.html";
+const AUDIO_WAKELOCK_NAME = "audio-playing";
+const VIDEO_WAKELOCK_NAME = "video-playing";
+const TEST_VIDEO_ID = "v";
+
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+async function checkWakelockWhenChangeTabVisibility({
+ description,
+ additionalParams,
+ lockAudio,
+ lockVideo,
+}) {
+ const originalTab = gBrowser.selectedTab;
+ info(`start a new tab for '${description}'`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ PAGE_URL
+ );
+
+ info(`wait for PIP video starting playing`);
+ await startPIPVideo(tab, additionalParams);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: lockAudio,
+ isForegroundLock: true,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: lockVideo,
+ isForegroundLock: true,
+ });
+
+ info(
+ `switch tab to background and still own foreground locks due to visible PIP video`
+ );
+ await BrowserTestUtils.switchTab(window.gBrowser, originalTab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: lockAudio,
+ isForegroundLock: true,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: lockVideo,
+ isForegroundLock: true,
+ });
+
+ info(`pausing PIP video should release all locks`);
+ await pausePIPVideo(tab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: false,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: false,
+ });
+
+ info(`resuming PIP video should request locks again`);
+ await resumePIPVideo(tab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: lockAudio,
+ isForegroundLock: true,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: lockVideo,
+ isForegroundLock: true,
+ });
+
+ info(`switch tab to foreground again`);
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: lockAudio,
+ isForegroundLock: true,
+ });
+ await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: lockVideo,
+ isForegroundLock: true,
+ });
+
+ info(`remove tab`);
+ await BrowserTestUtils.closeWindow(tab.PIPWindow);
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function startPIPVideo(tab, { muted, volume } = {}) {
+ tab.PIPWindow = await triggerPictureInPicture(
+ tab.linkedBrowser,
+ TEST_VIDEO_ID
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [muted, volume, TEST_VIDEO_ID],
+ async (muted, volume, Id) => {
+ const video = content.document.getElementById(Id);
+ if (muted) {
+ video.muted = muted;
+ }
+ if (volume !== undefined) {
+ video.volume = volume;
+ }
+ ok(
+ await video.play().then(
+ () => true,
+ () => false
+ ),
+ `video started playing.`
+ );
+ }
+ );
+}
+
+function pausePIPVideo(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [TEST_VIDEO_ID], Id => {
+ content.document.getElementById(Id).pause();
+ });
+}
+
+function resumePIPVideo(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [TEST_VIDEO_ID], async Id => {
+ await content.document.getElementById(Id).play();
+ });
+}
diff --git a/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js b/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js
new file mode 100644
index 0000000000..7c40b5fe1a
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js
@@ -0,0 +1,127 @@
+/**
+ * Test if wakelock can be required correctly when we play web audio. The
+ * wakelock should only be required when web audio is audible.
+ */
+
+const AUDIO_WAKELOCK_NAME = "audio-playing";
+const VIDEO_WAKELOCK_NAME = "video-playing";
+
+add_task(async function testCheckAudioWakelockWhenChangeTabVisibility() {
+ await checkWakelockWhenChangeTabVisibility({
+ description: "playing audible web audio",
+ needLock: true,
+ });
+ await checkWakelockWhenChangeTabVisibility({
+ description: "suspended web audio",
+ additionalParams: {
+ suspend: true,
+ },
+ needLock: false,
+ });
+});
+
+add_task(
+ async function testBrieflyAudibleAudioContextReleasesAudioWakeLockWhenInaudible() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+
+ info(`make a short noise on web audio`);
+ await Promise.all([
+ // As the sound would only happen for a really short period, calling
+ // checking wakelock first helps to ensure that we won't miss that moment.
+ waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: true,
+ isForegroundLock: true,
+ }),
+ createWebAudioDocument(tab, { stopTimeOffset: 0.1 }),
+ ]);
+ await ensureNeverAcquireVideoWakelock();
+
+ info(`audio wakelock should be released after web audio becomes silent`);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, false, {
+ needLock: false,
+ });
+ await ensureNeverAcquireVideoWakelock();
+
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+/**
+ * Following are helper functions.
+ */
+async function checkWakelockWhenChangeTabVisibility({
+ description,
+ additionalParams,
+ needLock,
+ elementIdForEnteringPIPMode,
+}) {
+ const originalTab = gBrowser.selectedTab;
+ info(`start a new tab for '${description}'`);
+ const mediaTab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ await createWebAudioDocument(mediaTab, additionalParams);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock,
+ isForegroundLock: true,
+ });
+ await ensureNeverAcquireVideoWakelock();
+
+ info(`switch media tab to background`);
+ await BrowserTestUtils.switchTab(window.gBrowser, originalTab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock,
+ isForegroundLock: false,
+ });
+ await ensureNeverAcquireVideoWakelock();
+
+ info(`switch media tab to foreground again`);
+ await BrowserTestUtils.switchTab(window.gBrowser, mediaTab);
+ await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock,
+ isForegroundLock: true,
+ });
+ await ensureNeverAcquireVideoWakelock();
+
+ info(`remove media tab`);
+ BrowserTestUtils.removeTab(mediaTab);
+}
+
+function createWebAudioDocument(tab, { stopTimeOffset, suspend } = {}) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [suspend, stopTimeOffset],
+ async (suspend, stopTimeOffset) => {
+ // Create an oscillatorNode to produce sound.
+ content.ac = new content.AudioContext();
+ const ac = content.ac;
+ const dest = ac.destination;
+ const source = new content.OscillatorNode(ac);
+ source.start(ac.currentTime);
+ source.connect(dest);
+
+ if (stopTimeOffset) {
+ source.stop(ac.currentTime + 0.1);
+ }
+
+ if (suspend) {
+ await content.ac.suspend();
+ } else {
+ while (ac.state != "running") {
+ info(`wait until AudioContext starts running`);
+ await new Promise(r => (ac.onstatechange = r));
+ }
+ info("AudioContext is running");
+ }
+ }
+ );
+}
+
+function ensureNeverAcquireVideoWakelock() {
+ // Web audio won't play any video, we never need video wakelock.
+ return waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { needLock: false });
+}
diff --git a/toolkit/content/tests/browser/browser_moz_support_link_open_links_in_chrome.js b/toolkit/content/tests/browser/browser_moz_support_link_open_links_in_chrome.js
new file mode 100644
index 0000000000..be5909b9c8
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_moz_support_link_open_links_in_chrome.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ Ensures that the moz-support-link opens links correctly when
+ this widget is used in the chrome.
+*/
+
+async function createMozSupportLink() {
+ await import("chrome://global/content/elements/moz-support-link.mjs");
+ let supportLink = document.createElement("a", { is: "moz-support-link" });
+ supportLink.setAttribute("support-page", "dnt");
+ let navigatorToolbox = document.getElementById("navigator-toolbox");
+ navigatorToolbox.appendChild(supportLink);
+
+ // If we do not wait for the element to be translated,
+ // then there will be no visible text.
+ await document.l10n.translateElements([supportLink]);
+ return supportLink;
+}
+
+add_task(async function test_open_link_in_chrome_with_keyboard() {
+ let supportTab;
+ // Open link with Enter key
+ let supportLink = await createMozSupportLink();
+ supportLink.focus();
+ const supportTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ Services.urlFormatter.formatURLPref("app.support.baseURL") + "dnt"
+ );
+ await EventUtils.synthesizeKey("KEY_Enter");
+ supportTab = await supportTabPromise;
+ Assert.ok(supportTab, "Support tab in new tab opened with Enter key");
+
+ await BrowserTestUtils.removeTab(supportTab);
+
+ let supportWindow;
+
+ // Open link with Shift + Enter key combination
+ supportLink.focus();
+ const supportWindowPromise = BrowserTestUtils.waitForNewWindow(
+ Services.urlFormatter.formatURLPref("app.support.baseURL") + "dnt"
+ );
+ await EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true });
+ supportWindow = await supportWindowPromise;
+ Assert.ok(
+ supportWindow,
+ "Support tab in new window opened with Shift+Enter key"
+ );
+ supportLink.remove();
+ await BrowserTestUtils.closeWindow(supportWindow);
+});
+
+add_task(async function test_open_link_in_chrome_with_mouse() {
+ let supportTab;
+ // Open link with mouse click
+
+ let supportLink = await createMozSupportLink();
+ const supportTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ Services.urlFormatter.formatURLPref("app.support.baseURL") + "dnt"
+ );
+ // This synthesize call works if you add a debugger statement before the call.
+ EventUtils.synthesizeMouseAtCenter(supportLink, {});
+
+ supportTab = await supportTabPromise;
+ Assert.ok(supportTab, "Support tab in new tab opened");
+
+ await BrowserTestUtils.removeTab(supportTab);
+
+ let supportWindow;
+
+ // Open link with Shift + mouse click combination
+ const supportWindowPromise = BrowserTestUtils.waitForNewWindow(
+ Services.urlFormatter.formatURLPref("app.support.baseURL") + "dnt"
+ );
+ await EventUtils.synthesizeMouseAtCenter(supportLink, { shiftKey: true });
+ supportWindow = await supportWindowPromise;
+ Assert.ok(supportWindow, "Support tab in new window opened");
+ await BrowserTestUtils.closeWindow(supportWindow);
+ supportLink = document.querySelector("a[support-page]");
+ supportLink.remove();
+});
diff --git a/toolkit/content/tests/browser/browser_quickfind_editable.js b/toolkit/content/tests/browser/browser_quickfind_editable.js
new file mode 100644
index 0000000000..7ece285602
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_quickfind_editable.js
@@ -0,0 +1,59 @@
+const PAGE =
+ "data:text/html,<div contenteditable>foo</div><input><textarea></textarea>";
+const DESIGNMODE_PAGE =
+ "data:text/html,<body onload='document.designMode=\"on\";'>";
+const HOTKEYS = ["/", "'"];
+
+async function test_hotkeys(browser, expected) {
+ let findbar = await gBrowser.getFindBar();
+ for (let key of HOTKEYS) {
+ is(findbar.hidden, true, "findbar is hidden");
+ await BrowserTestUtils.sendChar(key, gBrowser.selectedBrowser);
+ is(
+ findbar.hidden,
+ expected,
+ "findbar should" + (expected ? "" : " not") + " be hidden"
+ );
+ if (!expected) {
+ await closeFindbarAndWait(findbar);
+ }
+ }
+}
+
+async function focus_element(browser, query) {
+ await SpecialPowers.spawn(browser, [query], async function focus(query) {
+ let element = content.document.querySelector(query);
+ element.focus();
+ });
+}
+
+add_task(async function test_hotkey_on_editable_element() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function do_tests(browser) {
+ await test_hotkeys(browser, false);
+ const ELEMENTS = ["div", "input", "textarea"];
+ for (let elem of ELEMENTS) {
+ await focus_element(browser, elem);
+ await test_hotkeys(browser, true);
+ await focus_element(browser, ":root");
+ await test_hotkeys(browser, false);
+ }
+ }
+ );
+});
+
+add_task(async function test_hotkey_on_designMode_document() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: DESIGNMODE_PAGE,
+ },
+ async function do_tests(browser) {
+ await test_hotkeys(browser, true);
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_remoteness_change_listeners.js b/toolkit/content/tests/browser/browser_remoteness_change_listeners.js
new file mode 100644
index 0000000000..80e940ab29
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_remoteness_change_listeners.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that adding progress listeners to a browser doesn't break things
+ * when switching the remoteness of that browser.
+ */
+add_task(async function test_remoteness_switch_listeners() {
+ await BrowserTestUtils.withNewTab("about:support", async function (browser) {
+ let wpl;
+ let navigated = new Promise(resolve => {
+ wpl = {
+ onLocationChange() {
+ is(browser.currentURI.spec, "https://example.com/");
+ if (browser.currentURI?.spec == "https://example.com/") {
+ resolve();
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ Ci.nsISupportsWeakReference,
+ Ci.nsIWebProgressListener2,
+ Ci.nsIWebProgressListener,
+ ]),
+ };
+ browser.addProgressListener(wpl);
+ });
+
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ null,
+ "https://example.com/"
+ );
+ BrowserTestUtils.loadURIString(browser, "https://example.com/");
+ await Promise.all([loaded, navigated]);
+ browser.removeProgressListener(wpl);
+ });
+});
diff --git a/toolkit/content/tests/browser/browser_resume_bkg_video_on_tab_hover.js b/toolkit/content/tests/browser/browser_resume_bkg_video_on_tab_hover.js
new file mode 100644
index 0000000000..7dc9a8f42a
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_resume_bkg_video_on_tab_hover.js
@@ -0,0 +1,154 @@
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_silentAudioTrack.html";
+
+async function check_video_decoding_state(args) {
+ let video = content.document.getElementById("autoplay");
+ if (!video) {
+ ok(false, "Can't get the video element!");
+ }
+
+ let isSuspended = args.suspend;
+ let reload = args.reload;
+
+ if (reload) {
+ // It is too late to register event handlers when playback is half
+ // way done. Let's start playback from the beginning so we won't
+ // miss any events.
+ video.load();
+ video.play();
+ }
+
+ let state = isSuspended ? "suspended" : "resumed";
+ let event = isSuspended ? "mozentervideosuspend" : "mozexitvideosuspend";
+ return new Promise(resolve => {
+ video.addEventListener(
+ event,
+ function () {
+ ok(true, `Video decoding is ${state}.`);
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+async function check_should_send_unselected_tab_hover_msg(browser) {
+ info("did not update the value now, wait until it changes.");
+ if (browser.shouldHandleUnselectedTabHover) {
+ ok(
+ true,
+ "Should send unselected tab hover msg, someone is listening for it."
+ );
+ return true;
+ }
+ return BrowserTestUtils.waitForCondition(
+ () => browser.shouldHandleUnselectedTabHover,
+ "Should send unselected tab hover msg, someone is listening for it."
+ );
+}
+
+async function check_should_not_send_unselected_tab_hover_msg(browser) {
+ info("did not update the value now, wait until it changes.");
+ return BrowserTestUtils.waitForCondition(
+ () => !browser.shouldHandleUnselectedTabHover,
+ "Should not send unselected tab hover msg, no one is listening for it."
+ );
+}
+
+function get_video_decoding_suspend_promise(browser, reload) {
+ let suspend = true;
+ return SpecialPowers.spawn(
+ browser,
+ [{ suspend, reload }],
+ check_video_decoding_state
+ );
+}
+
+function get_video_decoding_resume_promise(browser) {
+ let suspend = false;
+ let reload = false;
+ return ContentTask.spawn(
+ browser,
+ { suspend, reload },
+ check_video_decoding_state
+ );
+}
+
+/**
+ * Because of bug1029451, we can't receive "mouseover" event correctly when
+ * we disable non-test mouse event. Therefore, we can't synthesize mouse event
+ * to simulate cursor hovering, so we temporarily use a hacky way to resume and
+ * suspend video decoding.
+ */
+function cursor_hover_over_tab_and_resume_video_decoding(browser) {
+ // TODO : simulate cursor hovering over the tab after fixing bug1029451.
+ browser.unselectedTabHover(true /* hover */);
+}
+
+function cursor_leave_tab_and_suspend_video_decoding(browser) {
+ // TODO : simulate cursor leaveing the tab after fixing bug1029451.
+ browser.unselectedTabHover(false /* leave */);
+}
+
+add_task(async function setup_test_preference() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.block-autoplay-until-in-foreground", false],
+ ["media.suspend-bkgnd-video.enabled", true],
+ ["media.suspend-bkgnd-video.delay-ms", 0],
+ ["media.resume-bkgnd-video-on-tabhover", true],
+ ],
+ });
+});
+
+/**
+ * TODO : add the following user-level tests after fixing bug1029451.
+ * test1 - only affect the unselected tab
+ * test2 - only affect the tab with suspended video
+ */
+add_task(async function resume_and_suspend_background_video_decoding() {
+ info("- open new background tab -");
+ let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("- before loading media, we shoudn't send the tab hover msg for tab -");
+ await check_should_not_send_unselected_tab_hover_msg(browser);
+ BrowserTestUtils.loadURIString(browser, PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("- should suspend background video decoding -");
+ await get_video_decoding_suspend_promise(browser, true);
+ await check_should_send_unselected_tab_hover_msg(browser);
+
+ info("- when cursor is hovering over the tab, resuming the video decoding -");
+ let promise = get_video_decoding_resume_promise(browser);
+ await cursor_hover_over_tab_and_resume_video_decoding(browser);
+ await promise;
+ await check_should_send_unselected_tab_hover_msg(browser);
+
+ info("- when cursor leaves the tab, suspending the video decoding -");
+ promise = get_video_decoding_suspend_promise(browser);
+ await cursor_leave_tab_and_suspend_video_decoding(browser);
+ await promise;
+ await check_should_send_unselected_tab_hover_msg(browser);
+
+ info("- select video's owner tab as foreground tab, should resume video -");
+ promise = get_video_decoding_resume_promise(browser);
+ await BrowserTestUtils.switchTab(window.gBrowser, tab);
+ await promise;
+ await check_should_send_unselected_tab_hover_msg(browser);
+
+ info("- video's owner tab goes to background again, should suspend video -");
+ promise = get_video_decoding_suspend_promise(browser);
+ let blankTab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ await promise;
+ await check_should_send_unselected_tab_hover_msg(browser);
+
+ info("- remove tabs -");
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(blankTab);
+});
diff --git a/toolkit/content/tests/browser/browser_richlistbox_keyboard.js b/toolkit/content/tests/browser/browser_richlistbox_keyboard.js
new file mode 100644
index 0000000000..a1287335b4
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_richlistbox_keyboard.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_richlistbox_keyboard() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await BrowserTestUtils.withNewTab("about:about", browser => {
+ let document = browser.contentDocument;
+ let box = document.createXULElement("richlistbox");
+
+ function checkTabIndices(selectedLine) {
+ for (let button of box.querySelectorAll(`.line${selectedLine} button`)) {
+ is(
+ button.tabIndex,
+ 0,
+ `Should have ensured buttons inside selected line ${selectedLine} are focusable`
+ );
+ }
+ for (let otherButton of box.querySelectorAll(
+ `richlistitem:not(.line${selectedLine}) button`
+ )) {
+ is(
+ otherButton.tabIndex,
+ -1,
+ `Should have ensured buttons outside selected line ${selectedLine} are not focusable`
+ );
+ }
+ }
+
+ let poem = `I wandered lonely as a cloud
+ That floats on high o'er vales and hills
+ When all at once I saw a crowd
+ A host, of golden daffodils;
+ Beside the lake, beneath the trees,
+ Fluttering and dancing in the breeze.`;
+ let items = poem.split("\n").map((line, index) => {
+ let item = document.createXULElement("richlistitem");
+ item.className = `line${index + 1}`;
+ let button1 = document.createXULElement("button");
+ button1.textContent = "Like";
+ let button2 = document.createXULElement("button");
+ button2.textContent = "Subscribe";
+ item.append(line.trim(), button1, button2);
+ return item;
+ });
+ box.append(...items);
+ document.body.prepend(box);
+ box.focus();
+ box.getBoundingClientRect(); // force a flush
+ box.selectedItem = box.firstChild;
+ checkTabIndices(1);
+ EventUtils.synthesizeKey("VK_DOWN", {}, document.defaultView);
+ is(
+ box.selectedItem.className,
+ "line2",
+ "Should have moved selection to the next line."
+ );
+ checkTabIndices(2);
+ EventUtils.synthesizeKey("VK_TAB", {}, document.defaultView);
+ is(
+ document.activeElement,
+ box.selectedItem.querySelector("button"),
+ "Initial button gets focus in the selected list item."
+ );
+ EventUtils.synthesizeKey("VK_UP", {}, document.defaultView);
+ checkTabIndices(1);
+ is(
+ document.activeElement,
+ box.selectedItem.querySelector("button"),
+ "Initial button gets focus in the selected list item when moving up with arrow key."
+ );
+ EventUtils.synthesizeKey("VK_DOWN", {}, document.defaultView);
+ checkTabIndices(2);
+ is(
+ document.activeElement,
+ box.selectedItem.querySelector("button"),
+ "Initial button gets focus in the selected list item when moving down with arrow key."
+ );
+ });
+});
diff --git a/toolkit/content/tests/browser/browser_saveImageURL.js b/toolkit/content/tests/browser/browser_saveImageURL.js
new file mode 100644
index 0000000000..c936b8ef84
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_saveImageURL.js
@@ -0,0 +1,76 @@
+"use strict";
+
+const IMAGE_PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/image_page.html";
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+
+MockFilePicker.init(window);
+MockFilePicker.returnValue = MockFilePicker.returnCancel;
+
+registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+});
+
+function waitForFilePicker() {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ MockFilePicker.showCallback = null;
+ ok(true, "Saw the file picker");
+ resolve();
+ };
+ });
+}
+
+/**
+ * Test that internalSave works when saving an image like the context menu does.
+ */
+add_task(async function preferred_API() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: IMAGE_PAGE,
+ },
+ async function (browser) {
+ let url = await SpecialPowers.spawn(browser, [], async function () {
+ let image = content.document.getElementById("image");
+ return image.href;
+ });
+
+ let filePickerPromise = waitForFilePicker();
+ internalSave(
+ url,
+ null, // originalURL
+ null, // document
+ "image.jpg",
+ null, // content disposition
+ "image/jpeg",
+ true, // bypass cache
+ null, // dialog title key
+ null, // chosen data
+ null, // no referrer info
+ null, // no document
+ false, // don't skip the filename prompt
+ null, // cache key
+ false, // not private.
+ gBrowser.contentPrincipal
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ let channel = docShell.currentDocumentChannel;
+ if (channel) {
+ todo(
+ channel.QueryInterface(Ci.nsIHttpChannelInternal)
+ .channelIsForDownload
+ );
+
+ // Throttleable is the only class flag assigned to downloads.
+ todo(
+ channel.QueryInterface(Ci.nsIClassOfService).classFlags ==
+ Ci.nsIClassOfService.Throttleable
+ );
+ }
+ });
+ await filePickerPromise;
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/browser_save_folder_standalone_image.js b/toolkit/content/tests/browser/browser_save_folder_standalone_image.js
new file mode 100644
index 0000000000..68b5a1f113
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_save_folder_standalone_image.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * TestCase for bug 1726801
+ * <https://bugzilla.mozilla.org/show_bug.cgi?id=1726801>
+ *
+ * Load an image in a standalone tab and verify that the per-site download
+ * folder is correctly retrieved when using "Save Page As" to save the image.
+ */
+
+/*
+ * ================
+ * Helper functions
+ * ================
+ */
+
+async function setFile(downloadLastDir, aURI, aValue) {
+ downloadLastDir.setFile(aURI, aValue);
+ await TestUtils.waitForTick();
+}
+
+function newDirectory() {
+ let tmpDir = FileUtils.getDir("TmpD", [], true);
+ let dir = tmpDir.clone();
+ dir.append("testdir");
+ dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ return dir;
+}
+
+function clearHistory() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+}
+
+async function clearHistoryAndWait() {
+ clearHistory();
+ await TestUtils.waitForTick();
+ await TestUtils.waitForTick();
+}
+
+/*
+ * ====
+ * Test
+ * ====
+ */
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+add_task(async function () {
+ const IMAGE_URL =
+ "http://mochi.test:8888/browser/toolkit/content/tests/browser/doggy.png";
+
+ await BrowserTestUtils.withNewTab(IMAGE_URL, async function (browser) {
+ let tmpDir = FileUtils.getDir("TmpD", [], true);
+ let dir = newDirectory();
+ let downloadLastDir = new DownloadLastDir(null);
+ // Set the desired target directory for the IMAGE_URL
+ await setFile(downloadLastDir, IMAGE_URL, dir);
+ // Ensure that "browser.download.lastDir" points to a different directory
+ await setFile(downloadLastDir, null, tmpDir);
+ registerCleanupFunction(async function () {
+ await clearHistoryAndWait();
+ dir.remove(true);
+ });
+
+ // Prepare mock file picker.
+ let showFilePickerPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = fp => resolve(fp.displayDirectory.path);
+ });
+ registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+ });
+
+ // Run "Save Page As"
+ EventUtils.synthesizeKey("s", { accelKey: true });
+
+ let dirPath = await showFilePickerPromise;
+ is(dirPath, dir.path, "Verify proposed download folder.");
+ });
+});
diff --git a/toolkit/content/tests/browser/browser_save_resend_postdata.js b/toolkit/content/tests/browser/browser_save_resend_postdata.js
new file mode 100644
index 0000000000..13db21a389
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_save_resend_postdata.js
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+/**
+ * Test for bug 471962 <https://bugzilla.mozilla.org/show_bug.cgi?id=471962>:
+ * When saving an inner frame as file only, the POST data of the outer page is
+ * sent to the address of the inner page.
+ *
+ * Test for bug 485196 <https://bugzilla.mozilla.org/show_bug.cgi?id=485196>:
+ * Web page generated by POST is retried as GET when Save Frame As used, and the
+ * page is no longer in the cache.
+ */
+function test() {
+ waitForExplicitFinish();
+
+ BrowserTestUtils.loadURIString(
+ gBrowser,
+ "http://mochi.test:8888/browser/toolkit/content/tests/browser/data/post_form_outer.sjs"
+ );
+
+ gBrowser.addEventListener("pageshow", function pageShown(event) {
+ if (event.target.location == "about:blank") {
+ return;
+ }
+ gBrowser.removeEventListener("pageshow", pageShown);
+
+ // Submit the form in the outer page, then wait for both the outer
+ // document and the inner frame to be loaded again.
+ gBrowser.addEventListener("DOMContentLoaded", handleOuterSubmit);
+ gBrowser.contentDocument.getElementById("postForm").submit();
+ });
+
+ var framesLoaded = 0;
+ var innerFrame;
+
+ function handleOuterSubmit() {
+ if (++framesLoaded < 2) {
+ return;
+ }
+
+ gBrowser.removeEventListener("DOMContentLoaded", handleOuterSubmit);
+
+ innerFrame = gBrowser.contentDocument.getElementById("innerFrame");
+
+ // Submit the form in the inner page.
+ gBrowser.addEventListener("DOMContentLoaded", handleInnerSubmit);
+ innerFrame.contentDocument.getElementById("postForm").submit();
+ }
+
+ function handleInnerSubmit() {
+ gBrowser.removeEventListener("DOMContentLoaded", handleInnerSubmit);
+
+ // Create the folder the page will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var file = destDir.clone();
+ file.append("no_default_file_name");
+ MockFilePicker.setFiles([file]);
+ MockFilePicker.showCallback = function (fp) {
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ };
+
+ mockTransferCallback = onTransferComplete;
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ destDir.remove(true);
+ });
+
+ var docToSave = innerFrame.contentDocument;
+ // We call internalSave instead of saveDocument to bypass the history
+ // cache.
+ internalSave(
+ docToSave.location.href,
+ null,
+ docToSave,
+ null,
+ null,
+ docToSave.contentType,
+ false,
+ null,
+ null,
+ docToSave.referrer ? makeURI(docToSave.referrer) : null,
+ docToSave,
+ false,
+ null
+ );
+ }
+
+ function onTransferComplete(downloadSuccess) {
+ ok(
+ downloadSuccess,
+ "The inner frame should have been downloaded successfully"
+ );
+
+ // Read the entire saved file.
+ var file = MockFilePicker.getNsIFile();
+ var fileContents = readShortFile(file);
+
+ // Check if outer POST data is found (bug 471962).
+ is(
+ fileContents.indexOf("inputfield=outer"),
+ -1,
+ "The saved inner frame does not contain outer POST data"
+ );
+
+ // Check if inner POST data is found (bug 485196).
+ isnot(
+ fileContents.indexOf("inputfield=inner"),
+ -1,
+ "The saved inner frame was generated using the correct POST data"
+ );
+
+ finish();
+ }
+}
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+function createTemporarySaveDirectory() {
+ var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ return saveDir;
+}
+
+/**
+ * Reads the contents of the provided short file (up to 1 MiB).
+ *
+ * @param aFile
+ * nsIFile object pointing to the file to be read.
+ *
+ * @return
+ * String containing the raw octets read from the file.
+ */
+function readShortFile(aFile) {
+ var inputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ inputStream.init(aFile, -1, 0, 0);
+ try {
+ var scrInputStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ scrInputStream.init(inputStream);
+ try {
+ // Assume that the file is much shorter than 1 MiB.
+ return scrInputStream.read(1048576);
+ } finally {
+ // Close the scriptable stream after reading, even if the operation
+ // failed.
+ scrInputStream.close();
+ }
+ } finally {
+ // Close the stream after reading, if it is still open, even if the read
+ // operation failed.
+ inputStream.close();
+ }
+}
diff --git a/toolkit/content/tests/browser/browser_starting_autoscroll_in_about_content.js b/toolkit/content/tests/browser/browser_starting_autoscroll_in_about_content.js
new file mode 100644
index 0000000000..f0daa480d4
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_starting_autoscroll_in_about_content.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testStartingAutoScrollInAboutContent() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab("about:support", async function (browser) {
+ let autoScroller;
+ let promiseStartAutoScroll = new Promise(resolve => {
+ let onPopupShown = event => {
+ if (event.originalTarget.id != "autoscroller") {
+ return;
+ }
+ autoScroller = event.originalTarget;
+ info('"popupshown" event is fired');
+ autoScroller.getBoundingClientRect(); // Flush layout of the autoscroller
+ resolve();
+ };
+ window.addEventListener("popupshown", onPopupShown, { capture: true });
+ registerCleanupFunction(() => {
+ window.removeEventListener("popupshown", onPopupShown, {
+ capture: true,
+ });
+ });
+ });
+
+ ok(!browser.isRemoteBrowser, "Browser should not be remote.");
+ await ContentTask.spawn(browser, null, async function () {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.documentElement.scrollHeight >
+ content.document.documentElement.clientHeight,
+ "The document should become scrollable"
+ );
+ });
+
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: browser,
+ offsetX: 10,
+ offsetY: 10, // XXX Assuming that there is no interactive content here.
+ });
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousedown",
+ button: 1, // middle click
+ target: browser,
+ offsetX: 10,
+ offsetY: 10,
+ });
+ info("Waiting to start autoscrolling");
+ await promiseStartAutoScroll;
+ ok(autoScroller != null, "Autoscrolling should be started");
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mouseup",
+ button: 1, // middle click
+ target: browser,
+ offsetX: 10,
+ offsetY: 10,
+ }); // release implicit capture
+ EventUtils.synthesizeKey("KEY_Escape"); // Close autoscroller
+ await TestUtils.waitForCondition(
+ () => autoScroller.state == "closed",
+ "autoscroll should be canceled"
+ );
+ });
+});
diff --git a/toolkit/content/tests/browser/browser_suspend_videos_outside_viewport.js b/toolkit/content/tests/browser/browser_suspend_videos_outside_viewport.js
new file mode 100644
index 0000000000..333bd0d41c
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_suspend_videos_outside_viewport.js
@@ -0,0 +1,39 @@
+/**
+ * This test is used to ensure we suspend video decoding if video is not in the
+ * viewport.
+ */
+"use strict";
+
+const PAGE =
+ "https://example.com/browser/toolkit/content/tests/browser/file_outside_viewport_videos.html";
+
+async function test_suspend_video_decoding() {
+ let videos = content.document.getElementsByTagName("video");
+ for (let video of videos) {
+ info(`- start video on the ${video.id} side and outside the viewport -`);
+ await video.play();
+ ok(true, `video started playing`);
+ ok(video.isVideoDecodingSuspended, `video decoding is suspended`);
+ }
+}
+
+add_task(async function setup_test_preference() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.suspend-bkgnd-video.enabled", true],
+ ["media.suspend-bkgnd-video.delay-ms", 0],
+ ],
+ });
+});
+
+add_task(async function start_test() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], test_suspend_video_decoding);
+ }
+ );
+});
diff --git a/toolkit/content/tests/browser/common/mockTransfer.js b/toolkit/content/tests/browser/common/mockTransfer.js
new file mode 100644
index 0000000000..f4afa44903
--- /dev/null
+++ b/toolkit/content/tests/browser/common/mockTransfer.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/MockObjects.js",
+ this
+);
+
+var mockTransferCallback;
+
+/**
+ * This "transfer" object implementation continues the currently running test
+ * when the download is completed, reporting true for success or false for
+ * failure as the first argument of the testRunner.continueTest function.
+ */
+function MockTransfer() {
+ this._downloadIsSuccessful = true;
+}
+
+MockTransfer.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsIWebProgressListener2",
+ "nsITransfer",
+ ]),
+
+ /* nsIWebProgressListener */
+ onStateChange: function MTFC_onStateChange(
+ aWebProgress,
+ aRequest,
+ aStateFlags,
+ aStatus
+ ) {
+ // If at least one notification reported an error, the download failed.
+ if (!Components.isSuccessCode(aStatus)) {
+ this._downloadIsSuccessful = false;
+ }
+
+ // If the download is finished
+ if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
+ ) {
+ // Continue the test, reporting the success or failure condition.
+ mockTransferCallback(this._downloadIsSuccessful);
+ }
+ },
+ onProgressChange() {},
+ onLocationChange() {},
+ onStatusChange: function MTFC_onStatusChange(
+ aWebProgress,
+ aRequest,
+ aStatus,
+ aMessage
+ ) {
+ // If at least one notification reported an error, the download failed.
+ if (!Components.isSuccessCode(aStatus)) {
+ this._downloadIsSuccessful = false;
+ }
+ },
+ onSecurityChange() {},
+ onContentBlockingEvent() {},
+
+ /* nsIWebProgressListener2 */
+ onProgressChange64() {},
+ onRefreshAttempted() {},
+
+ /* nsITransfer */
+ init() {},
+ initWithBrowsingContext() {},
+ setSha256Hash() {},
+ setSignatureInfo() {},
+};
+
+// Create an instance of a MockObjectRegisterer whose methods can be used to
+// temporarily replace the default "@mozilla.org/transfer;1" object factory with
+// one that provides the mock implementation above. To activate the mock object
+// factory, call the "register" method. Starting from that moment, all the
+// transfer objects that are requested will be mock objects, until the
+// "unregister" method is called.
+var mockTransferRegisterer = new MockObjectRegisterer(
+ "@mozilla.org/transfer;1",
+ MockTransfer
+);
diff --git a/toolkit/content/tests/browser/data/post_form_inner.sjs b/toolkit/content/tests/browser/data/post_form_inner.sjs
new file mode 100644
index 0000000000..14ce93ab84
--- /dev/null
+++ b/toolkit/content/tests/browser/data/post_form_inner.sjs
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ var body =
+ "<html>\
+ <body>\
+ Inner POST data: ";
+
+ var bodyStream = new BinaryInputStream(request.bodyInputStream);
+ var avail = 0;
+ while ((avail = bodyStream.available()) > 0) {
+ body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail));
+ }
+
+ body +=
+ '<form id="postForm" action="post_form_inner.sjs" method="post">\
+ <input type="text" name="inputfield" value="inner">\
+ <input type="submit">\
+ </form>\
+ </body>\
+ </html>';
+
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/toolkit/content/tests/browser/data/post_form_outer.sjs b/toolkit/content/tests/browser/data/post_form_outer.sjs
new file mode 100644
index 0000000000..0826a47cd0
--- /dev/null
+++ b/toolkit/content/tests/browser/data/post_form_outer.sjs
@@ -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/. */
+
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ var body =
+ "<html>\
+ <body>\
+ Outer POST data: ";
+
+ var bodyStream = new BinaryInputStream(request.bodyInputStream);
+ var avail = 0;
+ while ((avail = bodyStream.available()) > 0) {
+ body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail));
+ }
+
+ body +=
+ '<form id="postForm" action="post_form_outer.sjs" method="post">\
+ <input type="text" name="inputfield" value="outer">\
+ <input type="submit">\
+ </form>\
+ \
+ <iframe id="innerFrame" src="post_form_inner.sjs" width="400" height="200">\
+ \
+ </body>\
+ </html>';
+
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/toolkit/content/tests/browser/datetime/browser.ini b/toolkit/content/tests/browser/datetime/browser.ini
new file mode 100644
index 0000000000..e4991df5f0
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser.ini
@@ -0,0 +1,74 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_datetime_blur.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+[browser_datetime_datepicker.js]
+# This file was skipped before new tests were written based on it in Bug 1676068
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_datetime_datepicker_clear.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_datetime_datepicker_focus.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_datetime_datepicker_keynav.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_datetime_datepicker_markup.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_datetime_datepicker_min_max.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+[browser_datetime_datepicker_monthyear.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_datetime_datepicker_mousenav.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+[browser_datetime_datepicker_prev_next_month.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+[browser_datetime_showPicker.js]
+# do not skip
+[browser_datetime_toplevel.js]
+[browser_spinner.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
+[browser_spinner_keynav.js]
+skip-if =
+ tsan # Frequently times out on TSan
+ os == "win" && asan && fission # fails on asan/fission
+ os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_blur.js b/toolkit/content/tests/browser/datetime/browser_datetime_blur.js
new file mode 100644
index 0000000000..0671661e31
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_blur.js
@@ -0,0 +1,265 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE_CONTENT = `data:text/html,
+ <body onload='gBlurEvents = 0; gDateFocusEvents = 0; gTextFocusEvents = 0'>
+ <input type='date' id='date' onfocus='gDateFocusEvents++' onblur='gBlurEvents++'>
+ <input type='text' id='text' onfocus='gTextFocusEvents++'>
+ </body>`;
+
+function getBlurEvents() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.gBlurEvents;
+ });
+}
+
+function getDateFocusEvents() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.gDateFocusEvents;
+ });
+}
+
+function getTextFocusEvents() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.gTextFocusEvents;
+ });
+}
+
+/**
+ * Test that when a picker panel is opened by an input
+ * the input is not blurred
+ */
+add_task(async function test_parent_blur() {
+ info(
+ "Test that when a picker panel is opened by an input the parent is not blurred"
+ );
+
+ // Set "prefers-reduced-motion" media to "reduce"
+ // to avoid intermittent scroll failures (1803612, 1803687)
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 1]],
+ });
+ Assert.ok(
+ matchMedia("(prefers-reduced-motion: reduce)").matches,
+ "The reduce motion mode is active"
+ );
+
+ await helper.openPicker(PAGE_CONTENT, false, "showPicker");
+
+ Assert.equal(
+ await getDateFocusEvents(),
+ 0,
+ "Date input field is not calling a focus event when the '.showPicker()' method is called"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const input = content.document.querySelector("#date");
+
+ Assert.ok(
+ !input.matches(":focus"),
+ `The keyboard focus is not placed on the date input after showPicker is called`
+ );
+ });
+
+ let closedOnEsc = helper.promisePickerClosed();
+
+ // Close a date picker
+ EventUtils.synthesizeKey("KEY_Escape", {});
+
+ await closedOnEsc;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed on Escape"
+ );
+ Assert.equal(
+ await getDateFocusEvents(),
+ 0,
+ "Date input field is not focused when its picker is dismissed with Escape key"
+ );
+ Assert.equal(
+ await getBlurEvents(),
+ 0,
+ "Date input field is not blurred when the picker is closed with Escape key"
+ );
+
+ // Ensure focus is on the input field
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const input = content.document.querySelector("#date");
+
+ input.focus();
+
+ Assert.ok(
+ input.matches(":focus"),
+ `The keyboard focus is placed on the date input field`
+ );
+ });
+ Assert.equal(
+ await getDateFocusEvents(),
+ 1,
+ "A focus event was fired on the Date input field"
+ );
+
+ let readyOnKey = helper.waitForPickerReady();
+
+ // Open a date picker
+ EventUtils.synthesizeKey(" ", {});
+
+ await readyOnKey;
+
+ Assert.equal(
+ helper.panel.state,
+ "open",
+ "Date picker panel should be opened"
+ );
+ Assert.equal(
+ helper.panel
+ .querySelector("#dateTimePopupFrame")
+ .contentDocument.activeElement.getAttribute("role"),
+ "gridcell",
+ "The picker is opened and a calendar day is focused"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const input = content.document.querySelector("#date");
+
+ Assert.ok(
+ input.matches(":focus"),
+ `The keyboard focus is retained on the date input field`
+ );
+ Assert.equal(
+ input,
+ content.document.activeElement,
+ "Input field does not loose focus when its picker is opened and focused"
+ );
+ });
+
+ Assert.equal(
+ await getBlurEvents(),
+ 0,
+ "Date input field is not blurred when its picker is opened and focused"
+ );
+ Assert.equal(
+ await getDateFocusEvents(),
+ 1,
+ "No new focus events were fired on the Date input while its picker is opened"
+ );
+
+ info(
+ `Test that the date input field is not blurred after interacting
+ with a month-year panel`
+ );
+
+ // Move focus from the today's date to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+
+ Assert.ok(
+ helper.getElement(BTN_MONTH_YEAR).matches(":focus"),
+ "The month-year toggle button is focused"
+ );
+
+ // Open the month-year selection panel:
+ EventUtils.synthesizeKey(" ", {});
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "true",
+ "Month-year button is expanded when the spinners are shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is visible"
+ );
+
+ // Move focus from the month-year toggle button to the year spinner:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+
+ // Change the year spinner value from February 2023 to March 2023:
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const input = content.document.querySelector("#date");
+
+ Assert.ok(
+ input.matches(":focus"),
+ `The keyboard focus is retained on the date input field`
+ );
+ Assert.equal(
+ input,
+ content.document.activeElement,
+ "Input field does not loose focus when the month-year picker is opened and interacted with"
+ );
+ });
+
+ Assert.equal(
+ await getBlurEvents(),
+ 0,
+ "Date input field is not blurred after interacting with a month-year panel"
+ );
+
+ info(`Test that when a picker panel is opened and then it is closed
+ with a click on the other field, the focus is updated`);
+
+ let closedOnClick = helper.promisePickerClosed();
+
+ // Close a picker by clicking on another input
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#text",
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ await closedOnClick;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed when another element is clicked"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ const inputText = content.document.querySelector("#text");
+ const inputDate = content.document.querySelector("#date");
+
+ Assert.ok(
+ inputText.matches(":focus"),
+ `The keyboard focus is moved to the text input field`
+ );
+ Assert.equal(
+ inputText,
+ content.document.activeElement,
+ "Text input field gains a focus when clicked"
+ );
+ Assert.ok(
+ !inputDate.matches(":focus"),
+ `The keyboard focus is moved from the date input field`
+ );
+ Assert.notEqual(
+ inputDate,
+ content.document.activeElement,
+ "Date input field is not focused anymore"
+ );
+ });
+
+ Assert.equal(
+ await getBlurEvents(),
+ 1,
+ "Date input field is blurred when focus is moved to the text input field"
+ );
+ Assert.equal(
+ await getTextFocusEvents(),
+ 1,
+ "Text input field is focused when it is clicked"
+ );
+ Assert.equal(
+ await getDateFocusEvents(),
+ 1,
+ "No new focus events were fired on the Date input after its picker was closed"
+ );
+
+ await helper.tearDown();
+ // Clear the prefers-reduced-motion pref from the test profile:
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js
new file mode 100644
index 0000000000..b7c4df8d2a
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js
@@ -0,0 +1,369 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Create a list of abbreviations for calendar class names
+const W = "weekend",
+ O = "outside",
+ S = "selection",
+ R = "out-of-range",
+ T = "today",
+ P = "off-step";
+
+// Calendar classlist for 2016-12. Used to verify the classNames are correct.
+const calendarClasslist_201612 = [
+ [W, O],
+ [O],
+ [O],
+ [O],
+ [],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [S],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [W],
+ [W, O],
+ [O],
+ [O],
+ [O],
+ [O],
+ [O],
+ [W, O],
+];
+
+/**
+ * Test that date picker opens to today's date when input field is blank
+ */
+add_task(async function test_datepicker_today() {
+ info("Test that date picker opens to today's date when input field is blank");
+
+ const date = new Date();
+
+ await helper.openPicker("data:text/html, <input type='date'>");
+
+ if (date.getMonth() === new Date().getMonth()) {
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT_LOCAL(date),
+ "Today's date is opened"
+ );
+ Assert.equal(
+ helper.getElement(DAY_TODAY).getAttribute("aria-current"),
+ "date",
+ "Today's date is programmatically current"
+ );
+ Assert.equal(
+ helper.getElement(DAY_TODAY).getAttribute("tabindex"),
+ "0",
+ "Today's date is included in the focus order, when nothing is selected"
+ );
+ } else {
+ Assert.ok(
+ true,
+ "Skipping datepicker today test if month changes when opening picker."
+ );
+ }
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens to the correct month, with calendar days
+ * displayed correctly, given a date value is set.
+ */
+add_task(async function test_datepicker_open() {
+ info("Test the date picker markup with a set input date value");
+
+ const inputValue = "2016-12-15";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "2016-12-15 date is opened"
+ );
+
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "27",
+ "28",
+ "29",
+ "30",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15",
+ "16",
+ "17",
+ "18",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "24",
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ ],
+ "Calendar text for 2016-12 is correct"
+ );
+ Assert.deepEqual(
+ getCalendarClassList(),
+ calendarClasslist_201612,
+ "2016-12 classNames of the picker are correct"
+ );
+ Assert.equal(
+ helper.getElement(DAY_SELECTED).getAttribute("aria-selected"),
+ "true",
+ "Chosen date is programmatically selected"
+ );
+ Assert.equal(
+ helper.getElement(DAY_SELECTED).getAttribute("tabindex"),
+ "0",
+ "Selected date is included in the focus order"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure that the datepicker popup appears correctly positioned when
+ * the input field has been transformed.
+ */
+add_task(async function test_datepicker_transformed_position() {
+ const inputValue = "2016-12-15";
+
+ const style =
+ "transform: translateX(7px) translateY(13px); border-top: 2px; border-left: 5px; margin: 30px;";
+ const iframeContent = `<input id="date" type="date" value="${inputValue}" style="${style}">`;
+ await helper.openPicker(
+ "data:text/html,<iframe id='iframe' src='http://example.net/document-builder.sjs?html=" +
+ encodeURI(iframeContent) +
+ "'>",
+ true
+ );
+
+ let bc = helper.tab.linkedBrowser.browsingContext.children[0];
+ await verifyPickerPosition(bc, "date");
+
+ await helper.tearDown();
+});
+
+/**
+ * Make sure picker is in correct state when it is reopened.
+ */
+add_task(async function test_datepicker_reopen_state() {
+ const inputValue = "2016-12-15";
+ const nextMonth = "2017-01-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ // Navigate to the next month but do not commit the change
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue))
+ );
+
+ helper.click(helper.getElement(BTN_NEXT_MONTH));
+
+ // January 2017
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextMonth))
+ );
+
+ let closed = helper.promisePickerClosed();
+
+ EventUtils.synthesizeKey("KEY_Escape", {});
+
+ await closed;
+
+ Assert.equal(helper.panel.state, "closed", "Panel should be closed");
+
+ // December 2016
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let input = content.document.querySelector("input");
+ Assert.equal(
+ input.value,
+ "2016-12-15",
+ "The input value remains unchanged after the picker is dismissed"
+ );
+ });
+
+ let ready = helper.waitForPickerReady();
+
+ // Move focus from the browser to an input field and open a picker:
+ EventUtils.synthesizeKey("KEY_Tab", {});
+ EventUtils.synthesizeKey(" ", {});
+
+ await ready;
+
+ Assert.equal(helper.panel.state, "open", "Panel should be opened");
+
+ // December 2016
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue))
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When step attribute is set, calendar should show some dates as off-step.
+ */
+add_task(async function test_datepicker_step() {
+ const inputValue = "2016-12-15";
+ const inputStep = "5";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">`
+ );
+
+ Assert.deepEqual(
+ getCalendarClassList(),
+ mergeArrays(calendarClasslist_201612, [
+ // P denotes off-step
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ [P],
+ [],
+ [P],
+ [P],
+ [P],
+ ]),
+ "2016-12 with step"
+ );
+
+ await helper.tearDown();
+});
+
+// This test checks if the change event is considered as user input event.
+add_task(async function test_datepicker_handling_user_input() {
+ await helper.openPicker(`data:text/html, <input type="date">`);
+
+ let changeEventPromise = helper.promiseChange();
+
+ // Click the first item (top-left corner) of the calendar
+ helper.click(helper.getElement(DAYS_VIEW).children[0]);
+ await changeEventPromise;
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure datetime-local picker closes when selection is made.
+ */
+add_task(async function test_datetime_focus_to_input() {
+ info("Ensure datetime-local picker closes when focus moves to a time input");
+
+ await helper.openPicker(
+ `data:text/html,<input id=datetime type=datetime-local>`
+ );
+ let browser = helper.tab.linkedBrowser;
+ await verifyPickerPosition(browser, "datetime");
+
+ Assert.equal(helper.panel.state, "open", "Panel should be visible");
+
+ // Make selection to close the date dialog
+ await EventUtils.synthesizeKey(" ", {});
+
+ let closed = helper.promisePickerClosed();
+
+ await closed;
+
+ Assert.equal(helper.panel.state, "closed", "Panel should be closed now");
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js
new file mode 100644
index 0000000000..3c3de2dc98
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function testClear(key) {
+ const inputValue = "2023-03-03";
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+ let browser = helper.tab.linkedBrowser;
+
+ Assert.equal(helper.panel.state, "open", "Panel should be opened");
+
+ let closed = helper.promisePickerClosed();
+
+ // Clear the input fields
+ if (key) {
+ // Move focus from the selected date to the Clear button:
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ Assert.ok(
+ helper.getElement(BTN_CLEAR).matches(":focus"),
+ "The Clear button can receive keyboard focus"
+ );
+
+ EventUtils.synthesizeKey(key, {});
+ } else {
+ helper.click(helper.getElement(BTN_CLEAR));
+ }
+
+ await closed;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ is(
+ content.document.querySelector("input").value,
+ "",
+ "The input value is reset after the Clear button is pressed"
+ );
+ });
+
+ await helper.tearDown();
+}
+
+add_task(async function test_datepicker_clear_keyboard() {
+ await testClear(" ");
+});
+
+add_task(async function test_datepicker_clear_keyboard_enter() {
+ await testClear("KEY_Enter");
+});
+
+add_task(async function test_datepicker_clear_mouse() {
+ await testClear();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.js
new file mode 100644
index 0000000000..5a9a1ff1f9
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.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/. */
+
+"use strict";
+
+/**
+ * Ensure navigating through Datepicker using keyboard after a date
+ * has already been selected will keep the keyboard focus
+ * when reaching a different month (bug 1804466).
+ */
+add_task(async function test_focus_after_selection() {
+ info(
+ `Ensure navigating through Datepicker using keyboard after a date has already been selected will not lose keyboard focus when reaching a different month.`
+ );
+
+ // Set "prefers-reduced-motion" media to "reduce"
+ // to avoid intermittent scroll failures (1803612, 1803687)
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 1]],
+ });
+ Assert.ok(
+ matchMedia("(prefers-reduced-motion: reduce)").matches,
+ "The reduce motion mode is active"
+ );
+
+ const inputValue = "2022-12-12";
+ const prevMonth = "2022-10-01";
+ const nextYear = "2023-11-01";
+ const nextYearAfter = "2024-01-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value=${inputValue}>`
+ );
+ let browser = helper.tab.linkedBrowser;
+
+ info("Test behavior when selection is done on the calendar grid");
+
+ // Move focus from 2022-12-12 to 2022-10-24 by week
+ // Changing 2 month views along the way:
+ EventUtils.synthesizeKey("KEY_ArrowUp", { repeat: 7 });
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ "The calendar is updated to show the second previous month (2022-10)."
+ );
+
+ // 2022-10-24:
+ const focusedDayEl = getDayEl(24);
+
+ Assert.ok(
+ focusedDayEl.matches(":focus"),
+ "An expected focusable day within a calendar grid is focused"
+ );
+
+ let closed = helper.promisePickerClosed();
+
+ // Make a selection and close the picker
+ EventUtils.synthesizeKey(" ", {});
+
+ // Check the focus is returned to main browser window when a panel is closed
+ await SpecialPowers.spawn(browser, [], async () => {
+ const body = content.document.body;
+ // Testing the focus position within content:
+ Assert.deepEqual(
+ body,
+ content.document.activeElement,
+ `The main content's <body> received programmatic focus`
+ );
+ });
+
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel is closed when the selection is made"
+ );
+
+ let ready = helper.waitForPickerReady();
+
+ // Move the keyboard focus to the input field to reopen the picker
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ // Check the focus is returned to the Calendar button
+ await SpecialPowers.spawn(browser, [], async () => {
+ const input = content.document.querySelector("input");
+ // Testing the focus position within content:
+ Assert.deepEqual(
+ input,
+ content.document.activeElement,
+ `The input field includes programmatic focus`
+ );
+ });
+
+ // Reopen the picker
+ EventUtils.synthesizeKey(" ", {});
+
+ await ready;
+
+ Assert.equal(helper.panel.state, "open", "Panel is reopened");
+
+ // Move focus from 2022-10-24 to 2022-12-12 by week
+ // Changing 2 month views along the way:
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 7 });
+
+ // 2022-12-12:
+ const focusedDay = getDayEl(12);
+ const monthYearEl = helper.getElement(MONTH_YEAR);
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue));
+ },
+ `Should change to December 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+ Assert.equal(
+ focusedDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "There is a focusable day within a calendar grid"
+ );
+ Assert.ok(
+ focusedDay.matches(":focus"),
+ "The focusable day within a calendar grid is focused"
+ );
+
+ info("Test behavior when selection is done on the month-year panel");
+
+ // Move focus to the month-year toggle button and open it:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+ EventUtils.synthesizeKey(" ");
+
+ // Move focus to the month spin button and change its value
+ // from December to November:
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Move focus to the year spin button and change its value
+ // from 2022 to 2023:
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(nextYear));
+ },
+ `Should change to November 2023, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ // Make a selection, close the month picker
+ EventUtils.synthesizeKey(" ", {});
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is not visible"
+ );
+
+ // Move focus from 2023-11-12 to 2024-01-07 by week
+ // Changing 2 month views along the way:
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 8 });
+
+ // 2024-01-07:
+ const newFocusedDay = getDayEl(7);
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextYearAfter)),
+ "The calendar is updated to show another month (2024-01)."
+ );
+ Assert.equal(
+ newFocusedDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "There is a focusable day within a calendar grid"
+ );
+ Assert.ok(
+ newFocusedDay.matches(":focus"),
+ "The focusable day within a calendar grid is focused"
+ );
+
+ await helper.tearDown();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js
new file mode 100644
index 0000000000..0b271ed77a
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js
@@ -0,0 +1,576 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure picker opens, closes, and updates its value with key bindings appropriately.
+ */
+add_task(async function test_datepicker_keyboard_nav() {
+ info(
+ "Ensure picker opens, closes, and updates its value with key bindings appropriately."
+ );
+
+ const inputValue = "2016-12-15";
+ const prevMonth = "2016-11-01";
+ await helper.openPicker(
+ `data:text/html,<input id=date type=date value=${inputValue}>`
+ );
+ let browser = helper.tab.linkedBrowser;
+ Assert.equal(helper.panel.state, "open", "Panel should be opened");
+
+ await testCalendarBtnAttribute("aria-expanded", "true");
+
+ let closed = helper.promisePickerClosed();
+
+ // Close on Escape anywhere
+ EventUtils.synthesizeKey("KEY_Escape", {});
+
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed after Escape from anywhere on the window"
+ );
+
+ await testCalendarBtnAttribute("aria-expanded", "false");
+
+ let ready = helper.waitForPickerReady();
+
+ // Ensure focus is on the input field
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("#date").focus();
+ });
+
+ info("Test that input updates with the keyboard update the picker");
+
+ // NOTE: After a Tab, the first input field (the month one) is focused,
+ // so down arrow will change the selected month.
+ //
+ // This assumes en-US locale, which seems fine for testing purposes (as
+ // DATE_FORMAT and other bits around do the same).
+ BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser);
+
+ // Toggle the picker on Space anywhere within the input
+ BrowserTestUtils.synthesizeKey(" ", {}, browser);
+
+ await ready;
+
+ await testCalendarBtnAttribute("aria-expanded", "true");
+
+ Assert.equal(
+ helper.panel.state,
+ "open",
+ "Panel should be opened on Space from anywhere within the input field"
+ );
+
+ Assert.equal(
+ helper.panel.querySelector("#dateTimePopupFrame").contentDocument
+ .activeElement.textContent,
+ "15",
+ "Picker is opened with a focus set to the currently selected date"
+ );
+
+ let monthYearEl = helper.getElement(MONTH_YEAR);
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(prevMonth));
+ },
+ `Should change to November 2016, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.ok(
+ true,
+ "The date on both the Calendar and Month-Year button was updated when updating months with Down arrow key"
+ );
+
+ closed = helper.promisePickerClosed();
+
+ // Close on Escape and return the focus to the input field (the month input in en-US locale)
+ EventUtils.synthesizeKey("KEY_Escape", {}, window);
+
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed on Escape"
+ );
+
+ // Check the focus is returned to the Month field
+ await SpecialPowers.spawn(browser, [], async () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ // Separators "/" are odd children of the wrapper
+ const monthField = shadowRoot.getElementById("edit-wrapper").children[0];
+ // Testing the focus position within content:
+ Assert.equal(
+ input,
+ content.document.activeElement,
+ `The input field includes programmatic focus`
+ );
+ // Testing the focus indication within the shadow-root:
+ Assert.ok(
+ monthField.matches(":focus"),
+ `The keyboard focus was returned to the Month field`
+ );
+ });
+
+ // Move focus to the second field (the day input in en-US locale)
+ BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, browser);
+
+ // Change the day to 2016-12-16
+ BrowserTestUtils.synthesizeKey("KEY_ArrowUp", {}, browser);
+
+ ready = helper.waitForPickerReady();
+
+ // Open the picker on Space within the input to check the date update
+ await BrowserTestUtils.synthesizeKey(" ", {}, browser);
+
+ await ready;
+
+ await testCalendarBtnAttribute("aria-expanded", "true");
+
+ Assert.equal(helper.panel.state, "open", "Panel should be opened on Space");
+
+ let selectedDayEl = helper.getElement(DAY_SELECTED);
+ await BrowserTestUtils.waitForMutationCondition(
+ selectedDayEl,
+ { childList: true },
+ () => {
+ return selectedDayEl.textContent === "16";
+ },
+ `Should change to the 16th, instead got ${
+ helper.getElement(DAY_SELECTED).textContent
+ }`
+ );
+
+ Assert.ok(
+ true,
+ "The date on the Calendar was updated when updating days with Up arrow key"
+ );
+
+ closed = helper.promisePickerClosed();
+
+ // Close on Escape and return the focus to the input field (the day input in en-US locale)
+ EventUtils.synthesizeKey("KEY_Escape", {}, window);
+
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed on Escape"
+ );
+
+ await testCalendarBtnAttribute("aria-expanded", "false");
+
+ // Check the focus is returned to the Day field
+ await SpecialPowers.spawn(browser, [], async () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ // Separators "/" are odd children of the wrapper
+ const dayField = shadowRoot.getElementById("edit-wrapper").children[2];
+ // Testing the focus position within content:
+ Assert.equal(
+ input,
+ content.document.activeElement,
+ `The input field includes programmatic focus`
+ );
+ // Testing the focus indication within the shadow-root:
+ Assert.ok(
+ dayField.matches(":focus"),
+ `The keyboard focus was returned to the Day field`
+ );
+ });
+
+ info("Test the Calendar button can toggle the picker with Enter/Space");
+
+ // Move focus to the Calendar button
+ BrowserTestUtils.synthesizeKey("KEY_Tab", {}, browser);
+ BrowserTestUtils.synthesizeKey("KEY_Tab", {}, browser);
+
+ // Toggle the picker on Enter on Calendar button
+ await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser);
+
+ await helper.waitForPickerReady();
+
+ Assert.equal(
+ helper.panel.state,
+ "open",
+ "Panel should be opened on Enter from the Calendar button"
+ );
+
+ await testCalendarBtnAttribute("aria-expanded", "true");
+
+ // Move focus from 2016-11-16 to 2016-11-17
+ EventUtils.synthesizeKey("KEY_ArrowRight", {});
+
+ // Make a selection by pressing Space on date gridcell
+ await EventUtils.synthesizeKey(" ", {});
+
+ await helper.promisePickerClosed();
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed on Space from the date gridcell"
+ );
+ await testCalendarBtnAttribute("aria-expanded", "false");
+
+ // Check the focus is returned to the Calendar button
+ await SpecialPowers.spawn(browser, [], async () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarBtn = shadowRoot.getElementById("calendar-button");
+ // Testing the focus position within content:
+ Assert.equal(
+ input,
+ content.document.activeElement,
+ `The input field includes programmatic focus`
+ );
+ // Testing the focus indication within the shadow-root:
+ Assert.ok(
+ calendarBtn.matches(":focus"),
+ `The keyboard focus was returned to the Calendar button`
+ );
+ });
+
+ // Check the Backspace on Calendar button is not doing anything
+ await EventUtils.synthesizeKey("KEY_Backspace", {});
+
+ // The Calendar button is on its place and the input value is not changed
+ // (bug 1804669)
+ await SpecialPowers.spawn(browser, [], () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarBtn = shadowRoot.getElementById("calendar-button");
+ Assert.equal(
+ calendarBtn.children[0].tagName,
+ "svg",
+ `Calendar button has an <svg> child`
+ );
+ Assert.equal(input.value, "2016-11-17", `Input's value is not removed`);
+ });
+
+ // Toggle the picker on Space on Calendar button
+ await EventUtils.synthesizeKey(" ", {});
+
+ await helper.waitForPickerReady();
+
+ Assert.equal(
+ helper.panel.state,
+ "open",
+ "Panel should be opened on Space from the Calendar button"
+ );
+
+ await testCalendarBtnAttribute("aria-expanded", "true");
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure calendar follows Arrow key bindings appropriately.
+ */
+add_task(async function test_datepicker_keyboard_arrows() {
+ info("Ensure calendar follows Arrow key bindings appropriately.");
+
+ const inputValue = "2016-12-10";
+ const prevMonth = "2016-11-01";
+ await helper.openPicker(
+ `data:text/html,<input id=date type=date value=${inputValue}>`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+ Assert.equal(helper.panel.state, "open", "Panel should be opened");
+
+ // Move focus from 2016-12-10 to 2016-12-11:
+ EventUtils.synthesizeKey("KEY_ArrowRight", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "11",
+ "Arrow Right moves focus to the next day"
+ );
+
+ // Move focus from 2016-12-11 to 2016-12-04:
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "4",
+ "Arrow Up moves focus to the same weekday of the previous week"
+ );
+
+ // Move focus from 2016-12-04 to 2016-12-03:
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "3",
+ "Arrow Left moves focus to the previous day"
+ );
+
+ // Move focus from 2016-12-03 to 2016-11-26:
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "26",
+ "Arrow Up updates the view to be on the previous month, if needed"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ "Arrow Up updates the spinner to show the previous month, if needed"
+ );
+
+ // Move focus from 2016-11-26 to 2016-12-03:
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "3",
+ "Arrow Down updates the view to be on the next month, if needed"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "Arrow Down updates the spinner to show the next month, if needed"
+ );
+
+ // Move focus from 2016-12-03 to 2016-12-10:
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "10",
+ "Arrow Down moves focus to the same day of the next week"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure calendar follows Home/End key bindings appropriately.
+ */
+add_task(async function test_datepicker_keyboard_home_end() {
+ info("Ensure calendar follows Home/End key bindings appropriately.");
+
+ const inputValue = "2016-12-15";
+ const prevMonth = "2016-11-01";
+ await helper.openPicker(
+ `data:text/html,<input id=date type=date value=${inputValue}>`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+ Assert.equal(helper.panel.state, "open", "Panel should be opened");
+
+ // Move focus from 2016-12-15 to 2016-12-11 (in the en-US locale):
+ EventUtils.synthesizeKey("KEY_Home", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "11",
+ "Home key moves focus to the first day/Sunday of the current week"
+ );
+
+ // Move focus from 2016-12-11 to 2016-12-17 (in the en-US locale):
+ EventUtils.synthesizeKey("KEY_End", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "17",
+ "End key moves focus to the last day/Saturday of the current week"
+ );
+
+ // Move focus from 2016-12-17 to 2016-12-31:
+ EventUtils.synthesizeKey("KEY_End", { ctrlKey: true });
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "31",
+ "Ctrl + End keys move focus to the last day of the current month"
+ );
+
+ // Move focus from 2016-12-31 to 2016-12-01:
+ EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true });
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "1",
+ "Ctrl + Home keys move focus to the first day of the current month"
+ );
+
+ // Move focus from 2016-12-01 to 2016-11-27 (in the en-US locale):
+ EventUtils.synthesizeKey("KEY_Home", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "27",
+ "Home key updates the view to be on the previous month, if needed"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ "Home key updates the spinner to show the previous month, if needed"
+ );
+
+ // Move focus from 2016-11-27 to 2016-12-03 (in the en-US locale):
+ EventUtils.synthesizeKey("KEY_End", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "3",
+ "End key updates the view to be on the next month, if needed"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "End key updates the spinner to show the next month, if needed"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure calendar follows Page Up/Down key bindings appropriately.
+ */
+add_task(async function test_datepicker_keyboard_pgup_pgdown() {
+ info("Ensure calendar follows Page Up/Down key bindings appropriately.");
+
+ const inputValue = "2023-01-31";
+ const prevMonth = "2022-12-31";
+ const prevYear = "2021-12-01";
+ const nextMonth = "2023-01-31";
+ const nextShortMonth = "2023-03-03";
+ await helper.openPicker(
+ `data:text/html,<input id=date type=date value=${inputValue}>`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+ Assert.equal(helper.panel.state, "open", "Panel should be opened");
+
+ // Move focus from 2023-01-31 to 2022-12-31:
+ EventUtils.synthesizeKey("KEY_PageUp", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "31",
+ "Page Up key moves focus to the same day of the previous month"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ "Page Up key updates the month-year button to show the previous month"
+ );
+
+ // Move focus from 2022-12-31 to 2022-12-01
+ // (because 2022-11-31 does not exist):
+ EventUtils.synthesizeKey("KEY_PageUp", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "1",
+ `When the same day does not exists in the previous month Page Up key moves
+ focus to the same day of the same week of the current month`
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ `When the same day does not exist in the previous month
+ Page Up key does not update the month-year button and shows the current month`
+ );
+
+ // Move focus from 2022-12-01 to 2021-12-01:
+ EventUtils.synthesizeKey("KEY_PageUp", { shiftKey: true });
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "1",
+ "Page Up with Shift key moves focus to the same day of the same month of the previous year"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevYear)),
+ "Page Up with Shift key updates the month-year button to show the same month of the previous year"
+ );
+
+ // Move focus from 2021-12-01 to 2022-12-01 month by month (bug 1806645):
+ EventUtils.synthesizeKey("KEY_PageDown", { repeat: 12 });
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "1",
+ "When repeated, Page Down key moves focus to the same day of the same month of the next year"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ "When repeated, Page Down key updates the month-year button to show the same month of the next year"
+ );
+
+ // Move focus from 2022-12-01 to 2021-12-01 month by month (bug 1806645):
+ EventUtils.synthesizeKey("KEY_PageUp", { repeat: 12 });
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "1",
+ "When repeated, Page Up moves focus to the same day of the same month of the previous year"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevYear)),
+ "When repeated, Page Up key updates the month-year button to show the same month of the previous year"
+ );
+
+ // Move focus from 2021-12-01 to 2022-12-01:
+ EventUtils.synthesizeKey("KEY_PageDown", { shiftKey: true });
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "1",
+ "Page Down with Shift key moves focus to the same day of the same month of the next year"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ "Page Down with Shift key updates the month-year button to show the same month of the next year"
+ );
+
+ // Move focus from 2016-12-01 to 2016-12-31:
+ EventUtils.synthesizeKey("KEY_End", { ctrlKey: true });
+ // Move focus from 2022-12-31 to 2023-01-31:
+ EventUtils.synthesizeKey("KEY_PageDown", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "31",
+ "Page Down key moves focus to the same day of the next month"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextMonth)),
+ "Page Down key updates the month-year button to show the next month"
+ );
+
+ // Move focus from 2023-01-31 to 2023-03-03:
+ EventUtils.synthesizeKey("KEY_PageDown", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ "3",
+ `When the same day does not exists in the next month, Page Down key moves
+ focus to the same day of the same week of the month after`
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextShortMonth)),
+ "Page Down key updates the month-year button to show the month after"
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js
new file mode 100644
index 0000000000..1c60afa8bf
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js
@@ -0,0 +1,483 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test that date picker opens with accessible markup
+ */
+add_task(async function test_datepicker_markup() {
+ info("Test that the date picker opens with accessible markup");
+
+ await helper.openPicker("data:text/html, <input type='date'>");
+
+ Assert.equal(
+ helper.getElement(DIALOG_PICKER).getAttribute("role"),
+ "dialog",
+ "Datepicker dialog has an appropriate ARIA role"
+ );
+ Assert.ok(
+ helper.getElement(DIALOG_PICKER).getAttribute("aria-modal"),
+ "Datepicker dialog is a modal"
+ );
+ Assert.equal(
+ helper.getElement(BTN_PREV_MONTH).tagName,
+ "button",
+ "Previous Month control is a button"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).tagName,
+ "button",
+ "Month picker view toggle is a button"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).getAttribute("aria-expanded"),
+ "false",
+ "Month picker view toggle is collapsed when the dialog is hidden"
+ );
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).getAttribute("aria-live"),
+ "polite",
+ "Month picker view toggle is a live region when it's not expanded"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection spinner is not visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection spinner is programmatically hidden"
+ );
+ Assert.equal(
+ helper.getElement(BTN_NEXT_MONTH).tagName,
+ "button",
+ "Next Month control is a button"
+ );
+ Assert.equal(
+ helper.getElement(DAYS_VIEW).parentNode.tagName,
+ "table",
+ "Calendar view is marked up as a table"
+ );
+ Assert.equal(
+ helper.getElement(DAYS_VIEW).parentNode.getAttribute("role"),
+ "grid",
+ "Calendar view is a grid"
+ );
+ Assert.ok(
+ helper.getElement(
+ `#${helper
+ .getElement(DAYS_VIEW)
+ .parentNode.getAttribute("aria-labelledby")}`
+ ),
+ "Calendar view has a valid accessible name"
+ );
+ Assert.equal(
+ helper.getElement(WEEK_HEADER).firstChild.tagName,
+ "tr",
+ "Week headers within the Calendar view are marked up as table rows"
+ );
+ Assert.equal(
+ helper.getElement(WEEK_HEADER).firstChild.firstChild.tagName,
+ "th",
+ "Weekdays within the Calendar view are marked up as header cells"
+ );
+ Assert.equal(
+ helper.getElement(WEEK_HEADER).firstChild.firstChild.getAttribute("role"),
+ "columnheader",
+ "Weekdays within the Calendar view are grid column headers"
+ );
+ Assert.equal(
+ helper.getElement(DAYS_VIEW).firstChild.tagName,
+ "tr",
+ "Weeks within the Calendar view are marked up as table rows"
+ );
+ Assert.equal(
+ helper.getElement(DAYS_VIEW).firstChild.firstChild.tagName,
+ "td",
+ "Days within the Calendar view are marked up as table cells"
+ );
+ Assert.equal(
+ helper.getElement(DAYS_VIEW).firstChild.firstChild.getAttribute("role"),
+ "gridcell",
+ "Days within the Calendar view are gridcells"
+ );
+ Assert.equal(
+ helper.getElement(BTN_CLEAR).tagName,
+ "button",
+ "Clear control is a button"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker has localizable labels
+ */
+add_task(async function test_datepicker_l10n() {
+ info("Test that the date picker has localizable labels");
+
+ await helper.openPicker("data:text/html, <input type='date'>");
+
+ const testcases = [
+ {
+ selector: DIALOG_PICKER,
+ id: "date-picker-label",
+ args: null,
+ },
+ {
+ selector: MONTH_YEAR_NAV,
+ id: "date-spinner-label",
+ args: null,
+ },
+ {
+ selector: BTN_PREV_MONTH,
+ id: "date-picker-previous",
+ args: null,
+ },
+ {
+ selector: BTN_NEXT_MONTH,
+ id: "date-picker-next",
+ args: null,
+ },
+ {
+ selector: BTN_CLEAR,
+ id: "date-picker-clear-button",
+ args: null,
+ },
+ ];
+
+ // Check "aria-label" attributes
+ for (let { selector, id, args } of testcases) {
+ const el = helper.getElement(selector);
+ const l10nAttrs = document.l10n.getAttributes(el);
+
+ Assert.ok(
+ el.hasAttribute("aria-label") || el.textContent,
+ `Datepicker "${selector}" element has accessible name`
+ );
+ Assert.deepEqual(
+ l10nAttrs,
+ {
+ id,
+ args,
+ },
+ `Datepicker "${selector}" element's accessible name is localizable`
+ );
+ }
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens to today's date, with today's and selected days
+ * marked up correctly, given a date value is set.
+ */
+add_task(async function test_datepicker_today_and_selected() {
+ info("Test today's and selected days' markup when a date value is set");
+
+ const date = new Date();
+ let inputValue = new Date();
+ // Both 2 and 10 dates are used as an example only to test that
+ // the current date and selected dates are marked up differently.
+ if (date.getDate() === 2) {
+ inputValue.setDate(10);
+ } else {
+ inputValue.setDate(2);
+ }
+ inputValue = inputValue.toISOString().split("T")[0];
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}"> `
+ );
+
+ if (date.getMonth() === new Date().getMonth()) {
+ Assert.notEqual(
+ helper.getElement(DAY_TODAY),
+ helper.getElement(DAY_SELECTED),
+ "Today and selected dates are different"
+ );
+ Assert.equal(
+ helper.getElement(DAY_TODAY).getAttribute("aria-current"),
+ "date",
+ "Today's date is programmatically current"
+ );
+ Assert.equal(
+ helper.getElement(DAY_SELECTED).getAttribute("aria-selected"),
+ "true",
+ "Chosen date is programmatically selected"
+ );
+ Assert.ok(
+ !helper.getElement(DAY_TODAY).hasAttribute("tabindex"),
+ "Today is not included in the focus order, when another day is selected"
+ );
+ Assert.equal(
+ helper.getElement(DAY_SELECTED).getAttribute("tabindex"),
+ "0",
+ "Selected date is included in the focus order"
+ );
+ } else {
+ Assert.ok(
+ true,
+ "Skipping datepicker today test if month changes when opening picker."
+ );
+ }
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker refreshes ARIA properties
+ * after the other month was displayed.
+ */
+add_task(async function test_datepicker_markup_refresh() {
+ const inputValue = "2016-12-05";
+ const minValue = "2016-12-05";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}" min="${minValue}">`
+ );
+
+ const secondRowDec = helper.getChildren(DAYS_VIEW)[1].children;
+
+ // 2016-12-05 Monday is selected (in en_US locale)
+ if (secondRowDec[1] === helper.getElement(DAY_SELECTED)) {
+ Assert.equal(
+ secondRowDec[1].getAttribute("aria-selected"),
+ "true",
+ "Chosen date is programmatically selected"
+ );
+ Assert.ok(
+ !secondRowDec[1].classList.contains("out-of-range"),
+ "Chosen date is not styled as out-of-range"
+ );
+ Assert.ok(
+ !secondRowDec[1].hasAttribute("aria-disabled"),
+ "Chosen date is not programmatically disabled"
+ );
+ // I.e. 2016-12-04 Sunday is out-of-range (in en_US locale)
+ Assert.ok(
+ secondRowDec[0].classList.contains("out-of-range"),
+ "Less than min date is styled as out-of-range"
+ );
+ Assert.equal(
+ secondRowDec[0].getAttribute("aria-disabled"),
+ "true",
+ "Less than min date is programmatically disabled"
+ );
+
+ // Change month view from December 2016 to January 2017
+ // to check an updated markup
+ helper.click(helper.getElement(BTN_NEXT_MONTH));
+
+ const secondRowJan = helper.getChildren(DAYS_VIEW)[1].children;
+
+ // 2017-01-02 Monday is not selected and in-range (in en_US locale)
+ Assert.equal(
+ secondRowJan[1].getAttribute("aria-selected"),
+ "false",
+ "Day with the same position as selected is not programmatically selected"
+ );
+ Assert.ok(
+ !secondRowJan[1].classList.contains("out-of-range"),
+ "Day with the same position as selected is not styled as out-of-range"
+ );
+ Assert.ok(
+ !secondRowJan[1].hasAttribute("aria-disabled"),
+ "Day with the same position as selected is not programmatically disabled"
+ );
+ // I.e. 2017-01-01 Sunday is in-range (in en_US locale)
+ Assert.ok(
+ !secondRowJan[0].classList.contains("out-of-range"),
+ "Day with the same as less than min date is not styled as out-of-range"
+ );
+ Assert.ok(
+ !secondRowJan[0].hasAttribute("aria-disabled"),
+ "Day with the same as less than min date is not programmatically disabled"
+ );
+ // 2016-12-05 was focused before the change, thus the same day of the month
+ // is expected to be focused now (2017-01-05):
+ Assert.equal(
+ secondRowJan[4].getAttribute("tabindex"),
+ "0",
+ "The same day of the month is made focusable"
+ );
+ Assert.ok(
+ !secondRowJan[0].hasAttribute("tabindex"),
+ "The first day of the month is not focusable"
+ );
+ Assert.ok(
+ !secondRowJan[1].hasAttribute("tabindex"),
+ "Day with the same position as selected is not focusable"
+ );
+ Assert.ok(!helper.getElement(DAY_TODAY), "No date is marked up as today");
+ Assert.ok(
+ !helper.getElement(DAY_SELECTED),
+ "No date is marked up as selected"
+ );
+ } else {
+ Assert.ok(
+ true,
+ "Skipping datepicker attributes flushing test if the week/locale is different from the en_US used for the test"
+ );
+ }
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date input field has a Calendar button with an accessible markup
+ */
+add_task(async function test_calendar_button_markup_date() {
+ info(
+ "Test that type=date input field has a Calendar button with an accessible markup"
+ );
+
+ await helper.openPicker("data:text/html, <input type='date'>");
+ let browser = helper.tab.linkedBrowser;
+
+ Assert.equal(helper.panel.state, "open", "Panel is visible");
+
+ let closed = helper.promisePickerClosed();
+
+ await testCalendarBtnAttribute("aria-expanded", "true");
+ await testCalendarBtnAttribute("aria-label", null, true);
+ await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarBtn = shadowRoot.getElementById("calendar-button");
+
+ Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button");
+ Assert.ok(
+ ContentTaskUtils.is_visible(calendarBtn),
+ "The Calendar button is visible"
+ );
+
+ calendarBtn.click();
+ });
+
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed on click on the Calendar button"
+ );
+
+ await testCalendarBtnAttribute("aria-expanded", "false");
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that datetime-local input field has a Calendar button
+ * with an accessible markup
+ */
+add_task(async function test_calendar_button_markup_datetime() {
+ info(
+ "Test that type=datetime-local input field has a Calendar button with an accessible markup"
+ );
+
+ await helper.openPicker("data:text/html, <input type='datetime-local'>");
+ let browser = helper.tab.linkedBrowser;
+
+ Assert.equal(helper.panel.state, "open", "Panel is visible");
+
+ let closed = helper.promisePickerClosed();
+
+ await testCalendarBtnAttribute("aria-expanded", "true");
+ await testCalendarBtnAttribute("aria-label", null, true);
+ await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarBtn = shadowRoot.getElementById("calendar-button");
+
+ Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button");
+ Assert.ok(
+ ContentTaskUtils.is_visible(calendarBtn),
+ "The Calendar button is visible"
+ );
+
+ calendarBtn.click();
+ });
+
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Panel should be closed on click on the Calendar button"
+ );
+
+ await testCalendarBtnAttribute("aria-expanded", "false");
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that time input field does not include a Calendar button,
+ * but opens a time picker panel on click within the field (with a pref)
+ */
+add_task(async function test_calendar_button_markup_time() {
+ info("Test that type=time input field does not include a Calendar button");
+
+ // Toggle a pref to allow a time picker to be shown
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.datetime.timepicker", true]],
+ });
+
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html, <input type='time'>"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarBtn = shadowRoot.getElementById("calendar-button");
+
+ Assert.ok(
+ ContentTaskUtils.is_hidden(calendarBtn),
+ "The Calendar control within a type=time input field is programmatically hidden"
+ );
+ });
+
+ let ready = helper.waitForPickerReady();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "input",
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ await ready;
+
+ Assert.equal(
+ helper.panel.state,
+ "open",
+ "Time picker panel should be opened on click from anywhere within the time input field"
+ );
+
+ let closed = helper.promisePickerClosed();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "input",
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Time picker panel should be closed on click from anywhere within the time input field"
+ );
+
+ BrowserTestUtils.removeTab(testTab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js
new file mode 100644
index 0000000000..3b0de45672
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js
@@ -0,0 +1,405 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Create a list of abbreviations for calendar class names
+const W = "weekend",
+ O = "outside",
+ S = "selection",
+ R = "out-of-range",
+ T = "today",
+ P = "off-step";
+
+// Calendar classlist for 2016-12. Used to verify the classNames are correct.
+const calendarClasslist_201612 = [
+ [W, O],
+ [O],
+ [O],
+ [O],
+ [],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [S],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [W],
+ [W],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [W],
+ [W, O],
+ [O],
+ [O],
+ [O],
+ [O],
+ [O],
+ [W, O],
+];
+
+/**
+ * When min and max attributes are set, calendar should show some dates as
+ * out-of-range.
+ */
+add_task(async function test_datepicker_min_max() {
+ const inputValue = "2016-12-15";
+ const inputMin = "2016-12-05";
+ const inputMax = "2016-12-25";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`
+ );
+
+ Assert.deepEqual(
+ getCalendarClassList(),
+ mergeArrays(calendarClasslist_201612, [
+ // R denotes out-of-range
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ ]),
+ "2016-12 with min & max"
+ );
+
+ Assert.ok(
+ helper
+ .getElement(DAYS_VIEW)
+ .firstChild.firstChild.getAttribute("aria-disabled"),
+ "An out-of-range date is programmatically disabled"
+ );
+
+ Assert.ok(
+ !helper.getElement(DAY_SELECTED).hasAttribute("aria-disabled"),
+ "An in-range date is not programmatically disabled"
+ );
+
+ await helper.tearDown();
+});
+
+add_task(async function test_datepicker_abs_min() {
+ const inputValue = "0001-01-01";
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15",
+ "16",
+ "17",
+ "18",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "24",
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ ],
+ "0001-01"
+ );
+
+ await helper.tearDown();
+});
+
+add_task(async function test_datepicker_abs_max() {
+ const inputValue = "275760-09-13";
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "13",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ],
+ "275760-09"
+ );
+
+ await helper.tearDown();
+});
+
+// Bug 1726546
+add_task(async function test_datetime_local_min() {
+ const inputValue = "2016-12-15T04:00";
+ const inputMin = "2016-12-05T12:22";
+ const inputMax = "2016-12-25T12:22";
+
+ await helper.openPicker(
+ `data:text/html,<input type="datetime-local" value="${inputValue}" min="${inputMin}" max="${inputMax}">`
+ );
+
+ Assert.deepEqual(
+ getCalendarClassList(),
+ mergeArrays(calendarClasslist_201612, [
+ // R denotes out-of-range
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ [R],
+ ]),
+ "2016-12 with min & max"
+ );
+
+ await helper.tearDown();
+});
+
+// Bug 1726546
+add_task(async function test_datetime_local_min_select_invalid() {
+ const inputValue = "2016-12-15T05:00";
+ const inputMin = "2016-12-05T12:22";
+ const inputMax = "2016-12-25T12:22";
+
+ await helper.openPicker(
+ `data:text/html,<input type="datetime-local" value="${inputValue}" min="${inputMin}" max="${inputMax}">`
+ );
+
+ let changePromise = helper.promiseChange();
+
+ // Select the minimum day (the 5th, which is the 2nd child of 2nd row).
+ // The date becomes invalid (we select 2016-12-05T05:00).
+ helper.click(helper.getElement(DAYS_VIEW).children[1].children[1]);
+
+ await changePromise;
+
+ let [value, invalid] = await SpecialPowers.spawn(
+ helper.tab.linkedBrowser,
+ [],
+ async () => {
+ let input = content.document.querySelector("input");
+ return [input.value, input.matches(":invalid")];
+ }
+ );
+
+ Assert.equal(value, "2016-12-05T05:00", "Value should've changed");
+ Assert.ok(invalid, "input should be now invalid");
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens to the minium valid date when the value property is lower than the min property
+ */
+add_task(async function test_datepicker_value_lower_than_min() {
+ const date = new Date();
+ const inputValue = "2001-02-03";
+ const minValue = "2004-05-06";
+ const maxValue = "2007-08-09";
+
+ await helper.openPicker(
+ `data:text/html, <input type='date' value="${inputValue}" min="${minValue}" max="${maxValue}">`
+ );
+
+ if (date.getMonth() === new Date().getMonth()) {
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(minValue))
+ );
+ } else {
+ Assert.ok(
+ true,
+ "Skipping datepicker value lower than min test if month changes when opening picker."
+ );
+ }
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens to the maximum valid date when the value property is higher than the max property
+ */
+add_task(async function test_datepicker_value_higher_than_max() {
+ const date = new Date();
+ const minValue = "2001-02-03";
+ const maxValue = "2004-05-06";
+ const inputValue = "2007-08-09";
+
+ await helper.openPicker(
+ `data:text/html, <input type='date' value="${inputValue}" min="${minValue}" max="${maxValue}">`
+ );
+
+ if (date.getMonth() === new Date().getMonth()) {
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(maxValue))
+ );
+ } else {
+ Assert.ok(
+ true,
+ "Skipping datepicker value higher than max test if month changes when opening picker."
+ );
+ }
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js
new file mode 100644
index 0000000000..5eacda421e
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Ensure the month-year panel of a date input handles Space and Enter appropriately.
+ */
+add_task(async function test_monthyear_close_date() {
+ info(
+ "Ensure the month-year panel of a date input handles Space and Enter appropriately."
+ );
+
+ const inputValue = "2022-11-11";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value=${inputValue}>`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+
+ // Move focus from the selected date to the month-year toggle button:
+ await EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+
+ // Test a month spinner
+ await testKeyOnSpinners("KEY_Enter", pickerDoc);
+ await testKeyOnSpinners(" ", pickerDoc);
+
+ // Test a year spinner
+ await testKeyOnSpinners("KEY_Enter", pickerDoc, 2);
+ await testKeyOnSpinners(" ", pickerDoc, 2);
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the month-year panel of a datetime-local input handles Space and Enter appropriately.
+ */
+add_task(async function test_monthyear_close_datetime() {
+ info(
+ "Ensure the month-year panel of a datetime-local input handles Space and Enter appropriately."
+ );
+
+ const inputValue = "2022-11-11T11:11";
+
+ await helper.openPicker(
+ `data:text/html, <input type="datetime-local" value=${inputValue}>`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+
+ // Move focus from the selected date to the month-year toggle button:
+ await EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+
+ // Test a month spinner
+ await testKeyOnSpinners("KEY_Enter", pickerDoc);
+ await testKeyOnSpinners(" ", pickerDoc);
+
+ // Test a year spinner
+ await testKeyOnSpinners("KEY_Enter", pickerDoc, 2);
+ await testKeyOnSpinners(" ", pickerDoc, 2);
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the month-year panel of a date input can be closed with Escape key.
+ */
+add_task(async function test_monthyear_escape_date() {
+ info("Ensure the month-year panel of a date input can be closed with Esc.");
+
+ const inputValue = "2022-12-12";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value=${inputValue}>`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+
+ // Move focus from the today's date to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+
+ // Test a month spinner
+ await testKeyOnSpinners("KEY_Escape", pickerDoc);
+
+ // Test a year spinner
+ await testKeyOnSpinners("KEY_Escape", pickerDoc, 2);
+
+ info(
+ `Testing "KEY_Escape" behavior without any interaction with spinners
+ (bug 1815184)`
+ );
+
+ Assert.ok(
+ helper.getElement(BTN_MONTH_YEAR).matches(":focus"),
+ "The month-year toggle button is focused"
+ );
+
+ // Open the month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "true",
+ "Month-year button is expanded when the spinners are shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is visible"
+ );
+
+ // Close the month-year selection panel without interacting with its spinners:
+ EventUtils.synthesizeKey("KEY_Escape", {});
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "false",
+ "Month-year button is collapsed when the spinners are hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is not visible"
+ );
+ Assert.ok(
+ helper
+ .getElement(DAYS_VIEW)
+ .querySelector('[tabindex="0"]')
+ .matches(":focus"),
+ "A focusable day within a calendar grid is focused"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the month-year panel of a datetime-local input can be closed with Escape key.
+ */
+add_task(async function test_monthyear_escape_datetime() {
+ info(
+ "Ensure the month-year panel of a datetime-local input can be closed with Esc."
+ );
+
+ const inputValue = "2022-12-12T01:01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="datetime-local" value=${inputValue}>`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+
+ // Move focus from the today's date to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+
+ // Test a month spinner
+ await testKeyOnSpinners("KEY_Escape", pickerDoc);
+
+ // Test a year spinner
+ await testKeyOnSpinners("KEY_Escape", pickerDoc, 2);
+
+ info(
+ `Testing "KEY_Escape" behavior without any interaction with spinners
+ (bug 1815184)`
+ );
+
+ Assert.ok(
+ helper.getElement(BTN_MONTH_YEAR).matches(":focus"),
+ "The month-year toggle button is focused"
+ );
+
+ // Open the month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "true",
+ "Month-year button is expanded when the spinners are shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is visible"
+ );
+
+ // Close the month-year selection panel without interacting with its spinners:
+ EventUtils.synthesizeKey("KEY_Escape", {});
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "false",
+ "Month-year button is collapsed when the spinners are hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is not visible"
+ );
+ Assert.ok(
+ helper
+ .getElement(DAYS_VIEW)
+ .querySelector('[tabindex="0"]')
+ .matches(":focus"),
+ "A focusable day within a calendar grid is focused"
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js
new file mode 100644
index 0000000000..d38992df1b
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * When the previous month button is clicked, calendar should display the dates
+ * for the previous month.
+ */
+add_task(async function test_datepicker_prev_month_btn() {
+ const inputValue = "2016-12-15";
+ const prevMonth = "2016-11-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ helper.click(helper.getElement(BTN_PREV_MONTH));
+
+ // 2016-11-15:
+ const focusableDay = getDayEl(15);
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth))
+ );
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15",
+ "16",
+ "17",
+ "18",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "24",
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ ],
+ "2016-11"
+ );
+ Assert.equal(
+ focusableDay.textContent,
+ "15",
+ "The same day of the month is present within a calendar grid"
+ );
+ Assert.equal(
+ focusableDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When the next month button is clicked, calendar should display the dates for
+ * the next month.
+ */
+add_task(async function test_datepicker_next_month_btn() {
+ const inputValue = "2016-12-15";
+ const nextMonth = "2017-01-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ helper.click(helper.getElement(BTN_NEXT_MONTH));
+
+ // 2017-01-15:
+ const focusableDay = getDayEl(15);
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextMonth))
+ );
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15",
+ "16",
+ "17",
+ "18",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "24",
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ ],
+ "2017-01"
+ );
+ Assert.equal(
+ focusableDay.textContent,
+ "15",
+ "The same day of the month is present within a calendar grid"
+ );
+ Assert.equal(
+ focusableDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When a date on the calendar is clicked, date picker should close and set
+ * value to the input box.
+ */
+add_task(async function test_datepicker_clicked() {
+ info("When a calendar day is clicked, the picker closes, the value is set");
+ const inputValue = "2016-12-15";
+ const firstDayOnCalendar = "2016-11-27";
+
+ await helper.openPicker(
+ `data:text/html, <input id="date" type="date" value="${inputValue}">`
+ );
+
+ let browser = helper.tab.linkedBrowser;
+ Assert.equal(helper.panel.state, "open", "Panel should be opened");
+
+ // Click the first item (top-left corner) of the calendar
+ let promise = BrowserTestUtils.waitForContentEvent(browser, "input");
+ helper.click(helper.getElement(DAYS_VIEW).querySelector("td"));
+ await promise;
+
+ let value = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.querySelector("input").value;
+ });
+
+ Assert.equal(value, firstDayOnCalendar);
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js
new file mode 100644
index 0000000000..1734e6fdc0
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js
@@ -0,0 +1,534 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * When the Previous Month button is pressed, calendar should display
+ * the dates for the previous month.
+ */
+add_task(async function test_datepicker_prev_month_btn() {
+ const inputValue = "2016-12-15";
+ const prevMonth = "2016-11-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ // Move focus from the selected date to the Previous Month button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+ EventUtils.synthesizeKey(" ", {});
+
+ // 2016-11-15:
+ const focusableDay = getDayEl(15);
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth))
+ );
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15",
+ "16",
+ "17",
+ "18",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "24",
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ ],
+ "The calendar is updated to show the previous month (2016-11)"
+ );
+ Assert.ok(
+ helper.getElement(BTN_PREV_MONTH).matches(":focus"),
+ "Focus stays on a Previous Month button after it's pressed"
+ );
+ Assert.equal(
+ focusableDay.textContent,
+ "15",
+ "The same day of the month is present within a calendar grid"
+ );
+ Assert.equal(
+ focusableDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+
+ // Move focus from the Previous Month button to the same day of the month (2016-11-15):
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+
+ Assert.ok(
+ focusableDay.matches(":focus"),
+ "The same day of the previous month can be focused with a keyboard"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When the Next Month button is clicked, calendar should display the dates for
+ * the next month.
+ */
+add_task(async function test_datepicker_next_month_btn() {
+ const inputValue = "2016-12-15";
+ const nextMonth = "2017-01-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ // Move focus from the selected date to the Next Month button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 4 });
+ EventUtils.synthesizeKey(" ", {});
+
+ // 2017-01-15:
+ const focusableDay = getDayEl(15);
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextMonth))
+ );
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "10",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15",
+ "16",
+ "17",
+ "18",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "24",
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "31",
+ "1",
+ "2",
+ "3",
+ "4",
+ ],
+ "The calendar is updated to show the next month (2017-01)."
+ );
+ Assert.ok(
+ helper.getElement(BTN_NEXT_MONTH).matches(":focus"),
+ "Focus stays on a Next Month button after it's pressed"
+ );
+ Assert.equal(
+ focusableDay.textContent,
+ "15",
+ "The same day of the month is present within a calendar grid"
+ );
+ Assert.equal(
+ focusableDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+
+ // Move focus from the Next Month button to the same day of the month (2017-01-15):
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ Assert.ok(
+ focusableDay.matches(":focus"),
+ "The same day of the next month can be focused with a keyboard"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When the Previous Month button is pressed, calendar should display
+ * the dates for the previous month on RTL build (bug 1806823).
+ */
+add_task(async function test_datepicker_prev_month_btn_rtl() {
+ const inputValue = "2016-12-15";
+ const prevMonth = "2016-11-01";
+
+ await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] });
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ // Move focus from the selected date to the Previous Month button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+ EventUtils.synthesizeKey(" ", {});
+
+ // 2016-11-15:
+ const focusableDay = getDayEl(15);
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(prevMonth)),
+ "The calendar is updated to show the previous month (2016-11)"
+ );
+ Assert.ok(
+ helper.getElement(BTN_PREV_MONTH).matches(":focus"),
+ "Focus stays on a Previous Month button after it's pressed"
+ );
+ Assert.equal(
+ focusableDay.textContent,
+ "15",
+ "The same day of the month is present within a calendar grid"
+ );
+ Assert.equal(
+ focusableDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+
+ // Move focus from the Previous Month button to the same day of the month (2016-11-15):
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+
+ Assert.ok(
+ focusableDay.matches(":focus"),
+ "The same day of the previous month can be focused with a keyboard"
+ );
+
+ await helper.tearDown();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * When the Next Month button is clicked, calendar should display the dates for
+ * the next month on RTL build (bug 1806823).
+ */
+add_task(async function test_datepicker_next_month_btn_rtl() {
+ const inputValue = "2016-12-15";
+ const nextMonth = "2017-01-01";
+
+ await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] });
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+
+ // Move focus from the selected date to the Next Month button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 4 });
+ EventUtils.synthesizeKey(" ", {});
+
+ // 2017-01-15:
+ const focusableDay = getDayEl(15);
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextMonth)),
+ "The calendar is updated to show the next month (2017-01)."
+ );
+ Assert.ok(
+ helper.getElement(BTN_NEXT_MONTH).matches(":focus"),
+ "Focus stays on a Next Month button after it's pressed"
+ );
+ Assert.equal(
+ focusableDay.textContent,
+ "15",
+ "The same day of the month is present within a calendar grid"
+ );
+ Assert.equal(
+ focusableDay,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+
+ // Move focus from the Next Month button to the same day of the month (2017-01-15):
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ Assert.ok(
+ focusableDay.matches(":focus"),
+ "The same day of the next month can be focused with a keyboard"
+ );
+
+ await helper.tearDown();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * When Previous/Next Month buttons or arrow keys are used to change a month view
+ * when a time value is incomplete for datetime-local inputs,
+ * calendar should update the month (bug 1817785).
+ */
+add_task(async function test_datepicker_reopened_prev_next_month_btn() {
+ info("Setup a datetime-local datepicker to its reopened state for testing");
+
+ let inputValueDT = "2023-05-02T01:01";
+ let prevMonth = new Date("2023-04-02");
+
+ await helper.openPicker(
+ `data:text/html, <input type="datetime-local" value="${inputValueDT}">`
+ );
+
+ let closed = helper.promisePickerClosed();
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ await closed;
+
+ Assert.equal(
+ helper.panel.state,
+ "closed",
+ "Date picker panel should be closed"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const editFields = shadowRoot.querySelectorAll(".datetime-edit-field");
+ const amPm = editFields[5];
+ amPm.focus();
+
+ Assert.ok(
+ amPm.matches(":focus"),
+ "Period of the day within the input is focused"
+ );
+ });
+
+ // Use Backspace key to clear the value of the AM/PM section of the input
+ // and wait for input.value to change to null (bug 1833988):
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ const input = content.document.querySelector("input");
+
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("KEY_Backspace", {}, content);
+
+ await ContentTaskUtils.waitForMutationCondition(
+ input,
+ { attributeFilter: ["value"] },
+ () => input.value == ""
+ );
+
+ Assert.ok(
+ !input.value,
+ `Expected an input value to be changed to 'null' when a time value became incomplete, instead got ${input.value}`
+ );
+ });
+
+ let ready = helper.waitForPickerReady();
+
+ // Move focus to a day section of the input and open a picker:
+ EventUtils.synthesizeKey("KEY_Tab", {});
+ EventUtils.synthesizeKey(" ", {});
+
+ await ready;
+
+ Assert.equal(
+ helper.panel.querySelector("#dateTimePopupFrame").contentDocument
+ .activeElement.textContent,
+ "2",
+ "Picker is opened with a focus set to the currently selected date"
+ );
+
+ info("Test the Previous Month button behavior");
+
+ // Move focus from the selected date to the Previous Month button,
+ // and activate it to move calendar from 2023-05-02 to 2023-04-02:
+ EventUtils.synthesizeKey("KEY_Tab", {
+ repeat: 2,
+ });
+ EventUtils.synthesizeKey(" ", {});
+
+ // Same date of the previous month should be visible and focusable
+ // (2023-04-02) but the focus should remain on the Previous Month button:
+ const focusableDayPrevMonth = getDayEl(2);
+ const monthYearEl = helper.getElement(MONTH_YEAR);
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ {
+ childList: true,
+ },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(prevMonth));
+ },
+ `Should change to the previous month (April 2023), instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+ Assert.ok(
+ true,
+ `The date on both the Calendar and Month-Year button was updated
+ when Previous Month button was used`
+ );
+ Assert.ok(
+ helper.getElement(BTN_PREV_MONTH).matches(":focus"),
+ "Focus stays on a Previous Month button after it's pressed"
+ );
+ Assert.equal(
+ focusableDayPrevMonth,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+ Assert.equal(
+ focusableDayPrevMonth.textContent,
+ "2",
+ "The same day of the month is present within a calendar grid"
+ );
+
+ // Move focus from the Previous Month button to the same day of the month (2023-04-02):
+ EventUtils.synthesizeKey("KEY_Tab", {
+ repeat: 3,
+ });
+
+ Assert.ok(
+ focusableDayPrevMonth.matches(":focus"),
+ "The same day of the previous month can be focused with a keyboard"
+ );
+
+ info("Test the Next Month button behavior");
+
+ // Move focus from the focused date to the Next Month button and activate it,
+ // (from 2023-04-02 to 2023-05-02):
+ EventUtils.synthesizeKey("KEY_Tab", {
+ repeat: 4,
+ });
+ EventUtils.synthesizeKey(" ", {});
+
+ // Same date of the next month should be visible and focusable
+ // (2023-05-02) but the focus should remain on the Next Month button:
+ const focusableDayNextMonth = getDayEl(2);
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ {
+ childList: true,
+ },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValueDT));
+ },
+ `Should change to May 2023, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+ Assert.ok(
+ true,
+ `The date on both the Calendar and Month-Year button was updated
+ when Next Month button was used`
+ );
+ Assert.ok(
+ helper.getElement(BTN_NEXT_MONTH).matches(":focus"),
+ "Focus stays on a Next Month button after it's pressed"
+ );
+ Assert.equal(
+ focusableDayNextMonth,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "The same day of the month is focusable within a calendar grid"
+ );
+ Assert.equal(
+ focusableDayNextMonth.textContent,
+ "2",
+ "The same day of the month is present within a calendar grid"
+ );
+
+ // Move focus from the Next Month button to the focusable day of the month (2023-05-02):
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ Assert.ok(
+ focusableDayNextMonth.matches(":focus"),
+ "The same day of the month can be focused with a keyboard"
+ );
+
+ info("Test the arrow navigation behavior");
+
+ // Move focus from the focused date to the same weekday of the previous month,
+ // (From 2023-05-02 to 2023-04-25):
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ {
+ childList: true,
+ },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(prevMonth));
+ },
+ `Should change to the previous month, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+ Assert.ok(
+ true,
+ `The date on both the Calendar and Month-Year button was updated
+ when an Up Arrow key was used`
+ );
+
+ // Move focus from the focused date to the same weekday of the next month,
+ // (from 2023-04-25 to 2023-05-02):
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ {
+ childList: true,
+ },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValueDT));
+ },
+ `Should change to the previous month, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+ Assert.ok(
+ true,
+ `The date on both the Calendar and Month-Year button was updated
+ when a Down Arrow key was used`
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_showPicker.js b/toolkit/content/tests/browser/datetime/browser_datetime_showPicker.js
new file mode 100644
index 0000000000..817c8958cd
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_showPicker.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/. */
+
+"use strict";
+
+/**
+ * Test that date picker opens with showPicker.
+ */
+add_task(async function test_datepicker_showPicker() {
+ const date = new Date();
+
+ await helper.openPicker(
+ "data:text/html, <input type='date'>",
+ false,
+ "showPicker"
+ );
+
+ if (date.getMonth() === new Date().getMonth()) {
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT_LOCAL(date),
+ "Date picker opens when a showPicker method is called"
+ );
+ } else {
+ Assert.ok(
+ true,
+ "Skipping datepicker today test if month changes when opening picker."
+ );
+ }
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens with showPicker and the explicit value.
+ */
+add_task(async function test_datepicker_showPicker_value() {
+ await helper.openPicker(
+ "data:text/html, <input type='date' value='2012-10-15'>",
+ false,
+ "showPicker"
+ );
+
+ Assert.equal(
+ helper.getElement(MONTH_YEAR).textContent,
+ DATE_FORMAT_LOCAL(new Date("2012-10-12")),
+ "Date picker opens when a showPicker method is called"
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js b/toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js
new file mode 100644
index 0000000000..2e97e4d2da
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ let input = document.createElement("input");
+ input.type = "date";
+ registerCleanupFunction(() => input.remove());
+ document.body.appendChild(input);
+
+ let shown = BrowserTestUtils.waitForDateTimePickerPanelShown(window);
+
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+
+ EventUtils.synthesizeMouseAtCenter(
+ shadowRoot.getElementById("calendar-button"),
+ {}
+ );
+
+ let popup = await shown;
+ ok(!!popup, "Should've shown the popup");
+
+ let hidden = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+ popup.hidePopup();
+
+ await hidden;
+ popup.remove();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_spinner.js b/toolkit/content/tests/browser/datetime/browser_spinner.js
new file mode 100644
index 0000000000..81ccef39ea
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_spinner.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";
+
+/**
+ * Test that the Month spinner opens with an accessible markup
+ */
+add_task(async function test_spinner_month_markup() {
+ info("Test that the Month spinner opens with an accessible markup");
+
+ const inputValue = "2022-09-09";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+ helper.click(helper.getElement(MONTH_YEAR));
+
+ const spinnerMonth = helper.getElement(SPINNER_MONTH);
+ const spinnerMonthPrev = spinnerMonth.children[0];
+ const spinnerMonthBtn = spinnerMonth.children[1];
+ const spinnerMonthNext = spinnerMonth.children[2];
+
+ Assert.equal(
+ spinnerMonthPrev.tagName,
+ "button",
+ "Spinner's Previous Month control is a button"
+ );
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("role"),
+ "spinbutton",
+ "Spinner control is a spinbutton"
+ );
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("tabindex"),
+ "0",
+ "Spinner control is included in the focus order"
+ );
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuemin"),
+ "0",
+ "Spinner control has a min value set"
+ );
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuemax"),
+ "11",
+ "Spinner control has a max value set"
+ );
+ // September 2022 as an example
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "8",
+ "Spinner control has a current value set"
+ );
+ Assert.equal(
+ spinnerMonthNext.tagName,
+ "button",
+ "Spinner's Next Month control is a button"
+ );
+
+ testAttribute(spinnerMonthBtn, "aria-valuetext");
+
+ let visibleEls = spinnerMonthBtn.querySelectorAll(
+ ":scope > :not([aria-hidden])"
+ );
+ Assert.equal(
+ visibleEls.length,
+ 0,
+ "There should be no children of the spinner without aria-hidden"
+ );
+
+ info("Test that the month spinner has localizable labels");
+
+ testAttributeL10n(
+ spinnerMonthPrev,
+ "aria-label",
+ "date-spinner-month-previous"
+ );
+ testAttributeL10n(spinnerMonthBtn, "aria-label", "date-spinner-month");
+ testAttributeL10n(spinnerMonthNext, "aria-label", "date-spinner-month-next");
+
+ await testReducedMotionProp(
+ spinnerMonthBtn,
+ "scroll-behavior",
+ "smooth",
+ "auto"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that the Year spinner opens with an accessible markup
+ */
+add_task(async function test_spinner_year_markup() {
+ info("Test that the year spinner opens with an accessible markup");
+
+ const inputValue = "2022-06-06";
+ const inputMin = "2020-06-01";
+ const inputMax = "2030-12-31";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`
+ );
+ helper.click(helper.getElement(MONTH_YEAR));
+
+ const spinnerYear = helper.getElement(SPINNER_YEAR);
+ const spinnerYearPrev = spinnerYear.children[0];
+ const spinnerYearBtn = spinnerYear.children[1];
+ const spinnerYearNext = spinnerYear.children[2];
+
+ Assert.equal(
+ spinnerYearPrev.tagName,
+ "button",
+ "Spinner's Previous Year control is a button"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("role"),
+ "spinbutton",
+ "Spinner control is a spinbutton"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("tabindex"),
+ "0",
+ "Spinner control is included in the focus order"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuemin"),
+ "2020",
+ "Spinner control has a min value set, when the range is provided"
+ );
+ // 2020-2030 range is an example
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuemax"),
+ "2030",
+ "Spinner control has a max value set, when the range is provided"
+ );
+ // June 2022 is an example
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Spinner control has a current value set"
+ );
+ Assert.equal(
+ spinnerYearNext.tagName,
+ "button",
+ "Spinner's Next Year control is a button"
+ );
+
+ testAttribute(spinnerYearBtn, "aria-valuetext");
+
+ let visibleEls = spinnerYearBtn.querySelectorAll(
+ ":scope > :not([aria-hidden])"
+ );
+ Assert.equal(
+ visibleEls.length,
+ 0,
+ "There should be no children of the spinner without aria-hidden"
+ );
+
+ info("Test that the year spinner has localizable labels");
+
+ testAttributeL10n(
+ spinnerYearPrev,
+ "aria-label",
+ "date-spinner-year-previous"
+ );
+ testAttributeL10n(spinnerYearBtn, "aria-label", "date-spinner-year");
+ testAttributeL10n(spinnerYearNext, "aria-label", "date-spinner-year-next");
+
+ await testReducedMotionProp(
+ spinnerYearBtn,
+ "scroll-behavior",
+ "smooth",
+ "auto"
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js b/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js
new file mode 100644
index 0000000000..ece96ce1cf
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js
@@ -0,0 +1,622 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_setup(async function setPrefsReducedMotion() {
+ // Set "prefers-reduced-motion" media to "reduce"
+ // to avoid intermittent scroll failures (1803612, 1803687)
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 1]],
+ });
+ Assert.ok(
+ matchMedia("(prefers-reduced-motion: reduce)").matches,
+ "The reduce motion mode is active"
+ );
+});
+
+/**
+ * Ensure the month spinner follows arrow key bindings appropriately.
+ */
+add_task(async function test_spinner_month_keyboard_arrows() {
+ info("Ensure the month spinner follows arrow key bindings appropriately.");
+
+ const inputValue = "2022-12-10";
+ const nextMonthValue = "2022-01-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+
+ info("Testing general keyboard navigation");
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "false",
+ "Month-year button is collapsed when a picker is opened (by default)"
+ );
+
+ // Move focus from the selection to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+ // Open month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1];
+ const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1];
+
+ let monthYearEl = helper.getElement(MONTH_YEAR);
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "true",
+ "Month-year button is expanded when the spinners are shown"
+ );
+ // December 2022 is an example:
+ Assert.equal(
+ pickerDoc.activeElement.textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "Month-year toggle button is focused"
+ );
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Month Spinner control is ready"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Year Spinner control is ready"
+ );
+
+ // Move focus from the month-year toggle button to the month spinner:
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ Assert.equal(
+ pickerDoc.activeElement.getAttribute("aria-valuenow"),
+ "11",
+ "Tab moves focus to the month spinner"
+ );
+
+ info("Testing Up/Down Arrow keys behavior of the Month Spinner");
+
+ // Change the month-year from December 2022 to January 2022:
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(nextMonthValue));
+ },
+ `Should change to January 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "0",
+ "Down Arrow selects the next month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Down Arrow on a month spinner does not update the year"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextMonthValue)),
+ "Down Arrow updates the month-year button to the next month"
+ );
+
+ // Change the month-year from January 2022 to December 2022:
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue));
+ },
+ `Should change to December 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Up Arrow selects the previous month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Up Arrow on a month spinner does not update the year"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "Up Arrow updates the month-year button to the previous month"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the month spinner follows Page Up/Down key bindings appropriately.
+ */
+add_task(async function test_spinner_month_keyboard_pageup_pagedown() {
+ info(
+ "Ensure the month spinner follows Page Up/Down key bindings appropriately."
+ );
+
+ const inputValue = "2022-12-10";
+ const nextFifthMonthValue = "2022-05-10";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+ // const browser = helper.tab.linkedBrowser;
+ // Move focus from the selection to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+ // Open month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1];
+ const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1];
+
+ let monthYearEl = helper.getElement(MONTH_YEAR);
+
+ // Move focus from the month-year toggle button to the month spinner:
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ // Change the month-year from December 2022 to May 2022:
+ EventUtils.synthesizeKey("KEY_PageDown", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return (
+ monthYearEl.textContent == DATE_FORMAT(new Date(nextFifthMonthValue))
+ );
+ },
+ `Should change to May 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "4",
+ "Page Down selects the fifth later month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Page Down on a month spinner does not update the year"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextFifthMonthValue)),
+ "Page Down updates the month-year button to the fifth later month"
+ );
+
+ // Change the month-year from May 2022 to December 2022:
+ EventUtils.synthesizeKey("KEY_PageUp", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue));
+ },
+ `Should change to December 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Page Up selects the fifth earlier month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Page Up on a month spinner does not update the year"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "Page Up updates the month-year button to the fifth earlier month"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the month spinner follows Home/End key bindings appropriately.
+ */
+add_task(async function test_spinner_month_keyboard_home_end() {
+ info("Ensure the month spinner follows Home/End key bindings appropriately.");
+
+ const inputValue = "2022-12-11";
+ const firstMonthValue = "2022-01-11";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+ // const browser = helper.tab.linkedBrowser;
+ // Move focus from the selection to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+ // Open month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1];
+ const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1];
+
+ let monthYearEl = helper.getElement(MONTH_YEAR);
+
+ // Move focus from the month-year toggle button to the month spinner:
+ EventUtils.synthesizeKey("KEY_Tab", {});
+
+ // Change the month-year from December 2022 to January 2022:
+ EventUtils.synthesizeKey("KEY_Home", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(firstMonthValue));
+ },
+ `Should change to January 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "0",
+ "Home key selects the first month of the year (min value)"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Home key does not update the year value"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(firstMonthValue)),
+ "Home key updates the month-year button to the first month of the same year (min value)"
+ );
+
+ // Change the month-year from January 2022 to December 2022:
+ EventUtils.synthesizeKey("KEY_End", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue));
+ },
+ `Should change to December 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "End key selects the last month of the year (max value)"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "End key does not update the year value"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "End key updates the month-year button to the last month of the same year (max value)"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the year spinner follows arrow key bindings appropriately.
+ */
+add_task(async function test_spinner_year_keyboard_arrows() {
+ info("Ensure the year spinner follows arrow key bindings appropriately.");
+
+ const inputValue = "2022-12-10";
+ const nextYearValue = "2023-12-01";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+ let pickerDoc = helper.panel.querySelector(
+ "#dateTimePopupFrame"
+ ).contentDocument;
+
+ info("Testing general keyboard navigation");
+
+ // Move focus from the selection to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+ // Open month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1];
+ const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1];
+
+ let monthYearEl = helper.getElement(MONTH_YEAR);
+
+ // December 2022 is an example:
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Year Spinner control is ready"
+ );
+
+ // Move focus from the month-year toggle button to the year spinner:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+
+ Assert.equal(
+ pickerDoc.activeElement.getAttribute("aria-valuenow"),
+ "2022",
+ "Tab can move the focus to the year spinner"
+ );
+
+ info("Testing Up/Down Arrow keys behavior of the Year Spinner");
+
+ // Change the month-year from December 2022 to December 2023:
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(nextYearValue));
+ },
+ `Should change to December 2023, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Down Arrow on the year spinner does not change the month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2023",
+ "Down Arrow updates the year to the next"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextYearValue)),
+ "Down Arrow updates the month-year button to the next year"
+ );
+
+ // Change the month-year from December 2023 to December 2022:
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue));
+ },
+ `Should change to December 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Up Arrow on the year spinner does not change the month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Up Arrow updates the year to the previous"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "Up Arrow updates the month-year button to the previous year"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the year spinner follows Page Up/Down key bindings appropriately.
+ */
+add_task(async function test_spinner_year_keyboard_pageup_pagedown() {
+ info(
+ "Ensure the year spinner follows Page Up/Down key bindings appropriately."
+ );
+
+ const inputValue = "2022-12-10";
+ const nextFifthYearValue = "2027-12-10";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}">`
+ );
+ // const browser = helper.tab.linkedBrowser;
+ // Move focus from the selection to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+ // Open month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1];
+ const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1];
+
+ let monthYearEl = helper.getElement(MONTH_YEAR);
+
+ // Move focus from the month-year toggle button to the year spinner:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+
+ // Change the month-year from December 2022 to December 2027:
+ EventUtils.synthesizeKey("KEY_PageDown", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return (
+ monthYearEl.textContent == DATE_FORMAT(new Date(nextFifthYearValue))
+ );
+ },
+ `Should change to December 2027, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Page Down on the year spinner does not change the month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2027",
+ "Page Down selects the fifth later year"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(nextFifthYearValue)),
+ "Page Down updates the month-year button to the fifth later year"
+ );
+
+ // Change the month-year from December 2027 to December 2022:
+ EventUtils.synthesizeKey("KEY_PageUp", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue));
+ },
+ `Should change to December 2022, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Page Up on the year spinner does not change the month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2022",
+ "Page Up selects the fifth earlier year"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(inputValue)),
+ "Page Up updates the month-year button to the fifth earlier year"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * Ensure the year spinner follows Home/End key bindings appropriately.
+ */
+add_task(async function test_spinner_year_keyboard_home_end() {
+ info("Ensure the year spinner follows Home/End key bindings appropriately.");
+
+ const inputValue = "2022-12-10";
+ const minValue = "2020-10-10";
+ const maxValue = "2030-12-31";
+ const minYearValue = "2020-12-10";
+ const maxYearValue = "2030-12-10";
+
+ await helper.openPicker(
+ `data:text/html, <input type="date" value="${inputValue}" min="${minValue}" max="${maxValue}">`
+ );
+
+ // Move focus from the selection to the month-year toggle button:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+ // Open month-year selection panel with spinners:
+ EventUtils.synthesizeKey(" ", {});
+
+ const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1];
+ const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1];
+
+ let monthYearEl = helper.getElement(MONTH_YEAR);
+
+ // Move focus from the month-year toggle button to the year spinner:
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+
+ // Change the month-year from December 2022 to December 2020:
+ EventUtils.synthesizeKey("KEY_Home", {});
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(minYearValue));
+ },
+ `Should change to December 2020, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "Home key on the year spinner does not change the month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2020",
+ "Home key selects the min year value"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(minYearValue)),
+ "Home key updates the month-year button to the min year value"
+ );
+
+ // Change the month-year from December 2022 to December 2030:
+ EventUtils.synthesizeKey("KEY_End", {});
+
+ await BrowserTestUtils.waitForMutationCondition(
+ monthYearEl,
+ { childList: true },
+ () => {
+ return monthYearEl.textContent == DATE_FORMAT(new Date(maxYearValue));
+ },
+ `Should change to December 2030, instead got ${
+ helper.getElement(MONTH_YEAR).textContent
+ }`
+ );
+
+ Assert.equal(
+ spinnerMonthBtn.getAttribute("aria-valuenow"),
+ "11",
+ "End key on the year spinner does not change the month"
+ );
+ Assert.equal(
+ spinnerYearBtn.getAttribute("aria-valuenow"),
+ "2030",
+ "End key selects the max year value"
+ );
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).textContent,
+ DATE_FORMAT(new Date(maxYearValue)),
+ "End key updates the month-year button to the max year value"
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/datetime/head.js b/toolkit/content/tests/browser/datetime/head.js
new file mode 100644
index 0000000000..ca301a2e46
--- /dev/null
+++ b/toolkit/content/tests/browser/datetime/head.js
@@ -0,0 +1,441 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * Helper class for testing datetime input picker widget
+ */
+class DateTimeTestHelper {
+ constructor() {
+ this.panel = null;
+ this.tab = null;
+ this.frame = null;
+ }
+
+ /**
+ * Opens a new tab with the URL of the test page, and make sure the picker is
+ * ready for testing.
+ *
+ * @param {String} pageUrl
+ * @param {bool} inFrame true if input is in the first child frame
+ * @param {String} openMethod "click" or "showPicker"
+ */
+ async openPicker(pageUrl, inFrame, openMethod = "click") {
+ this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+ let bc = gBrowser.selectedBrowser;
+ if (inFrame) {
+ await SpecialPowers.spawn(bc, [], async function () {
+ const iframe = content.document.querySelector("iframe");
+ // Ensure the iframe's position is correct before doing any
+ // other operations
+ iframe.getBoundingClientRect();
+ });
+ bc = bc.browsingContext.children[0];
+ }
+ await SpecialPowers.spawn(bc, [], async function () {
+ // Ensure that screen coordinates are ok.
+ await SpecialPowers.contentTransformsReceived(content);
+ });
+
+ let shown = this.waitForPickerReady();
+
+ if (openMethod === "click") {
+ await SpecialPowers.spawn(bc, [], () => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ shadowRoot.getElementById("calendar-button").click();
+ });
+ } else if (openMethod === "showPicker") {
+ await SpecialPowers.spawn(bc, [], function () {
+ content.document.notifyUserGestureActivation();
+ content.document.querySelector("input").showPicker();
+ });
+ }
+ this.panel = await shown;
+ this.frame = this.panel.querySelector("#dateTimePopupFrame");
+ }
+
+ promisePickerClosed() {
+ return new Promise(resolve => {
+ this.panel.addEventListener("popuphidden", resolve, { once: true });
+ });
+ }
+
+ promiseChange(selector = "input") {
+ return SpecialPowers.spawn(
+ this.tab.linkedBrowser,
+ [selector],
+ async selector => {
+ let input = content.document.querySelector(selector);
+ await ContentTaskUtils.waitForEvent(input, "change", false, e => {
+ ok(
+ content.window.windowUtils.isHandlingUserInput,
+ "isHandlingUserInput should be true"
+ );
+ return true;
+ });
+ }
+ );
+ }
+
+ waitForPickerReady() {
+ return BrowserTestUtils.waitForDateTimePickerPanelShown(window);
+ }
+
+ /**
+ * Find an element on the picker.
+ *
+ * @param {String} selector
+ * @return {DOMElement}
+ */
+ getElement(selector) {
+ return this.frame.contentDocument.querySelector(selector);
+ }
+
+ /**
+ * Find the children of an element on the picker.
+ *
+ * @param {String} selector
+ * @return {Array<DOMElement>}
+ */
+ getChildren(selector) {
+ return Array.from(this.getElement(selector).children);
+ }
+
+ /**
+ * Click on an element
+ *
+ * @param {DOMElement} element
+ */
+ click(element) {
+ EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow);
+ }
+
+ /**
+ * Close the panel and the tab
+ */
+ async tearDown() {
+ if (this.panel.state != "closed") {
+ let pickerClosePromise = this.promisePickerClosed();
+ this.panel.hidePopup();
+ await pickerClosePromise;
+ }
+ BrowserTestUtils.removeTab(this.tab);
+ this.tab = null;
+ }
+
+ /**
+ * Clean up after tests. Remove the frame to prevent leak.
+ */
+ cleanup() {
+ this.frame?.remove();
+ this.frame = null;
+ this.panel = null;
+ }
+}
+
+let helper = new DateTimeTestHelper();
+
+registerCleanupFunction(() => {
+ helper.cleanup();
+});
+
+const BTN_MONTH_YEAR = "#month-year-label",
+ BTN_NEXT_MONTH = ".next",
+ BTN_PREV_MONTH = ".prev",
+ BTN_CLEAR = "#clear-button",
+ DAY_SELECTED = ".selection",
+ DAY_TODAY = ".today",
+ DAYS_VIEW = ".days-view",
+ DIALOG_PICKER = "#date-picker",
+ MONTH_YEAR = ".month-year",
+ MONTH_YEAR_NAV = ".month-year-nav",
+ MONTH_YEAR_VIEW = ".month-year-view",
+ SPINNER_MONTH = "#spinner-month",
+ SPINNER_YEAR = "#spinner-year",
+ WEEK_HEADER = ".week-header";
+const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
+ year: "numeric",
+ month: "long",
+ timeZone: "UTC",
+}).format;
+const DATE_FORMAT_LOCAL = new Intl.DateTimeFormat("en-US", {
+ year: "numeric",
+ month: "long",
+}).format;
+
+/**
+ * Helper function to find and return a gridcell element
+ * for a specific day of the month
+ *
+ * @return {Array[String]} TextContent of each gridcell within a calendar grid
+ */
+function getCalendarText() {
+ let calendarCells = [];
+ for (const tr of helper.getChildren(DAYS_VIEW)) {
+ for (const td of tr.children) {
+ calendarCells.push(td.textContent);
+ }
+ }
+ return calendarCells;
+}
+
+function getCalendarClassList() {
+ let calendarCellsClasses = [];
+ for (const tr of helper.getChildren(DAYS_VIEW)) {
+ for (const td of tr.children) {
+ calendarCellsClasses.push(td.classList);
+ }
+ }
+ return calendarCellsClasses;
+}
+
+/**
+ * Helper function to find and return a gridcell element
+ * for a specific day of the month
+ *
+ * @param {Number} day: A day of the month to find in the month grid
+ *
+ * @return {HTMLElement} A gridcell that represents the needed day of the month
+ */
+function getDayEl(dayNum) {
+ const dayEls = Array.from(
+ helper.getElement(DAYS_VIEW).querySelectorAll("td")
+ );
+ return dayEls.find(el => el.textContent === dayNum.toString());
+}
+
+function mergeArrays(a, b) {
+ return a.map((classlist, index) => classlist.concat(b[index]));
+}
+
+/**
+ * Helper function to check if a DOM element has a specific attribute
+ *
+ * @param {DOMElement} el: DOM Element to be tested
+ * @param {String} attr: The name of the attribute to be tested
+ */
+function testAttribute(el, attr) {
+ Assert.ok(
+ el.hasAttribute(attr),
+ `The "${el}" element has a "${attr}" attribute`
+ );
+}
+
+/**
+ * Helper function to check for l10n of an element's attribute
+ *
+ * @param {DOMElement} el: DOM Element to be tested
+ * @param {String} attr: The name of the attribute to be tested
+ * @param {String} id: Value of the "data-l10n-id" attribute of the element
+ * @param {Object} args: Args provided by the l10n object of the element
+ */
+function testAttributeL10n(el, attr, id, args = null) {
+ testAttribute(el, attr);
+ testLocalization(el, id, args);
+}
+
+/**
+ * Helper function to check the value of a Calendar button's specific attribute
+ *
+ * @param {String} attr: The name of the attribute to be tested
+ * @param {String} val: Value that is expected to be assigned to the attribute.
+ * @param {Boolean} presenceOnly: If "true", test only the presence of the attribute
+ */
+async function testCalendarBtnAttribute(attr, val, presenceOnly = false) {
+ let browser = helper.tab.linkedBrowser;
+
+ await SpecialPowers.spawn(
+ browser,
+ [attr, val, presenceOnly],
+ (attr, val, presenceOnly) => {
+ const input = content.document.querySelector("input");
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarBtn = shadowRoot.getElementById("calendar-button");
+
+ if (presenceOnly) {
+ Assert.ok(
+ calendarBtn.hasAttribute(attr),
+ `Calendar button has ${attr} attribute`
+ );
+ } else {
+ Assert.equal(
+ calendarBtn.getAttribute(attr),
+ val,
+ `Calendar button has ${attr} attribute set to ${val}`
+ );
+ }
+ }
+ );
+}
+
+/**
+ * Helper function to test if a submission/dismissal keyboard shortcut works
+ * on a month or a year selection spinner
+ *
+ * @param {String} key: A keyboard Event.key that will be synthesized
+ * @param {Object} document: Reference to the content document
+ * of the #dateTimePopupFrame
+ * @param {Number} tabs: How many times "Tab" key should be pressed
+ * to move a keyboard focus to a needed spinner
+ * (1 for month/default and 2 for year)
+ *
+ * @description Starts with the month-year toggle button being focused
+ * on the date/datetime-local input's datepicker panel
+ */
+async function testKeyOnSpinners(key, document, tabs = 1) {
+ info(`Testing "${key}" key behavior`);
+
+ Assert.equal(
+ document.activeElement,
+ helper.getElement(BTN_MONTH_YEAR),
+ "The month-year toggle button is focused"
+ );
+
+ // Open the month-year selection panel with spinners:
+ await EventUtils.synthesizeKey(" ", {});
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "true",
+ "Month-year button is expanded when the spinners are shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is visible"
+ );
+
+ // Move focus from the month-year toggle button to one of spinners:
+ await EventUtils.synthesizeKey("KEY_Tab", { repeat: tabs });
+
+ Assert.equal(
+ document.activeElement.getAttribute("role"),
+ "spinbutton",
+ "The spinner is focused"
+ );
+
+ // Confirm the spinbutton choice and close the month-year selection panel:
+ await EventUtils.synthesizeKey(key, {});
+
+ Assert.equal(
+ helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"),
+ "false",
+ "Month-year button is collapsed when the spinners are hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(helper.getElement(MONTH_YEAR_VIEW)),
+ "Month-year selection panel is not visible"
+ );
+ Assert.equal(
+ document.activeElement,
+ helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'),
+ "A focusable day within a calendar grid is focused"
+ );
+
+ // Return the focus to the month-year toggle button for future tests
+ // (passing a Previous button along the way):
+ await EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 });
+}
+
+/**
+ * Helper function to check for localization attributes of a DOM element
+ *
+ * @param {DOMElement} el: DOM Element to be tested
+ * @param {String} id: Value of the "data-l10n-id" attribute of the element
+ * @param {Object} args: Args provided by the l10n object of the element
+ */
+function testLocalization(el, id, args = null) {
+ const l10nAttrs = document.l10n.getAttributes(el);
+
+ Assert.deepEqual(
+ l10nAttrs,
+ {
+ id,
+ args,
+ },
+ `The "${id}" element is localizable`
+ );
+}
+
+/**
+ * Helper function to check if a CSS property respects reduced motion mode
+ *
+ * @param {DOMElement} el: DOM Element to be tested
+ * @param {String} prop: The name of the CSS property to be tested
+ * @param {Object} valueNotReduced: Default value of the tested CSS property
+ * for "prefers-reduced-motion: no-preference"
+ * @param {String} valueReduced: Value of the tested CSS property
+ * for "prefers-reduced-motion: reduce"
+ */
+async function testReducedMotionProp(el, prop, valueNotReduced, valueReduced) {
+ info(`Test the panel's CSS ${prop} value depending on a reduced motion mode`);
+
+ // Set "prefers-reduced-motion" media to "no-preference"
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 0]],
+ });
+
+ ok(
+ matchMedia("(prefers-reduced-motion: no-preference)").matches,
+ "The reduce motion mode is not active"
+ );
+ is(
+ getComputedStyle(el).getPropertyValue(prop),
+ valueNotReduced,
+ `Default ${prop} will be provided, when a reduce motion mode is not active`
+ );
+
+ // Set "prefers-reduced-motion" media to "reduce"
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 1]],
+ });
+
+ ok(
+ matchMedia("(prefers-reduced-motion: reduce)").matches,
+ "The reduce motion mode is active"
+ );
+ is(
+ getComputedStyle(el).getPropertyValue(prop),
+ valueReduced,
+ `Reduced ${prop} will be provided, when a reduce motion mode is active`
+ );
+}
+
+async function verifyPickerPosition(browsingContext, inputId) {
+ let inputRect = await SpecialPowers.spawn(
+ browsingContext,
+ [inputId],
+ async function (inputIdChild) {
+ let rect = content.document
+ .getElementById(inputIdChild)
+ .getBoundingClientRect();
+ return {
+ left: content.mozInnerScreenX + rect.left,
+ bottom: content.mozInnerScreenY + rect.bottom,
+ };
+ }
+ );
+
+ function is_close(got, exp, msg) {
+ // on some platforms we see differences of a fraction of a pixel - so
+ // allow any difference of < 1 pixels as being OK.
+ Assert.ok(
+ Math.abs(got - exp) < 1,
+ msg + ": " + got + " should be equal(-ish) to " + exp
+ );
+ }
+ const marginLeft = parseFloat(getComputedStyle(helper.panel).marginLeft);
+ const marginTop = parseFloat(getComputedStyle(helper.panel).marginTop);
+ is_close(
+ helper.panel.screenX - marginLeft,
+ inputRect.left,
+ "datepicker x position"
+ );
+ is_close(
+ helper.panel.screenY - marginTop,
+ inputRect.bottom,
+ "datepicker y position"
+ );
+}
diff --git a/toolkit/content/tests/browser/doggy.png b/toolkit/content/tests/browser/doggy.png
new file mode 100644
index 0000000000..73632d3229
--- /dev/null
+++ b/toolkit/content/tests/browser/doggy.png
Binary files differ
diff --git a/toolkit/content/tests/browser/empty.png b/toolkit/content/tests/browser/empty.png
new file mode 100644
index 0000000000..17ddf0c3ee
--- /dev/null
+++ b/toolkit/content/tests/browser/empty.png
Binary files differ
diff --git a/toolkit/content/tests/browser/file_contentTitle.html b/toolkit/content/tests/browser/file_contentTitle.html
new file mode 100644
index 0000000000..8d330aa0f2
--- /dev/null
+++ b/toolkit/content/tests/browser/file_contentTitle.html
@@ -0,0 +1,14 @@
+<html>
+<head><title>Test Page</title></head>
+<body>
+<script type="text/javascript">
+dump("Script!\n");
+addEventListener("load", () => {
+ // Trigger an onLocationChange event. We want to make sure the title is still correct afterwards.
+ location.hash = "#x2";
+ var event = new Event("TestLocationChange");
+ document.dispatchEvent(event);
+}, false);
+</script>
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/file_document_open_audio.html b/toolkit/content/tests/browser/file_document_open_audio.html
new file mode 100644
index 0000000000..1234299c67
--- /dev/null
+++ b/toolkit/content/tests/browser/file_document_open_audio.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>Test for bug 1572798</title>
+<script>
+ function openVideo() {
+ var w = window.open('', '', 'width = 640, height = 480, scrollbars=yes, menubar=no, toolbar=no, resizable=yes');
+ w.document.open();
+ w.document.write('<!DOCTYPE html><title>test popup</title><audio controls src="audio.ogg"></audio>');
+ w.document.close();
+ }
+</script>
+<button onclick="openVideo()">Open video</button>
diff --git a/toolkit/content/tests/browser/file_empty.html b/toolkit/content/tests/browser/file_empty.html
new file mode 100644
index 0000000000..d2b0361f09
--- /dev/null
+++ b/toolkit/content/tests/browser/file_empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Page left intentionally blank...</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/toolkit/content/tests/browser/file_findinframe.html b/toolkit/content/tests/browser/file_findinframe.html
new file mode 100644
index 0000000000..27a9d00a97
--- /dev/null
+++ b/toolkit/content/tests/browser/file_findinframe.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ <iframe src="data:text/html,&lt;body contenteditable&gt;Test&lt;/body&gt;"></iframe>
+ </body>
+</html>
diff --git a/toolkit/content/tests/browser/file_iframe_media.html b/toolkit/content/tests/browser/file_iframe_media.html
new file mode 100644
index 0000000000..929fb84002
--- /dev/null
+++ b/toolkit/content/tests/browser/file_iframe_media.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<video id="video" src="gizmo.mp4" loop></video>
+<script type="text/javascript">
+
+window.onmessage = async event => {
+ const video = document.getElementById("video");
+ const w = window.opener || window.parent;
+ if (event.data == "play") {
+ await video.play();
+ w.postMessage("played", "*");
+ }
+}
+
+</script>
diff --git a/toolkit/content/tests/browser/file_mediaPlayback2.html b/toolkit/content/tests/browser/file_mediaPlayback2.html
new file mode 100644
index 0000000000..890b494a05
--- /dev/null
+++ b/toolkit/content/tests/browser/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/toolkit/content/tests/browser/file_multipleAudio.html b/toolkit/content/tests/browser/file_multipleAudio.html
new file mode 100644
index 0000000000..5dc37febb4
--- /dev/null
+++ b/toolkit/content/tests/browser/file_multipleAudio.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+</head>
+<body>
+<audio id="autoplay" src="audio.ogg"></audio>
+<audio id="nonautoplay" src="audio.ogg"></audio>
+<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 audio = document.getElementById("autoplay");
+audio.loop = true;
+audio.play();
+
+</script>
+</body>
diff --git a/toolkit/content/tests/browser/file_multiplePlayingAudio.html b/toolkit/content/tests/browser/file_multiplePlayingAudio.html
new file mode 100644
index 0000000000..ae122506fb
--- /dev/null
+++ b/toolkit/content/tests/browser/file_multiplePlayingAudio.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+</head>
+<body>
+<audio id="audio1" src="audio.ogg" controls></audio>
+<audio id="audio2" src="audio.ogg" controls></audio>
+<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 audio1 = document.getElementById("audio1");
+audio1.loop = true;
+audio1.play();
+
+var audio2 = document.getElementById("audio2");
+audio2.loop = true;
+audio2.play();
+
+</script>
+</body>
diff --git a/toolkit/content/tests/browser/file_nonAutoplayAudio.html b/toolkit/content/tests/browser/file_nonAutoplayAudio.html
new file mode 100644
index 0000000000..4d2641021a
--- /dev/null
+++ b/toolkit/content/tests/browser/file_nonAutoplayAudio.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+</head>
+<body>
+<audio id="testAudio" src="audio.ogg" loop></audio>
diff --git a/toolkit/content/tests/browser/file_outside_viewport_videos.html b/toolkit/content/tests/browser/file_outside_viewport_videos.html
new file mode 100644
index 0000000000..84aa34358d
--- /dev/null
+++ b/toolkit/content/tests/browser/file_outside_viewport_videos.html
@@ -0,0 +1,41 @@
+<html>
+<head>
+ <title>outside viewport videos</title>
+<style>
+/**
+ * These CSS would move elements to the far left/right/top/bottom where user
+ * can not see elements in the viewport if user doesn't scroll the page.
+ */
+.outside-left {
+ position: absolute;
+ left: -1000%;
+}
+.outside-right {
+ position: absolute;
+ right: -1000%;
+}
+.outside-top {
+ position: absolute;
+ top: -1000%;
+}
+.outside-bottom {
+ position: absolute;
+ bottom: -1000%;
+}
+</style>
+</head>
+<body>
+ <div class="outside-left">
+ <video id="left" src="gizmo.mp4">
+ </div>
+ <div class="outside-right">
+ <video id="right" src="gizmo.mp4">
+ </div>
+ <div class="outside-top">
+ <video id="top" src="gizmo.mp4">
+ </div>
+ <div class="outside-bottom">
+ <video id="bottom" src="gizmo.mp4">
+ </div>
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/file_redirect.html b/toolkit/content/tests/browser/file_redirect.html
new file mode 100644
index 0000000000..4d5fa9dfd1
--- /dev/null
+++ b/toolkit/content/tests/browser/file_redirect.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>redirecting...</title>
+<script>
+window.addEventListener("load",
+ () => window.location = "file_redirect_to.html");
+</script>
+<body>
+redirectin u bro
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/file_redirect_to.html b/toolkit/content/tests/browser/file_redirect_to.html
new file mode 100644
index 0000000000..28c0b53713
--- /dev/null
+++ b/toolkit/content/tests/browser/file_redirect_to.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>redirected!</title>
+<script>
+window.addEventListener("load", () => {
+ var event = new Event("RedirectDone");
+ document.dispatchEvent(event);
+});
+</script>
+<body>
+u got redirected, bro
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/file_silentAudioTrack.html b/toolkit/content/tests/browser/file_silentAudioTrack.html
new file mode 100644
index 0000000000..afdf2c5297
--- /dev/null
+++ b/toolkit/content/tests/browser/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/toolkit/content/tests/browser/file_video.html b/toolkit/content/tests/browser/file_video.html
new file mode 100644
index 0000000000..3c70268fbb
--- /dev/null
+++ b/toolkit/content/tests/browser/file_video.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>video</title>
+</head>
+<body>
+<video id="v" src="gizmo.mp4" controls loop></video>
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/file_videoWithAudioOnly.html b/toolkit/content/tests/browser/file_videoWithAudioOnly.html
new file mode 100644
index 0000000000..be84d60c34
--- /dev/null
+++ b/toolkit/content/tests/browser/file_videoWithAudioOnly.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>video</title>
+</head>
+<body>
+<video id="v" src="audio.ogg" controls loop></video>
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/file_videoWithoutAudioTrack.html b/toolkit/content/tests/browser/file_videoWithoutAudioTrack.html
new file mode 100644
index 0000000000..a732b7c9d0
--- /dev/null
+++ b/toolkit/content/tests/browser/file_videoWithoutAudioTrack.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>video without audio track</title>
+</head>
+<body>
+<video id="v" src="gizmo-noaudio.webm" controls loop></video>
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/file_webAudio.html b/toolkit/content/tests/browser/file_webAudio.html
new file mode 100644
index 0000000000..f6fb5e7c07
--- /dev/null
+++ b/toolkit/content/tests/browser/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/toolkit/content/tests/browser/firebird.png b/toolkit/content/tests/browser/firebird.png
new file mode 100644
index 0000000000..de5c22f8ce
--- /dev/null
+++ b/toolkit/content/tests/browser/firebird.png
Binary files differ
diff --git a/toolkit/content/tests/browser/firebird.png^headers^ b/toolkit/content/tests/browser/firebird.png^headers^
new file mode 100644
index 0000000000..2918fdbe5f
--- /dev/null
+++ b/toolkit/content/tests/browser/firebird.png^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Found
+Location: doggy.png
diff --git a/toolkit/content/tests/browser/gizmo-noaudio.webm b/toolkit/content/tests/browser/gizmo-noaudio.webm
new file mode 100644
index 0000000000..9f412cb6e3
--- /dev/null
+++ b/toolkit/content/tests/browser/gizmo-noaudio.webm
Binary files differ
diff --git a/toolkit/content/tests/browser/gizmo.mp4 b/toolkit/content/tests/browser/gizmo.mp4
new file mode 100644
index 0000000000..87efad5ade
--- /dev/null
+++ b/toolkit/content/tests/browser/gizmo.mp4
Binary files differ
diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js
new file mode 100644
index 0000000000..2cf411479a
--- /dev/null
+++ b/toolkit/content/tests/browser/head.js
@@ -0,0 +1,244 @@
+"use strict";
+
+/**
+ * Set the findbar value to the given text, start a search for that text, and
+ * return a promise that resolves when the find has completed.
+ *
+ * @param gBrowser tabbrowser to search in the current tab.
+ * @param searchText text to search for.
+ * @param highlightOn true if highlight mode should be enabled before searching.
+ * @returns Promise resolves when find is complete.
+ */
+async function promiseFindFinished(gBrowser, searchText, highlightOn = false) {
+ let findbar = await gBrowser.getFindBar();
+ findbar.startFind(findbar.FIND_NORMAL);
+ let highlightElement = findbar.getElement("highlight");
+ if (highlightElement.checked != highlightOn) {
+ highlightElement.click();
+ }
+ return new Promise(resolve => {
+ executeSoon(() => {
+ findbar._findField.value = searchText;
+
+ let resultListener;
+ // When highlighting is on the finder sends a second "FOUND" message after
+ // the search wraps. This causes timing problems with e10s. waitMore
+ // forces foundOrTimeout wait for the second "FOUND" message before
+ // resolving the promise.
+ let waitMore = highlightOn;
+ let findTimeout = setTimeout(() => foundOrTimedout(null), 5000);
+ let foundOrTimedout = function (aData) {
+ if (aData !== null && waitMore) {
+ waitMore = false;
+ return;
+ }
+ if (aData === null) {
+ info("Result listener not called, timeout reached.");
+ }
+ clearTimeout(findTimeout);
+ findbar.browser?.finder.removeResultListener(resultListener);
+ resolve();
+ };
+
+ resultListener = {
+ onFindResult: foundOrTimedout,
+ onCurrentSelection() {},
+ onMatchesCountResult() {},
+ onHighlightFinished() {},
+ };
+ findbar.browser.finder.addResultListener(resultListener);
+ findbar._find();
+ });
+ });
+}
+
+/**
+ * A wrapper for the findbar's method "close", which is not synchronous
+ * because of animation.
+ */
+function closeFindbarAndWait(findbar) {
+ return new Promise(resolve => {
+ if (findbar.hidden) {
+ resolve();
+ return;
+ }
+ findbar.addEventListener("transitionend", function cont(aEvent) {
+ if (aEvent.propertyName != "visibility") {
+ return;
+ }
+ findbar.removeEventListener("transitionend", cont);
+ resolve();
+ });
+ let close = findbar.getElement("find-closebutton");
+ close.doCommand();
+ });
+}
+
+function pushPrefs(...aPrefs) {
+ return new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: aPrefs }, resolve);
+ });
+}
+
+/**
+ * Used to check whether the audio unblocking icon is in the tab.
+ */
+async function waitForTabBlockEvent(tab, expectBlocked) {
+ if (tab.activeMediaBlocked == expectBlocked) {
+ ok(true, "The tab should " + (expectBlocked ? "" : "not ") + "be blocked");
+ } else {
+ info("Block state doens't match, wait for attributes changes.");
+ await BrowserTestUtils.waitForEvent(
+ tab,
+ "TabAttrModified",
+ false,
+ event => {
+ if (event.detail.changed.includes("activemedia-blocked")) {
+ is(
+ tab.activeMediaBlocked,
+ expectBlocked,
+ "The tab should " + (expectBlocked ? "" : "not ") + "be blocked"
+ );
+ return true;
+ }
+ return false;
+ }
+ );
+ }
+}
+
+/**
+ * Used to check whether the tab has soundplaying attribute.
+ */
+async function waitForTabPlayingEvent(tab, expectPlaying) {
+ if (tab.soundPlaying == expectPlaying) {
+ ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+ } else {
+ info("Playing state doesn't match, wait for attributes changes.");
+ await BrowserTestUtils.waitForEvent(
+ tab,
+ "TabAttrModified",
+ false,
+ event => {
+ if (event.detail.changed.includes("soundplaying")) {
+ is(
+ tab.soundPlaying,
+ expectPlaying,
+ "The tab should " + (expectPlaying ? "" : "not ") + "be playing"
+ );
+ return true;
+ }
+ return false;
+ }
+ );
+ }
+}
+
+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);
+}
+
+/**
+ * Used to listen events if you just need it once
+ */
+function once(target, name) {
+ var p = new Promise(function (resolve, reject) {
+ target.addEventListener(
+ name,
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ return p;
+}
+
+/**
+ * check if current wakelock is equal to expected state, if not, then wait until
+ * the wakelock changes its state to expected state.
+ * @param needLock
+ * the wakolock should be locked or not
+ * @param isForegroundLock
+ * when the lock is on, the wakelock should be in the foreground or not
+ */
+async function waitForExpectedWakeLockState(
+ topic,
+ { needLock, isForegroundLock }
+) {
+ const powerManagerService = Cc["@mozilla.org/power/powermanagerservice;1"];
+ const powerManager = powerManagerService.getService(
+ Ci.nsIPowerManagerService
+ );
+ const wakelockState = powerManager.getWakeLockState(topic);
+ let expectedLockState = "unlocked";
+ if (needLock) {
+ expectedLockState = isForegroundLock
+ ? "locked-foreground"
+ : "locked-background";
+ }
+ if (wakelockState != expectedLockState) {
+ info(`wait until wakelock becomes ${expectedLockState}`);
+ await wakeLockObserved(
+ powerManager,
+ topic,
+ state => state == expectedLockState
+ );
+ }
+ is(
+ powerManager.getWakeLockState(topic),
+ expectedLockState,
+ `the wakelock state for '${topic}' is equal to '${expectedLockState}'`
+ );
+}
+
+function wakeLockObserved(powerManager, observeTopic, checkFn) {
+ return new Promise(resolve => {
+ function wakeLockListener() {}
+ wakeLockListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMMozWakeLockListener"]),
+ callback(topic, state) {
+ if (topic == observeTopic && checkFn(state)) {
+ powerManager.removeWakeLockListener(wakeLockListener.prototype);
+ resolve();
+ }
+ },
+ };
+ powerManager.addWakeLockListener(wakeLockListener.prototype);
+ });
+}
+
+function getTestWebBasedURL(fileName, { crossOrigin = false } = {}) {
+ const origin = crossOrigin ? "http://example.org" : "http://example.com";
+ return (
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ fileName
+ );
+}
diff --git a/toolkit/content/tests/browser/image.jpg b/toolkit/content/tests/browser/image.jpg
new file mode 100644
index 0000000000..5031808ad2
--- /dev/null
+++ b/toolkit/content/tests/browser/image.jpg
Binary files differ
diff --git a/toolkit/content/tests/browser/image_page.html b/toolkit/content/tests/browser/image_page.html
new file mode 100644
index 0000000000..522a1d8cf9
--- /dev/null
+++ b/toolkit/content/tests/browser/image_page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>OHAI</title>
+<body>
+<img id="image" src="image.jpg" />
+</body>
+</html>
diff --git a/toolkit/content/tests/browser/silentAudioTrack.webm b/toolkit/content/tests/browser/silentAudioTrack.webm
new file mode 100644
index 0000000000..8e08a86c45
--- /dev/null
+++ b/toolkit/content/tests/browser/silentAudioTrack.webm
Binary files differ