diff options
Diffstat (limited to '')
106 files changed, 12464 insertions, 0 deletions
diff --git a/toolkit/content/tests/browser/audio.ogg b/toolkit/content/tests/browser/audio.ogg Binary files differnew file mode 100644 index 0000000000..7f1833508a --- /dev/null +++ b/toolkit/content/tests/browser/audio.ogg 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.js b/toolkit/content/tests/browser/browser_default_image_filename.js new file mode 100644 index 0000000000..9add704664 --- /dev/null +++ b/toolkit/content/tests/browser/browser_default_image_filename.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +const DATA_IMAGE_GIF_URL = + "data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7"; +registerCleanupFunction(function () { + MockFilePicker.cleanup(); +}); +/** + * TestCase for bug 564387 + * <https://bugzilla.mozilla.org/show_bug.cgi?id=564387> + */ +add_task(async function () { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: DATA_IMAGE_GIF_URL, + }, + async function (browser) { + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "img", + { + type: "contextmenu", + button: 2, + }, + browser + ); + + await popupShownPromise; + + let showFilePickerPromise = new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + is(fp.defaultString, "Untitled.gif"); + resolve(); + }; + }); + + // Select "Save Image As" option from context menu + var saveImageAsCommand = document.getElementById("context-saveimage"); + 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 () { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: DATA_IMAGE_GIF_URL, + }, + async function (browser) { + let showFilePickerPromise = new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + is(fp.defaultString, "Untitled.gif"); + 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 Binary files differnew file mode 100644 index 0000000000..73632d3229 --- /dev/null +++ b/toolkit/content/tests/browser/doggy.png diff --git a/toolkit/content/tests/browser/empty.png b/toolkit/content/tests/browser/empty.png Binary files differnew file mode 100644 index 0000000000..17ddf0c3ee --- /dev/null +++ b/toolkit/content/tests/browser/empty.png 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,<body contenteditable>Test</body>"></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 Binary files differnew file mode 100644 index 0000000000..de5c22f8ce --- /dev/null +++ b/toolkit/content/tests/browser/firebird.png 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 Binary files differnew file mode 100644 index 0000000000..9f412cb6e3 --- /dev/null +++ b/toolkit/content/tests/browser/gizmo-noaudio.webm diff --git a/toolkit/content/tests/browser/gizmo.mp4 b/toolkit/content/tests/browser/gizmo.mp4 Binary files differnew file mode 100644 index 0000000000..87efad5ade --- /dev/null +++ b/toolkit/content/tests/browser/gizmo.mp4 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 Binary files differnew file mode 100644 index 0000000000..5031808ad2 --- /dev/null +++ b/toolkit/content/tests/browser/image.jpg 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 Binary files differnew file mode 100644 index 0000000000..8e08a86c45 --- /dev/null +++ b/toolkit/content/tests/browser/silentAudioTrack.webm |